Skip to content

Commit d2de776

Browse files
committed
Can now remove useless GROUP BY statements
1 parent fd480af commit d2de776

File tree

5 files changed

+107
-151
lines changed

5 files changed

+107
-151
lines changed

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ This is a drop-in zero-configuration Doctrine extension that optimizes all SQL q
77

88
* Removes JOIN's when they are not referenced anywhere else in the query and cannot have an impact on the result-set
99
size.
10-
11-
### To-DO List for the future:
12-
13-
* Removing of useless GROUP-BY statements.
10+
* Removes GROUP BY statements when all JOIN's are one-to-one and the grouping expression is a unique column.
1411

1512
## Setup
1613

composer.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

php/DefaultSQLOptimizer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Addiks\DoctrineSqlAutoOptimizer;
1313

14+
use Addiks\DoctrineSqlAutoOptimizer\Mutators\RemovePointlessGroupByMutator;
1415
use Addiks\DoctrineSqlAutoOptimizer\Mutators\RemovePointlessJoinsMutator;
1516
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstMutableNode;
1617
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstNode;
@@ -60,6 +61,7 @@ public static function defaultMutators(): array
6061
{
6162
return [
6263
RemovePointlessJoinsMutator::create(),
64+
RemovePointlessGroupByMutator::create(),
6365
];
6466
}
6567

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
/**
3+
* Copyright (C) 2019 Gerrit Addiks.
4+
* This package (including this file) was released under the terms of the GPL-3.0.
5+
* You should have received a copy of the GNU General Public License along with this program.
6+
* If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy.
7+
*
8+
* @license GPL-3.0
9+
* @author Gerrit Addiks <gerrit@addiks.de>
10+
*/
11+
12+
namespace Addiks\DoctrineSqlAutoOptimizer\Mutators;
13+
14+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstColumn;
15+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstExpression;
16+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstGroupBy;
17+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstJoin;
18+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstMutableNode;
19+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstNode;
20+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstSelect;
21+
use Addiks\StoredSQL\ExecutionContext;
22+
use Addiks\StoredSQL\Schema\Column;
23+
use Addiks\StoredSQL\Schema\Schemas;
24+
use Closure;
25+
26+
/** @psalm-import-type Mutator from SqlAstMutableNode */
27+
final class RemovePointlessGroupByMutator
28+
{
29+
/** @return Mutator */
30+
public static function create(): Closure
31+
{
32+
return Closure::fromCallable([
33+
new RemovePointlessGroupByMutator(),
34+
'removePointlessGroupBy',
35+
]);
36+
}
37+
38+
public function removePointlessGroupBy(
39+
SqlAstNode $node,
40+
int $offset,
41+
SqlAstMutableNode $parent,
42+
Schemas $schemas
43+
): void {
44+
if ($node instanceof SqlAstSelect) {
45+
/** @var SqlAstSelect $select */
46+
$select = $node;
47+
48+
/** @var SqlAstGroupBy|null $groupBy */
49+
$groupBy = $select->groupBy();
50+
51+
if (is_object($groupBy)) {
52+
/** @var ExecutionContext $context */
53+
$context = $select->createContext($schemas);
54+
55+
if ($this->isUniqueGroupedExpression($groupBy->expression(), $context)
56+
&& $this->hasOnlyOneToOneJoins($select, $context)) {
57+
$select->replaceGroupBy(null);
58+
}
59+
}
60+
}
61+
}
62+
63+
private function isUniqueGroupedExpression(
64+
SqlAstExpression $expression,
65+
ExecutionContext $context
66+
): bool {
67+
if ($expression instanceof SqlAstColumn) {
68+
/** @var Column|null $column */
69+
$column = $context->columnByNode($expression);
70+
71+
if ($column->unique()) {
72+
return true;
73+
}
74+
}
75+
76+
return false;
77+
}
78+
79+
private function hasOnlyOneToOneJoins(
80+
SqlAstSelect $select,
81+
ExecutionContext $context
82+
): bool {
83+
/** @var bool $hasOnlyOneToOneJoins */
84+
$hasOnlyOneToOneJoins = true;
85+
86+
/** @var SqlAstJoin $join */
87+
foreach ($select->joins() as $join) {
88+
if ($join->canChangeResultSetSize($context)) {
89+
$hasOnlyOneToOneJoins = false;
90+
break;
91+
}
92+
}
93+
94+
return $hasOnlyOneToOneJoins;
95+
}
96+
}

php/Mutators/RemovePointlessJoinsMutator.php

Lines changed: 4 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,15 @@
1111

1212
namespace Addiks\DoctrineSqlAutoOptimizer\Mutators;
1313

14+
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstAllColumnsSelector;
1415
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstColumn;
15-
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstExpression;
1616
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstJoin;
1717
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstMutableNode;
1818
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstNode;
19-
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstOperation;
2019
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstSelect;
2120
use Addiks\StoredSQL\ExecutionContext;
22-
use Addiks\StoredSQL\Schema\Column;
2321
use Addiks\StoredSQL\Schema\Schemas;
2422
use Closure;
25-
use Webmozart\Assert\Assert;
26-
use Addiks\StoredSQL\AbstractSyntaxTree\SqlAstAllColumnsSelector;
2723

2824
/** @psalm-import-type Mutator from SqlAstMutableNode */
2925
final class RemovePointlessJoinsMutator
@@ -56,7 +52,7 @@ public function removePointlessJoins(
5652

5753
foreach ($select->joins() as $join) {
5854
if (!$this->isJoinAliasUsedInSelect($join, $select)
59-
&& !$this->canJoinChangeResultSetSize($join, $select, $context)) {
55+
&& !$join->canJoinChangeResultSetSize($context)) {
6056
$select->replaceJoin($join, null);
6157
}
6258
}
@@ -68,18 +64,15 @@ private function isJoinAliasUsedInSelect(SqlAstJoin $join, SqlAstSelect $select)
6864
/** @var bool $isJoinAliasUsedInSelect */
6965
$isJoinAliasUsedInSelect = false;
7066

71-
/** @var string $joinName */
72-
$joinName = ($join->alias() ?? $join->joinedTable())->toSql();
73-
7467
/** @var SqlAstNode $selectChildNode */
7568
foreach ($select->children() as $selectChildNode) {
7669
if ($selectChildNode === $join) {
7770
continue;
7871
}
7972

80-
$selectChildNode->walk([function (SqlAstNode $node) use (&$isJoinAliasUsedInSelect, $joinName): void {
73+
$selectChildNode->walk([function (SqlAstNode $node) use (&$isJoinAliasUsedInSelect, $join): void {
8174
if ($node instanceof SqlAstColumn) {
82-
if ($node->tableNameString() === $joinName) {
75+
if ($node->tableNameString() === $join->aliasName()) {
8376
$isJoinAliasUsedInSelect = true;
8477
}
8578

@@ -101,136 +94,4 @@ private function isJoinAliasUsedInSelect(SqlAstJoin $join, SqlAstSelect $select)
10194

10295
return $isJoinAliasUsedInSelect;
10396
}
104-
105-
private function canJoinChangeResultSetSize(
106-
SqlAstJoin $join,
107-
SqlAstSelect $select,
108-
ExecutionContext $context
109-
): bool {
110-
/** @var bool $canJoinChangeResultSetSize */
111-
$canJoinChangeResultSetSize = true;
112-
113-
/** @var SqlAstExpression|null $condition */
114-
$condition = $join->condition();
115-
116-
if ($join->isUsingColumnCondition()) {
117-
# "... JOIN foo USING(bar_id)"
118-
119-
if ($condition instanceof SqlAstColumn) {
120-
return $this->canUsingJoinChangeResultSetSize($join, $context);
121-
}
122-
123-
} elseif (is_object($condition)) {
124-
# "... JOIN foo ON(foo.id = bar.foo_id)"
125-
126-
return $this->canOnJoinChangeResultSetSize($join, $context);
127-
}
128-
129-
return true;
130-
}
131-
132-
private function canUsingJoinChangeResultSetSize(SqlAstJoin $join, ExecutionContext $context): bool
133-
{
134-
/** @var SqlAstExpression|null $column */
135-
$column = $join->condition();
136-
137-
Assert::isInstanceOf($column, SqlAstColumn::class);
138-
139-
/** @var string $columnName */
140-
$columnName = $column->columnNameString();
141-
142-
return !$context->isOneToOneRelation(
143-
$context->findTableWithColumn($columnName),
144-
$columnName,
145-
$join->joinedTable(),
146-
$columnName
147-
);
148-
}
149-
150-
private function canOnJoinChangeResultSetSize(SqlAstJoin $join, ExecutionContext $context): bool
151-
{
152-
/** @var SqlAstExpression|null $condition */
153-
$condition = $join->condition();
154-
155-
/** @var array<SqlAstOperation> $equations */
156-
$equations = $condition->extractFundamentalEquations();
157-
158-
/** @var array<SqlAstOperation> $alwaysFalseEquations */
159-
$alwaysFalseEquations = array_filter($equations, function (SqlAstOperation $equation): bool {
160-
return $equation->isAlwaysFalse();
161-
});
162-
163-
if (!empty($alwaysFalseEquations)) {
164-
return true;
165-
}
166-
167-
$equations = array_filter($equations, function (SqlAstOperation $equation): bool {
168-
return !$equation->isAlwaysTrue();
169-
});
170-
171-
/** @var string $joinAlias */
172-
$joinAlias = $join->aliasName();
173-
174-
foreach ($equations as $equation) {
175-
/** @var SqlAstExpression $leftSide */
176-
$leftSide = $equation->leftSide();
177-
178-
/** @var SqlAstExpression $rightSide */
179-
$rightSide = $equation->rightSide();
180-
181-
if ($leftSide instanceof SqlAstColumn && $leftSide->tableNameString() === $joinAlias) {
182-
/** @var SqlAstExpression $joiningSide */
183-
$joiningSide = $rightSide;
184-
185-
/** @var SqlAstExpression $joinedSide */
186-
$joinedSide = $leftSide;
187-
188-
} elseif ($rightSide instanceof SqlAstColumn && $rightSide->tableNameString() === $joinAlias) {
189-
/** @var SqlAstExpression $joiningSide */
190-
$joiningSide = $leftSide;
191-
192-
/** @var SqlAstExpression $joinedSide */
193-
$joinedSide = $rightSide;
194-
195-
} else {
196-
# Unknown condition, let's assume that this JOIN can change result size to be safe.
197-
return true;
198-
}
199-
200-
if ($joinedSide instanceof SqlAstColumn && $joiningSide instanceof SqlAstColumn) {
201-
/** @var Column|null $joinedColumn */
202-
$joinedColumn = $context->columnByNode($joinedSide);
203-
204-
/** @var Column|null $joiningColumn */
205-
$joiningColumn = $context->columnByNode($joiningSide);
206-
207-
if (is_object($joinedColumn) && is_object($joiningColumn)) {
208-
foreach ([
209-
[$join->isRightOuterJoin(), $joiningColumn],
210-
[$join->isLeftOuterJoin(), $joinedColumn],
211-
] as [$isOuterJoin, $column]) {
212-
if ($isOuterJoin) {
213-
if ($column->nullable() || !$column->unique()) {
214-
return true;
215-
}
216-
217-
} else {
218-
if (!$column->unique()) {
219-
return true;
220-
}
221-
}
222-
}
223-
224-
return false;
225-
}
226-
227-
} else {
228-
# Either a literal (which will change result size), or an unknown condition (which might change it).
229-
return true;
230-
}
231-
}
232-
233-
# All equations are either always true or always false. Either way, this JOIN changes the result size.
234-
return true;
235-
}
23697
}

0 commit comments

Comments
 (0)