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
+104 -8
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,14 +699,25 @@ 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;
}
$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] ) {
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 ( 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 ) ) ) {
@@ -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',