Files
2meet-data-optimizer/tests/bench/run.php
T
wpdev d3b72bd1af test(2meet-data-optimizer): add scale benchmark report (native vs hot vs entity)
- Benchmark 7 scenarios at 100k vendors / 1M listings with SQL shaped like
  WP_Query, TMDO_Query_Router and TMDO_Post_Query_Router
- Native meta_query takes 14-17s; flat tables bring searches to 0.3-0.6s
- Hot table lacks an hp_vendor index (3.4s); entity lacks composite indexes

Verify: TMDO_TEST_DB_NAME=wp_wpdo_bench php tests/bench/run.php --iterations=20
PR: PR-005
2026-09-25 21:29:58 +08:00

250 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Scale benchmark: native postmeta vs hot table vs entity table.
*
* Runs against the dataset from tests/bench/seed.php:
* TMDO_TEST_DB_NAME=wp_wpdo_bench php tests/bench/run.php [--iterations=20] [--seed=7]
*
* SQL mirrors what the code under test emits: WP_Query meta_query (INNER JOIN
* per clause, CAST for numerics, SQL_CALC_FOUND_ROWS + FOUND_ROWS()),
* TMDO_Query_Router (hot) and TMDO_Post_Query_Router (entity, %s-quoted values).
* Writes run inside a transaction that is rolled back, so the dataset never changes.
*
* Output: a Markdown table on stdout (p50 / p95 / max in ms, result rows, index used).
*
* @package TMDO\Tests\Bench
*/
declare(strict_types=1);
if ( 'cli' !== PHP_SAPI ) {
exit( 1 );
}
require_once __DIR__ . '/class-tmdo-bench-seeder.php';
const P = 'wp_itest_';
$opts = getopt( '', array( 'iterations::', 'seed::' ) );
$iterations = max( 1, (int) ( $opts['iterations'] ?? 20 ) );
$warmup = 3;
$db_name = (string) getenv( 'TMDO_TEST_DB_NAME' );
try {
TMDO_Bench_Seeder::assert_bench_db( $db_name );
} catch ( InvalidArgumentException $e ) {
fwrite( STDERR, $e->getMessage() . PHP_EOL );
exit( 2 );
}
$db = new mysqli(
getenv( 'TMDO_TEST_DB_HOST' ) ?: '127.0.0.1',
getenv( 'TMDO_TEST_DB_USER' ) ?: 'dbo',
(string) getenv( 'TMDO_TEST_DB_PASS' ),
$db_name
);
$db->set_charset( 'utf8mb4' );
$db->query( 'SET SESSION max_statement_time = 30' ); // MariaDB: cap runaway native queries.
$vendors = (int) $db->query( 'SELECT COUNT(*) FROM `' . P . "posts` WHERE post_type = 'hp_vendor'" )->fetch_row()[0];
$listings = (int) $db->query( 'SELECT COUNT(*) FROM `' . P . "posts` WHERE post_type = 'hp_listing'" )->fetch_row()[0];
if ( 0 === $vendors || 0 === $listings ) {
fwrite( STDERR, "No bench data — run tests/bench/seed.php first.\n" );
exit( 3 );
}
mt_srand( (int) ( $opts['seed'] ?? 7 ) );
// ── SQL builders ──────────────────────────────────────────────────────────
/** WP_Query shape with one INNER JOIN per meta clause. */
function native_search( array $meta_where ): string {
$joins = '';
$where = array();
foreach ( $meta_where as $n => $cond ) {
$alias = 0 === $n ? P . 'postmeta' : "mt{$n}";
$joins .= ' INNER JOIN `' . P . 'postmeta`' . ( 0 === $n ? '' : " AS {$alias}" ) . " ON ( p.ID = {$alias}.post_id )";
$where[] = '( ' . sprintf( $cond, $alias ) . ' )';
}
return 'SELECT SQL_CALC_FOUND_ROWS p.ID FROM `' . P . "posts` p{$joins} WHERE 1=1 AND ( " . implode( ' AND ', $where )
. " ) AND p.post_type = 'hp_listing' AND ((p.post_status = 'publish')) GROUP BY p.ID ORDER BY p.post_date DESC LIMIT 0, 20";
}
/** TMDO router shape: one LEFT JOIN to a flat table plus flat WHERE clauses. */
function flat_search( string $table, string $alias, string $conditions ): string {
return 'SELECT SQL_CALC_FOUND_ROWS p.ID FROM `' . P . "posts` p LEFT JOIN `" . P . "{$table}` AS `{$alias}` ON (p.ID = `{$alias}`.`post_id`)"
. " WHERE 1=1 AND p.post_type = 'hp_listing' AND ((p.post_status = 'publish')){$conditions} GROUP BY p.ID ORDER BY p.post_date DESC LIMIT 0, 20";
}
function random_ids( int $count, int $offset, int $max ): string {
$ids = array();
for ( $i = 0; $i < $count; $i++ ) {
$ids[] = $offset + mt_rand( 1, $max );
}
return implode( ',', $ids );
}
// ── Scenarios: label => [ variant => fn(): string[] (statements) ] ─────────
$price = static function (): array {
$lo = mt_rand( 100, 80000 );
return array( $lo, $lo + 20000 ); // ~20% of the 100..100000 range.
};
$scenarios = array(
'1. 價格區間搜尋(約 20%)' => array(
'native' => function () use ( $price ) {
[ $lo, $hi ] = $price();
return array( native_search( array( "%1\$s.meta_key = 'hp_price' AND CAST(%1\$s.meta_value AS DECIMAL(10,2)) BETWEEN '{$lo}' AND '{$hi}'" ) ), 'SELECT FOUND_ROWS()' );
},
'hot' => function () use ( $price ) {
[ $lo, $hi ] = $price();
return array( flat_search( 'wpdo_hot_hp_listing', 'h', " AND `h`.`hp_price` BETWEEN {$lo} AND {$hi}" ), 'SELECT FOUND_ROWS()' );
},
'entity' => function () use ( $price ) {
[ $lo, $hi ] = $price();
return array( flat_search( 'wpdo_post_hp_listing_core', 'e', " AND `e`.`hp_price` BETWEEN '{$lo}' AND '{$hi}'" ), 'SELECT FOUND_ROWS()' );
},
),
'2. 價格區間 + verified=1' => array(
'native' => function () use ( $price ) {
[ $lo, $hi ] = $price();
return array(
native_search(
array(
"%1\$s.meta_key = 'hp_price' AND CAST(%1\$s.meta_value AS DECIMAL(10,2)) BETWEEN '{$lo}' AND '{$hi}'",
"%1\$s.meta_key = 'hp_verified' AND %1\$s.meta_value = '1'",
)
),
'SELECT FOUND_ROWS()',
);
},
'hot' => function () use ( $price ) {
[ $lo, $hi ] = $price();
return array( flat_search( 'wpdo_hot_hp_listing', 'h', " AND `h`.`hp_price` BETWEEN {$lo} AND {$hi} AND `h`.`hp_verified` = 1" ), 'SELECT FOUND_ROWS()' );
},
'entity' => function () use ( $price ) {
[ $lo, $hi ] = $price();
return array( flat_search( 'wpdo_post_hp_listing_core', 'e', " AND `e`.`hp_price` BETWEEN '{$lo}' AND '{$hi}' AND `e`.`hp_verified` = '1'" ), 'SELECT FOUND_ROWS()' );
},
),
'3. 某 vendor 名下 listing' => array(
'post_parent' => fn() => array( 'SELECT SQL_CALC_FOUND_ROWS p.ID FROM `' . P . "posts` p WHERE p.post_parent = " . mt_rand( 1, $vendors ) . " AND p.post_type = 'hp_listing' AND ((p.post_status = 'publish')) ORDER BY p.post_date DESC LIMIT 0, 20", 'SELECT FOUND_ROWS()' ),
'native' => fn() => array( native_search( array( "%1\$s.meta_key = 'hp_vendor' AND %1\$s.meta_value = '" . mt_rand( 1, $vendors ) . "'" ) ), 'SELECT FOUND_ROWS()' ),
'hot' => fn() => array( flat_search( 'wpdo_hot_hp_listing', 'h', ' AND `h`.`hp_vendor` = ' . mt_rand( 1, $vendors ) ), 'SELECT FOUND_ROWS()' ),
'entity' => fn() => array( flat_search( 'wpdo_post_hp_listing_core', 'e', " AND `e`.`hp_vendor` = '" . mt_rand( 1, $vendors ) . "'" ), 'SELECT FOUND_ROWS()' ),
),
'4. 單筆 listing 讀全部 meta' => array(
'native' => fn() => array( 'SELECT post_id, meta_key, meta_value FROM `' . P . 'postmeta` WHERE post_id IN (' . random_ids( 1, $vendors, $listings ) . ') ORDER BY meta_id ASC' ),
'hot' => fn() => array( 'SELECT * FROM `' . P . 'wpdo_hot_hp_listing` WHERE post_id = ' . random_ids( 1, $vendors, $listings ) ),
'entity' => fn() => array( 'SELECT * FROM `' . P . 'wpdo_post_hp_listing_core` WHERE post_id = ' . random_ids( 1, $vendors, $listings ) ),
),
'5. 列表頁預載 20 筆 meta' => array(
'native' => fn() => array( 'SELECT post_id, meta_key, meta_value FROM `' . P . 'postmeta` WHERE post_id IN (' . random_ids( 20, $vendors, $listings ) . ') ORDER BY meta_id ASC' ),
'hot' => fn() => array( 'SELECT * FROM `' . P . 'wpdo_hot_hp_listing` WHERE post_id IN (' . random_ids( 20, $vendors, $listings ) . ')' ),
'entity' => fn() => array( 'SELECT * FROM `' . P . 'wpdo_post_hp_listing_core` WHERE post_id IN (' . random_ids( 20, $vendors, $listings ) . ')' ),
),
'6. 單一 vendor 讀使用者 meta' => array(
'native' => fn() => array( 'SELECT user_id, meta_key, meta_value FROM `' . P . 'usermeta` WHERE user_id IN (' . mt_rand( 1, $vendors ) . ') ORDER BY umeta_id ASC' ),
'entity' => fn() => array( 'SELECT * FROM `' . P . 'wpdo_user_hp_user` WHERE user_id = ' . mt_rand( 1, $vendors ) ),
),
'7. 寫入:更新一筆 listing 價格' => array(
'native' => fn() => array( 'UPDATE `' . P . "postmeta` SET meta_value = '" . mt_rand( 100, 100000 ) . ".00' WHERE post_id = " . random_ids( 1, $vendors, $listings ) . " AND meta_key = 'hp_price'" ),
'entity' => fn() => array( 'UPDATE `' . P . "wpdo_post_hp_listing_core` SET hp_price = '" . mt_rand( 100, 100000 ) . "' WHERE post_id = " . random_ids( 1, $vendors, $listings ) ),
'dual_write' => function () use ( $vendors, $listings ) {
$id = random_ids( 1, $vendors, $listings );
$v = mt_rand( 100, 100000 );
return array(
'UPDATE `' . P . "postmeta` SET meta_value = '{$v}.00' WHERE post_id = {$id} AND meta_key = 'hp_price'",
'UPDATE `' . P . "wpdo_post_hp_listing_core` SET hp_price = '{$v}' WHERE post_id = {$id}",
);
},
),
);
// ── Run ───────────────────────────────────────────────────────────────────
/**
* @return array{ms: float, rows: int|string, timeout: bool}
*/
function run_statements( mysqli $db, array $statements ): array {
$is_write = str_starts_with( $statements[0], 'UPDATE' );
if ( $is_write ) {
$db->begin_transaction();
}
$rows = 0;
$timeout = false;
$start = hrtime( true );
foreach ( $statements as $n => $sql ) {
try {
$res = $db->query( $sql );
} catch ( mysqli_sql_exception $e ) {
if ( 1969 !== $e->getCode() ) { // ER_STATEMENT_TIMEOUT.
throw $e;
}
$timeout = true;
break;
}
if ( $res instanceof mysqli_result ) {
$rows = str_starts_with( $sql, 'SELECT FOUND_ROWS' ) ? (int) $res->fetch_row()[0] : ( 0 === $n ? $res->num_rows : $rows );
$res->free();
} elseif ( $is_write ) {
$rows = $db->affected_rows;
}
}
$ms = ( hrtime( true ) - $start ) / 1e6;
if ( $is_write ) {
$db->rollback();
}
return array( 'ms' => $ms, 'rows' => $rows, 'timeout' => $timeout );
}
function explain_keys( mysqli $db, string $sql ): string {
if ( ! str_starts_with( $sql, 'SELECT' ) ) {
return '—';
}
$keys = array();
$res = $db->query( 'EXPLAIN ' . $sql );
while ( $row = $res->fetch_assoc() ) {
$keys[] = $row['table'] . ':' . ( $row['key'] ?? 'ALL' );
}
return implode( ', ', $keys );
}
function percentile( array $sorted, float $p ): float {
return $sorted[ (int) min( count( $sorted ) - 1, floor( $p * count( $sorted ) ) ) ];
}
printf( "Dataset: %d vendors, %d listings — %d iterations (+%d warm-up) per variant\n\n", $vendors, $listings, $iterations, $warmup );
echo "| 情境 | 形式 | p50 ms | p95 ms | max ms | 結果筆數(中位) | 使用索引 |\n|---|---|---:|---:|---:|---:|---|\n";
foreach ( $scenarios as $label => $variants ) {
foreach ( $variants as $variant => $build ) {
$times = array();
$rows = array();
$timeouts = 0;
$keys = explain_keys( $db, $build()[0] );
for ( $i = 0; $i < $warmup + $iterations; $i++ ) {
$r = run_statements( $db, $build() );
if ( $i < $warmup ) {
continue;
}
$timeouts += (int) $r['timeout'];
$times[] = $r['ms'];
$rows[] = $r['rows'];
}
sort( $times );
sort( $rows );
printf(
"| %s | %s | %.2f | %.2f | %.2f | %s | %s |\n",
$label,
$variant . ( $timeouts ? " ⚠ {$timeouts}× timeout" : '' ),
percentile( $times, 0.5 ),
percentile( $times, 0.95 ),
end( $times ),
$rows[ intdiv( count( $rows ), 2 ) ],
$keys
);
}
}