perf(2meet-data-optimizer): keep WordPress's split query on routed searches
- posts_request only drops SQL_CALC_FOUND_ROWS, but WordPress then treated the request as filtered and selected full post rows through a temporary table; split_the_query now keeps the ID-first split for routed searches when nothing else changed the request, and posts_request_ids drops SQL_CALC_FOUND_ROWS from the split ID query - Verify: tests/live/run.php router (12/12, IDs-first request, found_posts == native); dev36 fe.sh price 589→340 ms, price__asc 961→427 ms, rating__desc 1724→872 ms, hourly_rate__desc 855→884 ms (was 1206) - PR: PR-062b
This commit is contained in:
@@ -92,6 +92,8 @@ class TMDO_Post_Query_Router {
|
|||||||
add_filter( 'posts_where', array( $this, 'posts_where' ), 10, 2 );
|
add_filter( 'posts_where', array( $this, 'posts_where' ), 10, 2 );
|
||||||
add_filter( 'posts_orderby', array( $this, 'posts_orderby' ), 10, 2 );
|
add_filter( 'posts_orderby', array( $this, 'posts_orderby' ), 10, 2 );
|
||||||
add_filter( 'posts_request', array( $this, 'posts_request' ), 10, 2 );
|
add_filter( 'posts_request', array( $this, 'posts_request' ), 10, 2 );
|
||||||
|
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_filter( 'found_posts_query', array( $this, 'found_posts_query' ), 10, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,12 +340,37 @@ class TMDO_Post_Query_Router {
|
|||||||
if ( ! $routed || false === strpos( $request, 'SQL_CALC_FOUND_ROWS' ) ) {
|
if ( ! $routed || false === strpos( $request, 'SQL_CALC_FOUND_ROWS' ) ) {
|
||||||
return $request;
|
return $request;
|
||||||
}
|
}
|
||||||
$request = preg_replace( '/\bSQL_CALC_FOUND_ROWS\s+/', '', $request, 1 );
|
$request = self::without_found_rows( $request );
|
||||||
$matches = preg_replace( '/\s+LIMIT\s+\d+(\s*,\s*\d+)?\s*$/i', '', $request );
|
$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" );
|
$query->set( self::COUNT_VAR, "SELECT COUNT(*) FROM ( {$matches} ) AS tmdo_found" );
|
||||||
return $request;
|
return $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep WordPress's split query (IDs first, then cached post objects) on a
|
||||||
|
* routed search: posts_request only dropped SQL_CALC_FOUND_ROWS, yet
|
||||||
|
* WordPress takes any change for a filtered request and selects full rows
|
||||||
|
* (a temporary table sorting whole posts). The split ID request passes
|
||||||
|
* through posts_request_ids, which drops SQL_CALC_FOUND_ROWS again.
|
||||||
|
*
|
||||||
|
* @param bool $split Core decision.
|
||||||
|
* @param \WP_Query $query Current WP_Query.
|
||||||
|
* @param string $old_request Request before posts_request.
|
||||||
|
* @param array<string, string> $clauses Query clauses (fields, limits …).
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function split_the_query( $split, \WP_Query $query, $old_request = '', $clauses = array() ): bool {
|
||||||
|
$routed = ! empty( $query->get( self::QUERY_VAR ) ) || ! empty( $query->get( self::SORT_VAR ) );
|
||||||
|
if ( $split || ! $routed ) {
|
||||||
|
return (bool) $split;
|
||||||
|
}
|
||||||
|
global $wpdb;
|
||||||
|
// Core's other conditions: full rows, and a LIMIT under 500 or an external object cache.
|
||||||
|
return "{$wpdb->posts}.*" === ( $clauses['fields'] ?? '' )
|
||||||
|
&& self::without_found_rows( (string) $old_request ) === $query->request
|
||||||
|
&& ( ( '' !== ( $clauses['limits'] ?? '' ) && (int) $query->get( 'posts_per_page' ) < 500 ) || wp_using_ext_object_cache() );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Answer found rows from the cached COUNT of a routed search.
|
* Answer found rows from the cached COUNT of a routed search.
|
||||||
*
|
*
|
||||||
@@ -542,6 +569,16 @@ class TMDO_Post_Query_Router {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request without its SQL_CALC_FOUND_ROWS.
|
||||||
|
*
|
||||||
|
* @param string $request SELECT statement.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function without_found_rows( string $request ): string {
|
||||||
|
return (string) preg_replace( '/\bSQL_CALC_FOUND_ROWS\s+/', '', $request, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a meta query type casts to a number (NUMERIC, SIGNED, DECIMAL(p,s) …).
|
* Whether a meta query type casts to a number (NUMERIC, SIGNED, DECIMAL(p,s) …).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -133,4 +133,49 @@ class PostQueryRouterFoundRowsTest extends TestCase {
|
|||||||
$this->assertNotSame( $before, $after_write );
|
$this->assertNotSame( $before, $after_write );
|
||||||
$this->assertNotSame( $after_write, wp_cache_get_last_changed( 'posts' ) );
|
$this->assertNotSame( $after_write, wp_cache_get_last_changed( 'posts' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── split query: IDs first, then cached post objects ──────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WordPress's full-row request for a routed query, before and after posts_request.
|
||||||
|
*
|
||||||
|
* @return array{0:TMDO_Post_Query_Router, 1:WP_Query, 2:string, 3:array<string, string>}
|
||||||
|
*/
|
||||||
|
private function full_rows(): array {
|
||||||
|
global $wpdb;
|
||||||
|
list( $router, $query, $request ) = $this->routed();
|
||||||
|
$old_request = str_replace( "{$wpdb->posts}.ID FROM", "{$wpdb->posts}.* FROM", $request );
|
||||||
|
$query->request = $router->posts_request( $old_request, $query );
|
||||||
|
$query->set( 'posts_per_page', 1 );
|
||||||
|
return array( $router, $query, $old_request, array( 'fields' => "{$wpdb->posts}.*", 'limits' => 'LIMIT 0, 1' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_routed_full_row_request_is_still_split(): void {
|
||||||
|
list( $router, $query, $old_request, $clauses ) = $this->full_rows();
|
||||||
|
|
||||||
|
$this->assertTrue( $router->split_the_query( false, $query, $old_request, $clauses ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_request_changed_by_others_is_not_split(): void {
|
||||||
|
list( $router, $query, $old_request, $clauses ) = $this->full_rows();
|
||||||
|
$query->request .= ' /* another plugin */';
|
||||||
|
|
||||||
|
$this->assertFalse( $router->split_the_query( false, $query, $old_request, $clauses ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unrouted_query_keeps_the_core_decision(): void {
|
||||||
|
$router = new TMDO_Post_Query_Router();
|
||||||
|
|
||||||
|
$this->assertFalse( $router->split_the_query( false, new WP_Query(), 'SELECT 1', array( 'fields' => 'wp_itest_posts.*', 'limits' => 'LIMIT 0, 1' ) ) );
|
||||||
|
$this->assertTrue( $router->split_the_query( true, new WP_Query(), 'SELECT 1', array() ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_split_id_request_goes_through_posts_request_too(): void {
|
||||||
|
unset( $GLOBALS['_wp_filter_callbacks']['posts_request_ids'], $GLOBALS['_wp_filter_callbacks']['split_the_query'] );
|
||||||
|
$router = new TMDO_Post_Query_Router();
|
||||||
|
$router->register_hooks();
|
||||||
|
|
||||||
|
$this->assertContains( array( $router, 'posts_request' ), $GLOBALS['_wp_filter_callbacks']['posts_request_ids'] ?? array() );
|
||||||
|
$this->assertContains( array( $router, 'split_the_query' ), $GLOBALS['_wp_filter_callbacks']['split_the_query'] ?? array() );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ if ( ! function_exists( 'wp_cache_delete' ) ) { function wp_cache_delete( $key,
|
|||||||
|
|
||||||
if ( ! class_exists( 'WP_Query' ) ) {
|
if ( ! class_exists( 'WP_Query' ) ) {
|
||||||
class WP_Query {
|
class WP_Query {
|
||||||
|
public string $request = '';
|
||||||
private array $vars = [];
|
private array $vars = [];
|
||||||
public function get( string $key, $default = '' ) { return $this->vars[ $key ] ?? $default; }
|
public function get( string $key, $default = '' ) { return $this->vars[ $key ] ?? $default; }
|
||||||
public function set( string $key, $value ): void { $this->vars[ $key ] = $value; }
|
public function set( string $key, $value ): void { $this->vars[ $key ] = $value; }
|
||||||
|
|||||||
@@ -48,12 +48,7 @@ function tmdo_live_router_query( array $args, array $ids, bool $routed ): array
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
foreach ( array( 'pre_get_posts', 'posts_join', 'posts_where', 'posts_orderby', 'posts_request', 'found_posts_query' ) as $hook ) {
|
tmdo_live_router_unhook( $router );
|
||||||
$priority = has_filter( $hook, array( $router, $hook ) );
|
|
||||||
if ( false !== $priority ) {
|
|
||||||
remove_filter( $hook, array( $router, $hook ), $priority );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tmdo_live_assert_same( '', $wpdb->last_error, 'SQL error' );
|
tmdo_live_assert_same( '', $wpdb->last_error, 'SQL error' );
|
||||||
return array(
|
return array(
|
||||||
@@ -63,6 +58,22 @@ function tmdo_live_router_query( array $args, array $ids, bool $routed ): array
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove every hook TMDO_Post_Query_Router::register_hooks() added.
|
||||||
|
*
|
||||||
|
* @param TMDO_Post_Query_Router $router Router instance.
|
||||||
|
*/
|
||||||
|
function tmdo_live_router_unhook( TMDO_Post_Query_Router $router ): void {
|
||||||
|
foreach ( array( 'pre_get_posts', 'posts_join', 'posts_where', 'posts_orderby', 'posts_request', 'split_the_query', 'found_posts_query' ) as $method ) {
|
||||||
|
foreach ( 'posts_request' === $method ? array( 'posts_request', 'posts_request_ids' ) : array( $method ) as $hook ) {
|
||||||
|
$priority = has_filter( $hook, array( $router, $method ) );
|
||||||
|
if ( false !== $priority ) {
|
||||||
|
remove_filter( $hook, array( $router, $method ), $priority );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create posts of $post_type with the given meta, in post mode shadow_read
|
* Create posts of $post_type with the given meta, in post mode shadow_read
|
||||||
* (so the Hook Bus keeps the flat rows in step).
|
* (so the Hook Bus keeps the flat rows in step).
|
||||||
@@ -340,3 +351,33 @@ TMDO_Live::test(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TMDO_Live::test(
|
||||||
|
'a routed full-row search keeps the split query (IDs first) and native found_posts',
|
||||||
|
static function () {
|
||||||
|
global $wpdb;
|
||||||
|
$l = tmdo_live_router_listings();
|
||||||
|
$args = array(
|
||||||
|
'post_type' => 'hp_listing',
|
||||||
|
'post__in' => array_values( $l ),
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => 2,
|
||||||
|
'orderby' => 'ID',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'meta_query' => array( array( 'key' => 'hp_price', 'value' => 1000, 'compare' => '>=', 'type' => 'NUMERIC' ) ),
|
||||||
|
);
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
tmdo_live_assert_same( wp_list_pluck( $native->posts, 'ID' ), wp_list_pluck( $routed->posts, 'ID' ), 'page of posts' );
|
||||||
|
tmdo_live_assert_same( $native->found_posts, $routed->found_posts, 'found_posts' );
|
||||||
|
tmdo_live_assert_true( false !== strpos( $routed->request, 'wpdoflat_' ), 'routed: ' . $routed->request );
|
||||||
|
tmdo_live_assert_true( 1 === preg_match( '/^\s*SELECT\s+' . preg_quote( $wpdb->posts, '/' ) . '\.ID\s/', $routed->request ), 'IDs first: ' . $routed->request );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user