-
-
Notifications
You must be signed in to change notification settings - Fork 23
mysql query plan analyzer #377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
945c0a7
query plan analyzer
clxmstaab 170190a
wip
clxmstaab 5fe356d
Create RecordingReflector.php
clxmstaab e9f16e0
fix
clxmstaab 9778688
Merge branch 'main' into query-plan
clxmstaab cf9dcb8
progress
clxmstaab 0f469e5
cleanup
clxmstaab 56f7136
Update QueryReflection.php
clxmstaab bf7eefc
progress
clxmstaab c357e48
fix
clxmstaab d607ad5
fix
clxmstaab 1701dca
progress
clxmstaab bf9e3c3
Update query-plan-analysis.md
staabm d76a1f9
Merge branch 'main' into query-plan
clxmstaab 66cd396
progress
clxmstaab 6632980
record
clxmstaab bacd795
fix
clxmstaab d23d0b5
Update QueryPlanAnalyzerRuleTest.php
clxmstaab b19b74e
fix
clxmstaab 2d77d47
record
clxmstaab dd55bf1
fix
clxmstaab b4d461d
fix
clxmstaab e6501c7
prevent false-positives on small tables
clxmstaab 3735445
record
clxmstaab 57665c9
ignore derived tables
clxmstaab 08d9d4e
cs
clxmstaab File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Query Plan Analysis | ||
|
|
||
| Within your `phpstan-dba-bootstrap.php` file, you can optionally enable query plan analysis. | ||
| When enabled, `phpstan-dba` will error when queries are not using indices or queries are inefficient. | ||
|
|
||
| Passing `true` will enable the feature: | ||
|
|
||
| ```php | ||
| $config = new RuntimeConfiguration(); | ||
| $config->analyzeQueryPlans(true); | ||
| ``` | ||
|
|
||
| For more fine grained control, you can pass a positive-integer describing the number of unindexed reads a query is allowed to execute before being considered inefficient. | ||
| This will only affect queries which already use an index. | ||
|
|
||
| ```php | ||
| $config = new RuntimeConfiguration(); | ||
| $config->analyzeQueryPlans(100000); | ||
| ``` | ||
|
|
||
| To disable the effiency analysis but just check for queries not using indices at all, pass `0`. | ||
|
|
||
| ```php | ||
| $config = new RuntimeConfiguration(); | ||
| $config->analyzeQueryPlans(0); | ||
| ``` | ||
|
|
||
| **Note:** For a meaningful performance analysis it is vital to utilize a database, which containts data and schema as similar as possible to the production database. | ||
|
|
||
| **Note:** "Query Plan Analysis" requires an active database connection. | ||
|
|
||
| **Note:** ["Query Plan Analysis" is not yet supported on the PGSQL driver](https://github.com/staabm/phpstan-dba/issues/378) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace staabm\PHPStanDba\Analyzer; | ||
|
|
||
| use mysqli; | ||
| use PDO; | ||
| use PHPStan\ShouldNotHappenException; | ||
| use staabm\PHPStanDba\QueryReflection\QueryReflection; | ||
|
|
||
| final class QueryPlanAnalyzerMysql | ||
| { | ||
| /** | ||
| * number of unindexed reads allowed before a query is considered inefficient. | ||
| */ | ||
| public const DEFAULT_UNINDEXED_READS_THRESHOLD = 100000; | ||
| /** | ||
| * max number of rows in a table, for which we don't report errors, because using a index/table-scan wouldn't improve performance. | ||
| */ | ||
| public const DEFAULT_SMALL_TABLE_THRESHOLD = 1000; | ||
|
|
||
| /** | ||
| * @var PDO|mysqli | ||
| */ | ||
| private $connection; | ||
|
|
||
| /** | ||
| * @param PDO|mysqli $connection | ||
| */ | ||
| public function __construct($connection) | ||
| { | ||
| $this->connection = $connection; | ||
| } | ||
|
|
||
| /** | ||
| * @param non-empty-string $query | ||
| */ | ||
| public function analyze(string $query): QueryPlanResult | ||
| { | ||
| if ($this->connection instanceof PDO) { | ||
| $stmt = $this->connection->query('EXPLAIN '.$query); | ||
|
|
||
| // @phpstan-ignore-next-line | ||
| return $this->buildResult($stmt); | ||
| } else { | ||
| $result = $this->connection->query('EXPLAIN '.$query); | ||
| if ($result instanceof \mysqli_result) { | ||
| return $this->buildResult($result); | ||
| } | ||
| } | ||
|
|
||
| throw new ShouldNotHappenException(); | ||
| } | ||
|
|
||
| /** | ||
| * @param \IteratorAggregate<array-key, array{select_type: string, key: string|null, type: string|null, rows: positive-int, table: ?string}> $it | ||
| */ | ||
| private function buildResult($it): QueryPlanResult | ||
| { | ||
| $result = new QueryPlanResult(); | ||
|
|
||
| $allowedUnindexedReads = QueryReflection::getRuntimeConfiguration()->getNumberOfAllowedUnindexedReads(); | ||
| if (false === $allowedUnindexedReads) { | ||
| throw new ShouldNotHappenException(); | ||
| } | ||
|
|
||
| $allowedRowsNotRequiringIndex = QueryReflection::getRuntimeConfiguration()->getNumberOfRowsNotRequiringIndex(); | ||
| if (false === $allowedRowsNotRequiringIndex) { | ||
| throw new ShouldNotHappenException(); | ||
| } | ||
|
|
||
| foreach ($it as $row) { | ||
| // we cannot analyse tables without rows -> mysql will just return 'no matching row in const table' | ||
| if (null === $row['table']) { | ||
| continue; | ||
| } | ||
|
|
||
| if (null === $row['key'] && $row['rows'] > $allowedRowsNotRequiringIndex) { | ||
| // derived table aka. a expression that generates a table within the scope of a query FROM clause | ||
| // is a temporary table, which indexes cannot be created for. | ||
| if ('derived' === strtolower($row['select_type'])) { | ||
| continue; | ||
| } | ||
|
|
||
| $result->addRow($row['table'], QueryPlanResult::NO_INDEX); | ||
| } else { | ||
| if (null !== $row['type'] && 'all' === strtolower($row['type']) && $row['rows'] > $allowedRowsNotRequiringIndex) { | ||
| $result->addRow($row['table'], QueryPlanResult::TABLE_SCAN); | ||
| } elseif (true === $allowedUnindexedReads && $row['rows'] > self::DEFAULT_UNINDEXED_READS_THRESHOLD) { | ||
| $result->addRow($row['table'], QueryPlanResult::UNINDEXED_READS); | ||
| } elseif (\is_int($allowedUnindexedReads) && $row['rows'] > $allowedUnindexedReads) { | ||
| $result->addRow($row['table'], QueryPlanResult::UNINDEXED_READS); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return $result; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace staabm\PHPStanDba\Analyzer; | ||
|
|
||
| final class QueryPlanResult | ||
| { | ||
| public const NO_INDEX = 'no-index'; | ||
| public const TABLE_SCAN = 'table-scan'; | ||
| public const UNINDEXED_READS = 'unindexed-reads'; | ||
|
|
||
| /** | ||
| * @var array<string, self::*> | ||
| */ | ||
| private $result = []; | ||
|
|
||
| /** | ||
| * @param self::* $result | ||
| * | ||
| * @return void | ||
| */ | ||
| public function addRow(string $table, string $result) | ||
| { | ||
| $this->result[$table] = $result; | ||
| } | ||
|
|
||
| /** | ||
| * @return string[] | ||
| */ | ||
| public function getTablesNotUsingIndex(): array | ||
| { | ||
| $tables = []; | ||
| foreach ($this->result as $table => $result) { | ||
| if (self::NO_INDEX === $result) { | ||
| $tables[] = $table; | ||
| } | ||
| } | ||
|
|
||
| return $tables; | ||
| } | ||
|
|
||
| /** | ||
| * @return string[] | ||
| */ | ||
| public function getTablesDoingTableScan(): array | ||
| { | ||
| $tables = []; | ||
| foreach ($this->result as $table => $result) { | ||
| if (self::TABLE_SCAN === $result) { | ||
| $tables[] = $table; | ||
| } | ||
| } | ||
|
|
||
| return $tables; | ||
| } | ||
|
|
||
| /** | ||
| * @return string[] | ||
| */ | ||
| public function getTablesDoingUnindexedReads(): array | ||
| { | ||
| $tables = []; | ||
| foreach ($this->result as $table => $result) { | ||
| if (self::UNINDEXED_READS === $result) { | ||
| $tables[] = $table; | ||
| } | ||
| } | ||
|
|
||
| return $tables; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add mysqli_query and add a testcase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will be done in a followup