perf(2meet-data-optimizer): let flat-first sorts use a (column, id) index

- a group index may end with the entity ID column (post_id, ...)
- flat-first rewrites the posts.ID tie-breaks once, in the sort's direction:
  with a (column, post_id) index MySQL returns rows in order instead of
  sorting the whole match. dev36 (1.3M listings), price range sorted by price:
  850 ms filesort -> 2 ms. Ties are ordered by ID the same way as the column
  (natively ID DESC); pagination stays stable, result sets are unchanged
- Verify: EntityRegistryIndexesTest, PostQueryRouterFoundRowsTest (ASC
  tie-break, single tie-break), unit 704, integration 589, phpcs, stan;
  dev36 with the HivePress indexes (PR-119)
- PR: PR-118
This commit is contained in:
2026-09-27 22:40:59 +08:00
parent b2725b219d
commit eac0955b76
4 changed files with 44 additions and 7 deletions
@@ -226,14 +226,14 @@ class PostQueryRouterFoundRowsTest extends TestCase {
*
* @return array{0:TMDO_Post_Query_Router, 1:WP_Query, 2:string}
*/
private function sorted( string $compare = '>=' ): array {
private function sorted( string $compare = '>=', string $order = 'DESC' ): 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' );
$query->set( 'order', $order );
$router = new TMDO_Post_Query_Router();
$router->pre_get_posts( $query );
@@ -252,11 +252,26 @@ class PostQueryRouterFoundRowsTest extends TestCase {
$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->assertStringEndsWith( 'ORDER BY `wpdoflat_fr_item_fr_core`.`fr_price` DESC, `wpdoflat_fr_item_fr_core`.`post_id` DESC LIMIT 0, 10', $sql, 'posts.ID tie-breaks read the flat post_id, once' );
$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 ) );
}
/**
* The ID tie-break follows the sort direction, so a (column, post_id)
* index returns rows in order with no filesort (2.4 PR-118); ties are
* ordered by ID the same way as the column.
*/
public function test_flat_first_tie_break_follows_the_sort_direction(): void {
global $wpdb;
list( $router, $query, $request ) = $this->sorted( '>=', 'ASC' );
$sql = $router->posts_request( $request, $query );
$this->assertStringEndsWith( 'ORDER BY `wpdoflat_fr_item_fr_core`.`fr_price` ASC, `wpdoflat_fr_item_fr_core`.`post_id` ASC LIMIT 0, 10', $sql );
$this->assertSame( array( '2', '3' ), $wpdb->get_col( $sql ) );
}
public function test_date_ordered_filtered_request_keeps_wordpress_join_order(): void {
list( $router, $query, $request ) = $this->routed();
+13 -1
View File
@@ -14,7 +14,9 @@ class EntityRegistryIndexesTest extends TestCase {
protected function setUp(): void {
TMDO_Entity_Registry::init();
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
$adapter = $this->createMock( TMDO_Entity_Adapter_Interface::class );
$adapter->method( 'get_entity_id_column' )->willReturn( 'post_id' );
TMDO_Entity_Registry::register_adapter( 'post', $adapter );
}
private function fields(): array {
@@ -37,6 +39,16 @@ class EntityRegistryIndexesTest extends TestCase {
$this->assertSame( array( 'ok' => array( 'x_price' ) ), TMDO_Entity_Registry::get_group_indexes( 'post', 'x_core' ) );
}
/**
* A sort index ends with the entity ID, so a flat-first search ordered by a
* column with the ID tie-break reads the index in order (2.4 PR-118).
*/
public function test_an_index_may_end_with_the_entity_id_column(): void {
TMDO_Entity_Registry::register_group( 'post', 'x_core', $this->fields(), array( 'indexes' => array( 'price_id' => array( 'x_price', 'post_id' ), 'id_first' => array( 'post_id', 'x_price' ) ) ) );
$this->assertSame( array( 'price_id' => array( 'x_price', 'post_id' ) ), TMDO_Entity_Registry::get_group_indexes( 'post', 'x_core' ), 'the ID only as the last column' );
}
public function test_indexes_leave_the_field_definition_and_hash_alone(): void {
TMDO_Entity_Registry::register_group( 'post', 'plain', $this->fields() );
TMDO_Entity_Registry::register_group( 'post', 'indexed', $this->fields(), array( 'indexes' => array( 'flag_price' => array( 'x_flag', 'x_price' ) ) ) );