perf(2meet-data-optimizer): route sort-only searches from the flat table when every post has a row

- A sort-only search (HivePress _sort, no routed filter) LEFT JOINs the flat table to keep posts without a row; it now drives from the flat table (STRAIGHT_JOIN, INNER JOIN) when every post of the type has a flat row. The router gives a new post of a type with its own groups an empty row (wp_insert_post); coverage is one anti-join per group, cached 15 min and per post-mode change (forget_coverage()).
- A flat-first search drops the GROUP BY posts.ID WordPress adds for any tax query when the flat row is the only join (WooCommerce's product_visibility NOT IN on searches is a subquery); a joining tax query keeps it.
- Verify: live router suite (5 new cases, RED before); dev36 rating__desc page, ids identical: native ~3.3 s, 2.1 routing ~1.5–1.9 s, now ~0.51–0.85 s (main query 440 → 46 ms, COUNT 400 → 160 ms). p50 ≤ 300 ms is still not met: page rendering and HivePress's own get_posts() in pre_get_posts (suppress_filters, ~100 ms) remain.
- PR: PR-079
This commit is contained in:
2026-09-27 08:31:47 +08:00
parent ad7acc40b7
commit 84f6e32913
2 changed files with 236 additions and 8 deletions
+100 -4
View File
@@ -69,6 +69,9 @@ class TMDO_Post_Query_Router {
/** Object cache group for found-rows counts. */
private const COUNT_CACHE_GROUP = 'tmdo_found_rows';
/** Seconds a "every post of the type has a flat row" answer is reused. */
private const COVERAGE_TTL = 900;
/** Comparison operators the router can express on a flat column. */
private const COMPARES = array( '=', '!=', '<>', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE', 'BETWEEN', 'NOT BETWEEN', 'EXISTS', 'NOT EXISTS' );
@@ -95,6 +98,30 @@ class TMDO_Post_Query_Router {
add_filter( 'split_the_query', array( $this, 'split_the_query' ), 10, 4 );
add_filter( 'posts_request_ids', array( $this, 'posts_request' ), 10, 2 );
add_filter( 'found_posts_query', array( $this, 'found_posts_query' ), 10, 2 );
add_action( 'wp_insert_post', array( $this, 'add_flat_rows' ), 10, 3 );
}
/**
* A new post of a type with its own groups gets an empty row in each, so
* that "every post of the type has a flat row" keeps holding and sort-only
* searches can drive from the flat table (see flat_first()).
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post.
* @param bool $update Whether an existing post was updated.
* @return void
*/
public function add_flat_rows( $post_id, $post, $update ): void {
if ( $update || ! $post instanceof \WP_Post ) {
return;
}
global $wpdb;
foreach ( TMDO_Post_Fields::groups_for_post_type( $post->post_type ) as $group ) {
$table = TMDO_Schema_Manager::get_table_name( 'post', $group );
if ( TMDO_Schema_Manager::table_exists( $table ) ) {
$wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `{$table}` (`post_id`) VALUES (%d)", (int) $post_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
}
}
/**
@@ -422,6 +449,57 @@ class TMDO_Post_Query_Router {
self::$copies_usable = array();
}
/**
* Every post of the type has a row in the group's flat table. One
* anti-join per group, reused for COVERAGE_TTL and per post-mode change.
* A post inserted by direct SQL (no wp_insert_post) is seen at the next
* check at the latest; the `tmdo_router_flat_first` filter turns the
* flat-first route off.
*
* @param string $post_type Post type.
* @param string $group Entity group name.
* @return bool
*/
private static function covers_all( string $post_type, string $group ): bool {
$key = self::coverage_key( $post_type, $group );
$hit = get_transient( $key );
if ( false === $hit ) {
global $wpdb;
$table = TMDO_Schema_Manager::get_table_name( 'post', $group );
$missing = $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM {$wpdb->posts} p LEFT JOIN `{$table}` f ON f.post_id = p.ID WHERE p.post_type = %s AND f.post_id IS NULL LIMIT 1", $post_type ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$hit = null === $missing ? 'yes' : 'no';
set_transient( $key, $hit, self::COVERAGE_TTL );
}
return 'yes' === $hit;
}
/**
* Forget every cached coverage answer (tests, or after bulk writes that
* bypass wp_insert_post).
*
* @return void
*/
public static function forget_coverage(): void {
foreach ( TMDO_Entity_Registry::get_groups_for_type( 'post' ) as $group ) {
foreach ( TMDO_Entity_Registry::get_group_post_types( 'post', $group ) as $post_type ) {
delete_transient( self::coverage_key( $post_type, $group ) );
}
}
}
/**
* Transient name of a coverage answer; a post-mode change starts a new one.
*
* @param string $post_type Post type.
* @param string $group Entity group name.
* @return string
*/
private static function coverage_key( string $post_type, string $group ): string {
$entered = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() );
$since = is_array( $entered ) ? (int) ( $entered['post'] ?? 0 ) : 0;
return 'tmdo_cov_' . md5( "{$post_type}:{$group}:{$since}" );
}
/**
* The status / date copy route for a routed query, or [] when it does not apply.
*
@@ -621,15 +699,26 @@ class TMDO_Post_Query_Router {
private static function flat_first( string $request, \WP_Query $query ): string {
$routed = $query->get( self::QUERY_VAR );
$sort = $query->get( self::SORT_VAR );
if ( empty( $routed ) || ! is_array( $routed ) || ! empty( $query->get( self::COPY_VAR ) ) ) {
if ( empty( $sort ) || ! is_array( $sort ) || ! empty( $query->get( self::COPY_VAR ) ) ) {
return $request;
}
if ( empty( $routed ) || ! is_array( $routed ) ) {
// Sort only: the LEFT JOIN keeps posts without a flat row, so the
// flat table may drive only when every post of the type has one.
$post_type = (string) $sort['post_type'];
$group = (string) $sort['group'];
if ( ! self::covers_all( $post_type, $group ) ) {
return $request;
}
} else {
$post_type = (string) array_key_first( $routed );
$groups = array_unique( array_column( $routed[ $post_type ], 'group' ) );
$compares = array_diff( array_column( $routed[ $post_type ], 'compare' ), array( 'NOT EXISTS' ) );
if ( empty( $sort ) || 1 !== count( $groups ) || empty( $compares ) || $sort['group'] !== $groups[0] ) {
if ( 1 !== count( $groups ) || empty( $compares ) || $sort['group'] !== $groups[0] ) {
return $request;
}
$group = (string) $groups[0];
}
foreach ( array( 'p', 'page_id', 'name', 'pagename', 'attachment_id', 'post__in', 'post_parent', 'post_parent__in', 'author', 'author__in', 'author_name' ) as $var ) {
if ( ! empty( $query->get( $var ) ) ) {
return $request;
@@ -640,8 +729,8 @@ class TMDO_Post_Query_Router {
}
global $wpdb;
$table = $wpdb->prefix . 'wpdo_post_' . sanitize_key( $groups[0] );
$alias = self::table_alias( $post_type, $groups[0] );
$table = $wpdb->prefix . 'wpdo_post_' . sanitize_key( $group );
$alias = self::table_alias( $post_type, $group );
$on = "(`{$wpdb->posts}`.`ID` = `{$alias}`.`post_id`)";
$join = " LEFT JOIN `{$table}` AS `{$alias}` ON {$on}";
if ( 1 !== substr_count( $request, $join ) ) {
@@ -657,6 +746,13 @@ class TMDO_Post_Query_Router {
if ( 1 !== $replaced ) {
return $request;
}
// WordPress groups by posts.ID for any tax or meta query. With the flat row
// (one per post) as the only join there is nothing to collapse, and the
// GROUP BY makes MySQL sort the whole match in a temporary table
// (WooCommerce's product_visibility NOT IN on searches is a subquery).
if ( 1 === preg_match_all( '/\bJOIN\b/i', (string) $flat_first ) ) {
$flat_first = preg_replace( '/\s+GROUP BY\s+`?' . preg_quote( $wpdb->posts, '/' ) . '`?\.`?ID`?(?=\s)/', ' ', (string) $flat_first, 1 );
}
// Tie-breaks on posts.ID (ours, and any a plugin appended) read the flat post_id.
$flat_first = preg_replace_callback(
'/\bORDER BY\b(?!.*\bORDER BY\b).*$/s',
+132
View File
@@ -424,3 +424,135 @@ TMDO_Live::test(
tmdo_live_assert_true( 0 === strpos( ltrim( $routed->request ), 'SELECT STRAIGHT_JOIN' ), 'flat-first: ' . $routed->request );
}
);
/**
* HivePress-style sort-only search (rating__order), fields ids, first page.
*
* @param string $order ASC | DESC.
* @return array<string, mixed> WP_Query args.
*/
function tmdo_live_router_rating_sort( string $order ): array {
return array(
'post_type' => 'hp_listing',
'post_status' => 'publish',
'posts_per_page' => 10,
'fields' => 'ids',
'suppress_filters' => false,
'meta_query' => array(
array(
'relation' => 'OR',
'rating__order' => array( 'key' => 'hp_rating', 'type' => 'DECIMAL', 'compare' => 'NOT EXISTS' ),
array( 'key' => 'hp_rating', 'type' => 'DECIMAL', 'compare' => 'EXISTS' ),
),
),
'orderby' => 'rating__order',
'order' => $order,
);
}
/**
* Native and routed runs of the same query.
*
* @param array<string, mixed> $args Query args.
* @return array{0:WP_Query, 1:WP_Query}
*/
function tmdo_live_router_native_and_routed( array $args ): array {
$native = new WP_Query( $args );
$router = new TMDO_Post_Query_Router();
$router->register_hooks();
try {
$routed = new WP_Query( $args );
} finally {
tmdo_live_router_unhook( $router );
}
return array( $native, $routed );
}
TMDO_Live::test(
'a sort-only search drives from the flat table when every post of the type has a flat row (2.2 PR-079)',
static function () {
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
TMDO_Post_Query_Router::forget_coverage();
foreach ( array( 'DESC', 'ASC' ) as $order ) {
list( $native, $routed ) = tmdo_live_router_native_and_routed( tmdo_live_router_rating_sort( $order ) );
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), "{$order} ids" );
tmdo_live_assert_same( $native->found_posts, $routed->found_posts, "{$order} found_posts" );
tmdo_live_assert_true( 0 === strpos( ltrim( $routed->request ), 'SELECT STRAIGHT_JOIN' ), "{$order} flat-first: " . $routed->request );
}
}
);
TMDO_Live::test(
'a sort-only search keeps the LEFT JOIN while a post of the type has no flat row (2.2 PR-079)',
static function () {
global $wpdb;
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
$bare = TMDO_Live_Fixtures::post( 'hp_listing' );
$wpdb->delete( TMDO_Schema_Manager::get_table_name( 'post', 'hp_listing_core' ), array( 'post_id' => $bare ) );
TMDO_Post_Query_Router::forget_coverage();
try {
list( $native, $routed ) = tmdo_live_router_native_and_routed( tmdo_live_router_rating_sort( 'DESC' ) );
} finally {
TMDO_Post_Query_Router::forget_coverage();
}
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
tmdo_live_assert_true( false === strpos( $routed->request, 'STRAIGHT_JOIN' ), 'not flat-first: ' . $routed->request );
}
);
TMDO_Live::test(
'a post created while the router is active gets its empty flat row (2.2 PR-079)',
static function () {
global $wpdb;
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
$router = new TMDO_Post_Query_Router();
$router->register_hooks();
try {
$id = TMDO_Live_Fixtures::post( 'hp_listing' );
} finally {
tmdo_live_router_unhook( $router );
}
$table = TMDO_Schema_Manager::get_table_name( 'post', 'hp_listing_core' );
tmdo_live_assert_same( '1', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE post_id = %d", $id ) ), 'flat row created' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
);
TMDO_Live::test(
'a flat-first search drops the GROUP BY WordPress adds for a subquery-only tax query (2.2 PR-079)',
static function () {
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
TMDO_Post_Query_Router::forget_coverage();
// Like WooCommerce's product_visibility exclusion on front-end searches: NOT IN
// is a subquery, yet WordPress groups by posts.ID for any tax query.
$args = tmdo_live_router_rating_sort( 'DESC' ) + array(
'tax_query' => array( array( 'taxonomy' => 'hp_listing_category', 'field' => 'term_id', 'terms' => array( 999999999 ), 'operator' => 'NOT IN' ) ),
);
list( $native, $routed ) = tmdo_live_router_native_and_routed( $args );
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
tmdo_live_assert_true( 0 === strpos( ltrim( $routed->request ), 'SELECT STRAIGHT_JOIN' ), 'flat-first: ' . $routed->request );
tmdo_live_assert_true( false === stripos( $routed->request, 'GROUP BY' ), 'no GROUP BY: ' . $routed->request );
}
);
TMDO_Live::test(
'a flat-first search keeps the GROUP BY when a tax query joins term relationships (2.2 PR-079)',
static function () {
TMDO_Live_Fixtures::field( 'post', 'hp_rating', 'hp_listing' );
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
TMDO_Post_Query_Router::forget_coverage();
$terms = get_terms( array( 'taxonomy' => 'hp_listing_category', 'number' => 2, 'hide_empty' => true, 'fields' => 'ids' ) );
if ( count( (array) $terms ) < 2 ) {
throw new TMDO_Live_Skip( 'needs two used hp_listing_category terms' );
}
$args = tmdo_live_router_rating_sort( 'DESC' ) + array(
'tax_query' => array( array( 'taxonomy' => 'hp_listing_category', 'field' => 'term_id', 'terms' => $terms, 'operator' => 'IN' ) ),
);
list( $native, $routed ) = tmdo_live_router_native_and_routed( $args );
tmdo_live_assert_same( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
tmdo_live_assert_true( false !== stripos( $routed->request, 'GROUP BY' ), 'GROUP BY kept: ' . $routed->request );
}
);