Skip to content

Commit e1551a4

Browse files
staabmclxmstaab
andauthored
support debugMode() in query plan analysis (#398)
* support debugMode() in query plan analysis * record Co-authored-by: Markus Staab <m.staab@complex-it.de>
1 parent 8b0b4e0 commit e1551a4

File tree

6 files changed

+854
-589
lines changed

6 files changed

+854
-589
lines changed

src/Analyzer/QueryPlanAnalyzerMysql.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,17 @@ public function __construct($connection)
4242
*/
4343
public function analyze(string $query): QueryPlanResult
4444
{
45+
$simulatedQuery = 'EXPLAIN '.$query;
46+
4547
if ($this->connection instanceof PDO) {
46-
$stmt = $this->connection->query('EXPLAIN '.$query);
48+
$stmt = $this->connection->query($simulatedQuery);
4749

4850
// @phpstan-ignore-next-line
49-
return $this->buildResult($stmt);
51+
return $this->buildResult($simulatedQuery, $stmt);
5052
} else {
51-
$result = $this->connection->query('EXPLAIN '.$query);
53+
$result = $this->connection->query($simulatedQuery);
5254
if ($result instanceof \mysqli_result) {
53-
return $this->buildResult($result);
55+
return $this->buildResult($simulatedQuery, $result);
5456
}
5557
}
5658

@@ -60,9 +62,9 @@ public function analyze(string $query): QueryPlanResult
6062
/**
6163
* @param \IteratorAggregate<array-key, array{select_type: string, key: string|null, type: string|null, rows: positive-int, table: ?string}> $it
6264
*/
63-
private function buildResult($it): QueryPlanResult
65+
private function buildResult(string $simulatedQuery, $it): QueryPlanResult
6466
{
65-
$result = new QueryPlanResult();
67+
$result = new QueryPlanResult($simulatedQuery);
6668

6769
$allowedUnindexedReads = QueryReflection::getRuntimeConfiguration()->getNumberOfAllowedUnindexedReads();
6870
if (false === $allowedUnindexedReads) {

src/Analyzer/QueryPlanResult.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ final class QueryPlanResult
1515
*/
1616
private $result = [];
1717

18+
/**
19+
* @var string
20+
*/
21+
private $simulatedQuery;
22+
23+
public function __construct(string $simulatedQuery)
24+
{
25+
$this->simulatedQuery = $simulatedQuery;
26+
}
27+
28+
public function getSimulatedQuery(): string
29+
{
30+
return $this->simulatedQuery;
31+
}
32+
1833
/**
1934
* @param self::* $result
2035
*

src/Rules/QueryPlanAnalyzerRule.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,19 @@ private function analyze(CallLike $callLike, Scope $scope): array
124124
$ruleErrors = [];
125125
$queryReflection = new QueryReflection();
126126
$proposal = "\n\nConsider optimizing the query.\nIn some cases this is not a problem and this error should be ignored.";
127+
127128
foreach ($queryReflection->analyzeQueryPlan($scope, $queryExpr, $parameterTypes) as $queryPlanResult) {
129+
$suffix = $proposal;
130+
if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
131+
$suffix = $proposal."\n\nSimulated query: ".$queryPlanResult->getSimulatedQuery();
132+
}
133+
128134
$notUsingIndex = $queryPlanResult->getTablesNotUsingIndex();
129135
if (\count($notUsingIndex) > 0) {
130136
foreach ($notUsingIndex as $table) {
131137
$ruleErrors[] = RuleErrorBuilder::message(
132138
sprintf(
133-
"Query is not using an index on table '%s'.".$proposal,
139+
"Query is not using an index on table '%s'.".$suffix,
134140
$table
135141
))
136142
->line($callLike->getLine())
@@ -141,7 +147,7 @@ private function analyze(CallLike $callLike, Scope $scope): array
141147
foreach ($queryPlanResult->getTablesDoingTableScan() as $table) {
142148
$ruleErrors[] = RuleErrorBuilder::message(
143149
sprintf(
144-
"Query is using a full-table-scan on table '%s'.".$proposal,
150+
"Query is using a full-table-scan on table '%s'.".$suffix,
145151
$table
146152
))
147153
->line($callLike->getLine())
@@ -152,7 +158,7 @@ private function analyze(CallLike $callLike, Scope $scope): array
152158
foreach ($queryPlanResult->getTablesDoingUnindexedReads() as $table) {
153159
$ruleErrors[] = RuleErrorBuilder::message(
154160
sprintf(
155-
"Query is triggering too many unindexed-reads on table '%s'.".$proposal,
161+
"Query is triggering too many unindexed-reads on table '%s'.".$suffix,
156162
$table
157163
))
158164
->line($callLike->getLine())

tests/rules/QueryPlanAnalyzerRuleTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@ class QueryPlanAnalyzerRuleTest extends RuleTestCase
2020
* @var positive-int
2121
*/
2222
private $numberOfRowsNotRequiringIndex;
23+
/**
24+
* @var bool
25+
*/
26+
private $debugMode = false;
2327

2428
protected function tearDown(): void
2529
{
30+
QueryReflection::getRuntimeConfiguration()->debugMode(false);
2631
QueryReflection::getRuntimeConfiguration()->analyzeQueryPlans(false);
2732
}
2833

2934
protected function getRule(): Rule
3035
{
36+
QueryReflection::getRuntimeConfiguration()->debugMode($this->debugMode);
3137
QueryReflection::getRuntimeConfiguration()->analyzeQueryPlans($this->numberOfAllowedUnindexedReads, $this->numberOfRowsNotRequiringIndex);
3238

3339
return self::getContainer()->getByType(QueryPlanAnalyzerRule::class);
@@ -84,4 +90,50 @@ public function testNotUsingIndex(): void
8490
],
8591
]);
8692
}
93+
94+
public function testNotUsingIndexInDebugMode(): void
95+
{
96+
if ('pdo-pgsql' === getenv('DBA_REFLECTOR')) {
97+
$this->markTestSkipped('query plan analyzer is not yet implemented for pgsql');
98+
}
99+
100+
if ('recording' !== getenv('DBA_MODE')) {
101+
$this->markTestSkipped('query plan analyzer requires a active database connection');
102+
}
103+
104+
$this->debugMode = true;
105+
$this->numberOfAllowedUnindexedReads = true;
106+
$this->numberOfRowsNotRequiringIndex = 2;
107+
108+
$proposal = "\n\nConsider optimizing the query.\nIn some cases this is not a problem and this error should be ignored.";
109+
$tip = 'see Mysql Docs https://dev.mysql.com/doc/refman/8.0/en/select-optimization.html';
110+
111+
$this->analyse([__DIR__.'/data/query-plan-analyzer.php'], [
112+
[
113+
"Query is not using an index on table 'ada'.".$proposal."\n\nSimulated query: EXPLAIN SELECT * FROM `ada` WHERE email = 'test@example.com'",
114+
12,
115+
$tip,
116+
],
117+
[
118+
"Query is not using an index on table 'ada'.".$proposal."\n\nSimulated query: EXPLAIN SELECT *,adaid FROM `ada` WHERE email = 'test@example.com'",
119+
17,
120+
$tip,
121+
],
122+
[
123+
"Query is not using an index on table 'ada'.".$proposal."\n\nSimulated query: EXPLAIN SELECT * FROM ada WHERE email = '1970-01-01'",
124+
22,
125+
$tip,
126+
],
127+
[
128+
"Query is not using an index on table 'ada'.".$proposal."\n\nSimulated query: EXPLAIN SELECT * FROM ada WHERE email = '1970-01-01'",
129+
23,
130+
$tip,
131+
],
132+
[
133+
"Query is not using an index on table 'ada'.".$proposal."\n\nSimulated query: EXPLAIN SELECT * FROM ada WHERE email = '1970-01-01'",
134+
28,
135+
$tip,
136+
],
137+
]);
138+
}
87139
}

0 commit comments

Comments
 (0)