API: Add STRAIGHT_JOIN to ApiQueryUserContribs to avoid planner oddness
authorBrad Jorsch <bjorsch@wikimedia.org>
Tue, 23 Apr 2019 18:02:34 +0000 (14:02 -0400)
committerBrad Jorsch <bjorsch@wikimedia.org>
Fri, 26 Apr 2019 20:15:24 +0000 (16:15 -0400)
For some unknown reason, when the `actor` table has few enough rows (or
few enough compared to `revision_actor_temp`) MariaDB 10.1.37 decides it
makes more sense to fetch everything from `actor` + `revision_actor_temp`
and filesort rather than fetching the limited number of rows from
`revision_actor_temp`.

We can work around it by telling it to not reorder the query, but then
(unlike in I9da981c0) we also have to reorder it ourselves to put
`revision_actor_temp` first instead of `revision`.

Bug: T221511
Change-Id: Ic63875b26a051a2da58374d5d76c95a6fa8ecc8e

includes/api/ApiQueryUserContribs.php

index 5b178b7..379f1af 100644 (file)
@@ -331,9 +331,6 @@ class ApiQueryUserContribs extends ApiQueryBase {
                $db = $this->getDB();
 
                $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo( [ 'page' ] );
-               $this->addTables( $revQuery['tables'] );
-               $this->addJoinConds( $revQuery['joins'] );
-               $this->addFields( $revQuery['fields'] );
 
                if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) {
                        $revWhere = ActorMigration::newMigration()->getWhere( $db, 'rev_user', $users );
@@ -341,6 +338,24 @@ class ApiQueryUserContribs extends ApiQueryBase {
                        $userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name';
                        $tsField = 'revactor_timestamp';
                        $idField = 'revactor_rev';
+
+                       // T221511: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor`
+                       // before `revision_actor_temp` and filesorting is somehow better than querying $limit+1 rows
+                       // from `revision_actor_temp`. Tell it not to reorder the query (and also reorder it ourselves
+                       // because as generated by RevisionStore it'll have `revision` first rather than
+                       // `revision_actor_temp`). But not when uctag is used, as it seems as likely to be harmed as
+                       // helped in that case, and not when there's only one User because in that case it fetches
+                       // the one `actor` row as a constant and doesn't filesort.
+                       if ( count( $users ) > 1 && !isset( $this->params['tag'] ) ) {
+                               $revQuery['joins']['revision'] = $revQuery['joins']['temp_rev_user'];
+                               unset( $revQuery['joins']['temp_rev_user'] );
+                               $this->addOption( 'STRAIGHT_JOIN' );
+                               // It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing
+                               // when join conditions are given for all joins, but GergÅ‘ is wary of relying on that so pull
+                               // `revision_actor_temp` to the start.
+                               $revQuery['tables'] =
+                                       [ 'temp_rev_user' => $revQuery['tables']['temp_rev_user'] ] + $revQuery['tables'];
+                       }
                } else {
                        // If we're dealing with user names (rather than IDs) in read-old mode,
                        // pass false for ActorMigration::getWhere()'s $useId parameter so
@@ -353,6 +368,9 @@ class ApiQueryUserContribs extends ApiQueryBase {
                        $idField = 'rev_id';
                }
 
+               $this->addTables( $revQuery['tables'] );
+               $this->addJoinConds( $revQuery['joins'] );
+               $this->addFields( $revQuery['fields'] );
                $this->addWhere( $revWhere['conds'] );
 
                // Handle continue parameter