From 57247ed6ca1a61d19f26912be3ab1e19528996f2 Mon Sep 17 00:00:00 2001 From: d-ph Date: Mon, 22 Jul 2024 10:06:18 +0100 Subject: [PATCH] Make CountWalker use COUNT(*) when $distinct is explicitly set to false (#11552) This change makes CountWalker use COUNT(*) instead of COUNT(tbl.id), when the user declared that their query does not need to use (SELECT) DISTINCT, which is commonly the case when there are no JOINs in the query, or when the JOINs are only *ToOne. Research showed that COUNT(*) allows databases to use index(-only) scans more eagerly from any of the indexed columns, especially when the query is using a WHERE-condition that filters on an indexed column. --- src/Tools/Pagination/CountWalker.php | 36 ++++++++++--------- .../ORM/Tools/Pagination/CountWalkerTest.php | 14 ++++++++ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/Tools/Pagination/CountWalker.php b/src/Tools/Pagination/CountWalker.php index d2129435558..f11b25d6821 100644 --- a/src/Tools/Pagination/CountWalker.php +++ b/src/Tools/Pagination/CountWalker.php @@ -37,27 +37,31 @@ public function walkSelectStatement(SelectStatement $selectStatement): void throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction'); } - $fromRoot = reset($from); - $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; - $rootClass = $this->getMetadataForDqlAlias($rootAlias); - $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT); - $pathType = PathExpression::TYPE_STATE_FIELD; - if (isset($rootClass->associationMappings[$identifierFieldName])) { - $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; - } + $countPathExpressionOrLiteral = '*'; + if ($distinct) { + $fromRoot = reset($from); + $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable; + $rootClass = $this->getMetadataForDqlAlias($rootAlias); + $identifierFieldName = $rootClass->getSingleIdentifierFieldName(); + + $pathType = PathExpression::TYPE_STATE_FIELD; + if (isset($rootClass->associationMappings[$identifierFieldName])) { + $pathType = PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + } - $pathExpression = new PathExpression( - PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, - $rootAlias, - $identifierFieldName, - ); - $pathExpression->type = $pathType; + $countPathExpressionOrLiteral = new PathExpression( + PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, + $rootAlias, + $identifierFieldName, + ); + $countPathExpressionOrLiteral->type = $pathType; + } - $distinct = $this->_getQuery()->getHint(self::HINT_DISTINCT); $selectStatement->selectClause->selectExpressions = [ new SelectExpression( - new AggregateExpression('count', $pathExpression, $distinct), + new AggregateExpression('count', $countPathExpressionOrLiteral, $distinct), null, ), ]; diff --git a/tests/Tests/ORM/Tools/Pagination/CountWalkerTest.php b/tests/Tests/ORM/Tools/Pagination/CountWalkerTest.php index 5af90e87949..6a223e67f50 100644 --- a/tests/Tests/ORM/Tools/Pagination/CountWalkerTest.php +++ b/tests/Tests/ORM/Tools/Pagination/CountWalkerTest.php @@ -27,6 +27,20 @@ public function testCountQuery(): void ); } + public function testCountQueryWithoutDistinctUsesCountStar(): void + { + $query = $this->entityManager->createQuery( + 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a', + ); + $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CountWalker::class]); + $query->setHint(CountWalker::HINT_DISTINCT, false); + + self::assertEquals( + 'SELECT count(*) AS sclr_0 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id', + $query->getSQL(), + ); + } + public function testCountQueryMixedResultsWithName(): void { $query = $this->entityManager->createQuery(