perf(2meet-data-optimizer): cache found-rows counts for routed queries

- TMDO_Post_Query_Router drops SQL_CALC_FOUND_ROWS from routed searches
  (posts_request) and answers found_posts_query from a COUNT(*) of the
  same query, cached in the object cache under posts' last_changed.
- Hook Bus flat post writes and deletes move posts' last_changed: in
  aeav_only they skip update_metadata()'s own hooks, so query caches
  would otherwise outlive the write.
- Bench (1M): cached page 8 ms (was 694 / 363 ms for scenarios 1 / 2);
  after invalidation 8 ms + COUNT 608 / 344 ms, on par with before.
- REST X-WP-Total COUNT is on the frozen Hot zone path (ADR-003): not
  changed.
- Verify: core unit 659 / integration 475, HP 142/49, WC 10/22 green,
  phpcs clean
- PR: PR-030
This commit is contained in:
2026-09-26 01:44:45 +08:00
parent 754f904e3b
commit 10671e7b78
6 changed files with 211 additions and 0 deletions
@@ -27,6 +27,11 @@
* post_date and skips GROUP BY (one row per post), so MySQL can walk
* `idx_post_status_date` and stop at the LIMIT.
*
* Found rows (O-4): routed searches drop SQL_CALC_FOUND_ROWS (it counts every
* match inside the page query); found_posts_query answers from a COUNT of the
* same query, kept in the object cache until posts' last_changed moves (core
* post saves, and flat writes via TMDO_Hook_Bus).
*
* 🔒 v2.9.x → v2.10.x frozen contract: never touches user entity. Routes
* only entity_type='post'.
*
@@ -53,6 +58,12 @@ class TMDO_Post_Query_Router {
/** Query var holding the status / date copy route, when one applies. */
private const COPY_VAR = 'wpdo_post_copy_route';
/** Query var holding the COUNT for found_posts_query. */
private const COUNT_VAR = 'wpdo_post_count_sql';
/** Object cache group for found-rows counts. */
private const COUNT_CACHE_GROUP = 'tmdo_found_rows';
/**
* Per-request answer to "are this group's copies usable?".
*
@@ -71,6 +82,8 @@ class TMDO_Post_Query_Router {
add_filter( 'posts_where', array( $this, 'posts_where' ), 10, 2 );
add_filter( 'posts_groupby', array( $this, 'posts_groupby' ), 10, 2 );
add_filter( 'posts_orderby', array( $this, 'posts_orderby' ), 10, 2 );
add_filter( 'posts_request', array( $this, 'posts_request' ), 10, 2 );
add_filter( 'found_posts_query', array( $this, 'found_posts_query' ), 10, 2 );
}
/**
@@ -272,6 +285,46 @@ class TMDO_Post_Query_Router {
return $groupby;
}
/**
* Drop SQL_CALC_FOUND_ROWS from a routed search; keep its COUNT for later.
*
* @param string|null $request Full SELECT.
* @param \WP_Query $query Current WP_Query.
* @return string
*/
public function posts_request( ?string $request, \WP_Query $query ): string {
$request = (string) ( $request ?? '' );
if ( empty( $query->get( self::QUERY_VAR ) ) || false === strpos( $request, 'SQL_CALC_FOUND_ROWS' ) ) {
return $request;
}
$request = preg_replace( '/\bSQL_CALC_FOUND_ROWS\s+/', '', $request, 1 );
$matches = preg_replace( '/\s+LIMIT\s+\d+(\s*,\s*\d+)?\s*$/i', '', $request );
$query->set( self::COUNT_VAR, "SELECT COUNT(*) FROM ( {$matches} ) AS tmdo_found" );
return $request;
}
/**
* Answer found rows from the cached COUNT of a routed search.
*
* @param string|null $sql Default 'SELECT FOUND_ROWS()'.
* @param \WP_Query $query Current WP_Query.
* @return string
*/
public function found_posts_query( ?string $sql, \WP_Query $query ): string {
$count_sql = $query->get( self::COUNT_VAR );
if ( empty( $count_sql ) ) {
return (string) ( $sql ?? '' );
}
$key = md5( $count_sql ) . ':' . wp_cache_get_last_changed( 'posts' );
$total = wp_cache_get( $key, self::COUNT_CACHE_GROUP );
if ( false === $total ) {
global $wpdb;
$total = (int) $wpdb->get_var( $count_sql );
wp_cache_set( $key, $total, self::COUNT_CACHE_GROUP, HOUR_IN_SECONDS );
}
return 'SELECT ' . (int) $total;
}
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────