Skip to content

Commit

Permalink
Make CountWalker use COUNT(*) when $distinct is explicitly set to fal…
Browse files Browse the repository at this point in the history
…se (doctrine#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.
  • Loading branch information
d-ph committed Jul 22, 2024
1 parent 1281707 commit 57247ed
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 16 deletions.
36 changes: 20 additions & 16 deletions src/Tools/Pagination/CountWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
];
Expand Down
14 changes: 14 additions & 0 deletions tests/Tests/ORM/Tools/Pagination/CountWalkerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 57247ed

Please sign in to comment.