perf(2meet-data-optimizer): drive flat-sorted searches from the flat table
- A filtered search ordered by a flat column is rewritten to SELECT STRAIGHT_JOIN … FROM flat INNER JOIN wp_posts, with posts.ID tie-breaks (ours and plugin-appended) read as the equal flat post_id, so MariaDB sorts one table instead of looking every post up and sorting in a temporary table - Only when the routed conditions already require a flat row on the sort group, without the copy route or a selective post filter; filter tmdo_router_flat_first turns it off; split_the_query compares with the same rewrite - Verify: tests/live/run.php router (13/13); integration 522; dev36 page SQL Q07 263→147 ms, Q15 250→142 ms; fe.sh hourly_rate__desc 850→367 ms, ids == native - PR: PR-062c
This commit is contained in:
@@ -337,10 +337,14 @@ class TMDO_Post_Query_Router {
|
||||
public function posts_request( ?string $request, \WP_Query $query ): string {
|
||||
$request = (string) ( $request ?? '' );
|
||||
$routed = ! empty( $query->get( self::QUERY_VAR ) ) || ! empty( $query->get( self::SORT_VAR ) );
|
||||
if ( ! $routed || false === strpos( $request, 'SQL_CALC_FOUND_ROWS' ) ) {
|
||||
if ( ! $routed ) {
|
||||
return $request;
|
||||
}
|
||||
$counted = false !== strpos( $request, 'SQL_CALC_FOUND_ROWS' );
|
||||
$request = self::routed_request( $request, $query );
|
||||
if ( ! $counted ) {
|
||||
return $request;
|
||||
}
|
||||
$request = self::without_found_rows( $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" );
|
||||
return $request;
|
||||
@@ -367,7 +371,7 @@ class TMDO_Post_Query_Router {
|
||||
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
|
||||
&& self::routed_request( (string) $old_request, $query ) === $query->request
|
||||
&& ( ( '' !== ( $clauses['limits'] ?? '' ) && (int) $query->get( 'posts_per_page' ) < 500 ) || wp_using_ext_object_cache() );
|
||||
}
|
||||
|
||||
@@ -569,6 +573,81 @@ class TMDO_Post_Query_Router {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* What posts_request makes of a routed request.
|
||||
*
|
||||
* @param string $request SELECT statement.
|
||||
* @param \WP_Query $query Current WP_Query.
|
||||
* @return string
|
||||
*/
|
||||
private static function routed_request( string $request, \WP_Query $query ): string {
|
||||
return self::flat_first( self::without_found_rows( $request ), $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Drive a filtered search ordered by a flat column from its flat table.
|
||||
*
|
||||
* MariaDB starts from wp_posts (the type / status index), looks every
|
||||
* candidate up in the flat table and sorts in a temporary table. From the
|
||||
* flat table, with the posts.ID tie-break read as the equal flat post_id,
|
||||
* it sorts one table (dev36, 100k listings: meta_value_num 263 → 147 ms,
|
||||
* HivePress sort + filter 300 → 36 ms). Only when the routed conditions
|
||||
* already require a flat row (LEFT JOIN = INNER JOIN), all on the sort
|
||||
* group, without a selective post filter; `tmdo_router_flat_first` can
|
||||
* turn it off.
|
||||
*
|
||||
* @param string $request SELECT statement.
|
||||
* @param \WP_Query $query Current WP_Query.
|
||||
* @return string
|
||||
*/
|
||||
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 ) ) ) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
if ( ! apply_filters( 'tmdo_router_flat_first', true, $query ) ) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'wpdo_post_' . sanitize_key( $groups[0] );
|
||||
$alias = self::table_alias( $post_type, $groups[0] );
|
||||
$on = "(`{$wpdb->posts}`.`ID` = `{$alias}`.`post_id`)";
|
||||
$join = " LEFT JOIN `{$table}` AS `{$alias}` ON {$on}";
|
||||
if ( 1 !== substr_count( $request, $join ) ) {
|
||||
return $request;
|
||||
}
|
||||
$flat_first = preg_replace(
|
||||
'/\bFROM\s+' . preg_quote( $wpdb->posts, '/' ) . '\b/',
|
||||
"FROM `{$table}` AS `{$alias}` INNER JOIN {$wpdb->posts} ON {$on}",
|
||||
str_replace( $join, '', $request ),
|
||||
1,
|
||||
$replaced
|
||||
);
|
||||
if ( 1 !== $replaced ) {
|
||||
return $request;
|
||||
}
|
||||
// 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',
|
||||
static fn( $m ) => preg_replace( '/`?' . preg_quote( $wpdb->posts, '/' ) . '`?\.`?ID`?(?!\w)/', "`{$alias}`.`post_id`", $m[0] ),
|
||||
(string) $flat_first
|
||||
);
|
||||
return (string) preg_replace( '/^\s*SELECT\s/', 'SELECT STRAIGHT_JOIN ', (string) $flat_first, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* A request without its SQL_CALC_FOUND_ROWS.
|
||||
*
|
||||
|
||||
@@ -75,12 +75,12 @@ class PostQueryRouterFoundRowsTest extends TestCase {
|
||||
}
|
||||
|
||||
/** A routed query plus the SQL WordPress would build for it (page size 1). */
|
||||
private function routed(): array {
|
||||
private function routed( array $clause = array( 'key' => 'fr_price', 'value' => 15, 'compare' => '>=', 'type' => 'NUMERIC' ) ): array {
|
||||
global $wpdb;
|
||||
$query = new WP_Query();
|
||||
$query->set( 'post_type', 'fr_item' );
|
||||
$query->set( 'post_status', 'publish' );
|
||||
$query->set( 'meta_query', array( array( 'key' => 'fr_price', 'value' => 15, 'compare' => '>=', 'type' => 'NUMERIC' ) ) );
|
||||
$query->set( 'meta_query', array( $clause ) );
|
||||
$router = new TMDO_Post_Query_Router();
|
||||
$router->pre_get_posts( $query );
|
||||
|
||||
@@ -134,6 +134,86 @@ class PostQueryRouterFoundRowsTest extends TestCase {
|
||||
$this->assertNotSame( $after_write, wp_cache_get_last_changed( 'posts' ) );
|
||||
}
|
||||
|
||||
// ── flat-first: a filtered search ordered by a flat column ─────────────
|
||||
|
||||
/**
|
||||
* A routed search ordered by the flat price (named clause), with the
|
||||
* posts.ID tie-break a plugin such as HivePress appends.
|
||||
*
|
||||
* @return array{0:TMDO_Post_Query_Router, 1:WP_Query, 2:string}
|
||||
*/
|
||||
private function sorted( string $compare = '>=' ): array {
|
||||
global $wpdb;
|
||||
$query = new WP_Query();
|
||||
$query->set( 'post_type', 'fr_item' );
|
||||
$query->set( 'post_status', 'publish' );
|
||||
$query->set( 'meta_query', array( 'price' => array( 'key' => 'fr_price', 'value' => 15, 'compare' => $compare, 'type' => 'NUMERIC' ) ) );
|
||||
$query->set( 'orderby', 'price' );
|
||||
$query->set( 'order', 'DESC' );
|
||||
$router = new TMDO_Post_Query_Router();
|
||||
$router->pre_get_posts( $query );
|
||||
|
||||
$request = "SELECT SQL_CALC_FOUND_ROWS {$wpdb->posts}.ID FROM {$wpdb->posts} " . $router->posts_join( '', $query )
|
||||
. " WHERE 1=1 AND {$wpdb->posts}.post_type = 'fr_item' AND (({$wpdb->posts}.post_status = 'publish'))" . $router->posts_where( '', $query )
|
||||
. ' ORDER BY ' . $router->posts_orderby( '', $query ) . ", {$wpdb->posts}.ID DESC LIMIT 0, 10";
|
||||
return array( $router, $query, $request );
|
||||
}
|
||||
|
||||
public function test_sorted_filtered_request_drives_from_the_flat_table_with_the_same_rows(): void {
|
||||
global $wpdb;
|
||||
list( $router, $query, $request ) = $this->sorted();
|
||||
|
||||
$sql = $router->posts_request( $request, $query );
|
||||
|
||||
$this->assertStringStartsWith( 'SELECT STRAIGHT_JOIN ', $sql );
|
||||
$this->assertStringContainsString( 'FROM `' . self::TABLE . '` AS `wpdoflat_fr_item_fr_core` INNER JOIN ' . $wpdb->posts . ' ON', $sql );
|
||||
$this->assertStringNotContainsString( 'LEFT JOIN `' . self::TABLE . '`', $sql );
|
||||
$this->assertStringEndsWith( 'ORDER BY `wpdoflat_fr_item_fr_core`.`fr_price` DESC, `wpdoflat_fr_item_fr_core`.`post_id` DESC, `wpdoflat_fr_item_fr_core`.`post_id` DESC LIMIT 0, 10', $sql, 'posts.ID tie-breaks read the flat post_id' );
|
||||
$this->assertSame( array( '3', '2' ), $wpdb->get_col( $sql ) );
|
||||
$this->assertSame( $wpdb->get_col( str_replace( 'SQL_CALC_FOUND_ROWS ', '', $request ) ), $wpdb->get_col( $sql ) );
|
||||
}
|
||||
|
||||
public function test_date_ordered_filtered_request_keeps_wordpress_join_order(): void {
|
||||
list( $router, $query, $request ) = $this->routed();
|
||||
|
||||
$this->assertStringStartsNotWith( 'SELECT STRAIGHT_JOIN', $router->posts_request( $request, $query ) );
|
||||
}
|
||||
|
||||
public function test_flat_first_needs_a_condition_that_rejects_posts_without_a_flat_row(): void {
|
||||
list( $router, $query, $request ) = $this->sorted( 'NOT EXISTS' );
|
||||
|
||||
$this->assertStringStartsNotWith( 'SELECT STRAIGHT_JOIN', $router->posts_request( $request, $query ) );
|
||||
}
|
||||
|
||||
public function test_flat_first_is_skipped_for_selective_post_filters(): void {
|
||||
list( $router, $query, $request ) = $this->sorted();
|
||||
$query->set( 'post__in', array( 2 ) );
|
||||
|
||||
$this->assertStringStartsNotWith( 'SELECT STRAIGHT_JOIN', $router->posts_request( $request, $query ) );
|
||||
}
|
||||
|
||||
public function test_flat_first_can_be_turned_off(): void {
|
||||
list( $router, $query, $request ) = $this->sorted();
|
||||
$off = static fn() => false;
|
||||
add_filter( 'tmdo_router_flat_first', $off );
|
||||
try {
|
||||
$this->assertStringStartsNotWith( 'SELECT STRAIGHT_JOIN', $router->posts_request( $request, $query ) );
|
||||
} finally {
|
||||
remove_filter( 'tmdo_router_flat_first', $off );
|
||||
}
|
||||
}
|
||||
|
||||
public function test_flat_first_keeps_the_split_query(): void {
|
||||
global $wpdb;
|
||||
list( $router, $query, $request ) = $this->sorted();
|
||||
$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', 10 );
|
||||
|
||||
$this->assertStringStartsWith( 'SELECT STRAIGHT_JOIN ', $query->request );
|
||||
$this->assertTrue( $router->split_the_query( false, $query, $old_request, array( 'fields' => "{$wpdb->posts}.*", 'limits' => 'LIMIT 0, 10' ) ) );
|
||||
}
|
||||
|
||||
// ── split query: IDs first, then cached post objects ──────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -381,3 +381,45 @@ TMDO_Live::test(
|
||||
tmdo_live_assert_true( 1 === preg_match( '/^\s*SELECT\s+' . preg_quote( $wpdb->posts, '/' ) . '\.ID\s/', $routed->request ), 'IDs first: ' . $routed->request );
|
||||
}
|
||||
);
|
||||
|
||||
TMDO_Live::test(
|
||||
'a filtered search ordered by a flat column drives from the flat table and matches native',
|
||||
static function () {
|
||||
foreach ( array( 'hp_price', 'hp_hourly_rate' ) as $key ) {
|
||||
TMDO_Live_Fixtures::field( 'post', $key, 'hp_listing' );
|
||||
}
|
||||
TMDO_Live_Fixtures::use_modes( array( 'post' => 'shadow_read' ) );
|
||||
// A price band no seeded listing uses keeps the result to the fixtures.
|
||||
tmdo_live_router_posts(
|
||||
'hp_listing',
|
||||
array(
|
||||
'A' => array( 'hp_price' => '9876501', 'hp_hourly_rate' => '12' ),
|
||||
'B' => array( 'hp_price' => '9876502', 'hp_hourly_rate' => '15' ),
|
||||
'C' => array( 'hp_price' => '9876503' ),
|
||||
)
|
||||
);
|
||||
$args = array(
|
||||
'post_type' => 'hp_listing',
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 10,
|
||||
'fields' => 'ids',
|
||||
'suppress_filters' => false,
|
||||
'meta_query' => array( array( 'key' => 'hp_price', 'value' => array( 9876500, 9876510 ), 'compare' => 'BETWEEN', 'type' => 'NUMERIC' ) ),
|
||||
'meta_key' => 'hp_price',
|
||||
'orderby' => 'meta_value_num',
|
||||
'order' => 'DESC',
|
||||
);
|
||||
$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( array_map( 'intval', $native->posts ), array_map( 'intval', $routed->posts ), 'ids' );
|
||||
tmdo_live_assert_same( $native->found_posts, $routed->found_posts, 'found_posts' );
|
||||
tmdo_live_assert_true( 0 === strpos( ltrim( $routed->request ), 'SELECT STRAIGHT_JOIN' ), 'flat-first: ' . $routed->request );
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user