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"; } /** TMDO router shape with post_status / post_date copies (PR-029): INNER JOIN, no GROUP BY. */ function flat_copy_search( string $table, string $alias, string $conditions ): string { return 'SELECT SQL_CALC_FOUND_ROWS p.ID FROM `' . P . "posts` p INNER 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} AND `{$alias}`.`post_status` IN ('publish') ORDER BY `{$alias}`.`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()' ); }, 'entity+copy' => function () use ( $price ) { [ $lo, $hi ] = $price(); return array( flat_copy_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()' ); }, 'entity+copy' => function () use ( $price ) { [ $lo, $hi ] = $price(); return array( flat_copy_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 ); } }