Skip to content

Commit 55e386e

Browse files
committed
perf: optimize SchemaAnalyzer to reduce database queries
Significantly improve performance of optimize analyze command by eliminating duplicate database queries. This reduces test execution time from ~32s to ~24s for Oracle tests. Optimizations: 1. Pass cached table info and foreign keys to IndexSuggestionAnalyzer to avoid duplicate TableManager::info() and getForeignKeys() calls 2. Optimize getTableRowCount() - only called for tables with issues 3. Remove redundant tableExists() check - handle errors via try-catch 4. Filter Oracle system tables (BIN$%, SYS_%, DUAL) from analysis 5. Pass cached indexes to hasCompositeIndex() to avoid duplicate queries Performance improvement: ~25% faster (32s -> 24s for 5 Oracle optimize tests)
1 parent 152dc49 commit 55e386e

File tree

4 files changed

+68
-28
lines changed

4 files changed

+68
-28
lines changed

src/cli/IndexSuggestionAnalyzer.php

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,29 @@ public function __construct(PdoDb $db)
2323
*
2424
* @param string $table Table name
2525
* @param array<string, mixed> $options Analysis options
26+
* @param array<string, mixed>|null $cachedInfo Pre-fetched table info (columns, indexes) to avoid duplicate queries
27+
* @param array<int, array<string, mixed>>|null $cachedForeignKeys Pre-fetched foreign keys to avoid duplicate queries
2628
*
2729
* @return array<int, array<string, mixed>> Array of suggestions with priority, reason, and SQL
2830
*/
29-
public function analyze(string $table, array $options = []): array
31+
public function analyze(string $table, array $options = [], ?array $cachedInfo = null, ?array $cachedForeignKeys = null): array
3032
{
3133
$suggestions = [];
3234

33-
// Get table structure
34-
$info = TableManager::info($this->db, $table);
35+
// Use cached data if provided, otherwise fetch
36+
if ($cachedInfo !== null) {
37+
$info = $cachedInfo;
38+
} else {
39+
$info = TableManager::info($this->db, $table);
40+
}
3541
$columns = $info['columns'] ?? [];
3642
$existingIndexes = $this->extractIndexes($info['indexes'] ?? []);
37-
$foreignKeys = $this->getForeignKeys($table);
43+
44+
if ($cachedForeignKeys !== null) {
45+
$foreignKeys = $cachedForeignKeys;
46+
} else {
47+
$foreignKeys = $this->getForeignKeys($table);
48+
}
3849

3950
// Analyze columns
4051
$columnNames = $this->extractColumnNames($columns);
@@ -45,7 +56,7 @@ public function analyze(string $table, array $options = []): array
4556
$suggestions = array_merge($suggestions, $fkSuggestions);
4657

4758
// 2. Common patterns (MEDIUM priority)
48-
$patternSuggestions = $this->suggestCommonPatterns($table, $columns, $indexedColumns);
59+
$patternSuggestions = $this->suggestCommonPatterns($table, $columns, $indexedColumns, $existingIndexes);
4960
$suggestions = array_merge($suggestions, $patternSuggestions);
5061

5162
// 3. Timestamp columns for sorting (LOW priority)
@@ -225,10 +236,11 @@ protected function suggestForeignKeyIndexes(string $table, array $foreignKeys, a
225236
* @param string $table Table name
226237
* @param array<int, array<string, mixed>> $columns Table columns
227238
* @param array<string> $indexedColumns Already indexed columns
239+
* @param array<string, array<int, string>> $existingIndexes Existing indexes (for composite index check)
228240
*
229241
* @return array<int, array<string, mixed>> Suggestions
230242
*/
231-
protected function suggestCommonPatterns(string $table, array $columns, array $indexedColumns): array
243+
protected function suggestCommonPatterns(string $table, array $columns, array $indexedColumns, array $existingIndexes): array
232244
{
233245
$suggestions = [];
234246
$columnNames = $this->extractColumnNames($columns);
@@ -285,7 +297,7 @@ protected function suggestCommonPatterns(string $table, array $columns, array $i
285297
// Composite index for status + created_at pattern
286298
if (in_array('status', $columnNames, true) && in_array('created_at', $columnNames, true)) {
287299
$compositeCols = ['status', 'created_at'];
288-
$hasComposite = $this->hasCompositeIndex($table, $compositeCols);
300+
$hasComposite = $this->hasCompositeIndex($table, $compositeCols, $existingIndexes);
289301
if (!$hasComposite) {
290302
$indexName = $this->generateIndexName($table, $compositeCols);
291303
$sql = $this->buildCreateIndexSql($table, $indexName, $compositeCols);
@@ -397,14 +409,19 @@ protected function findColumn(array $columns, string $columnName): ?array
397409
*
398410
* @param string $table Table name
399411
* @param array<int, string> $columns Column names
412+
* @param array<string, array<int, string>>|null $cachedIndexes Pre-extracted indexes to avoid duplicate queries
400413
*
401414
* @return bool
402415
*/
403-
protected function hasCompositeIndex(string $table, array $columns): bool
416+
protected function hasCompositeIndex(string $table, array $columns, ?array $cachedIndexes = null): bool
404417
{
405418
try {
406-
$info = TableManager::info($this->db, $table);
407-
$indexes = $this->extractIndexes($info['indexes'] ?? []);
419+
if ($cachedIndexes !== null) {
420+
$indexes = $cachedIndexes;
421+
} else {
422+
$info = TableManager::info($this->db, $table);
423+
$indexes = $this->extractIndexes($info['indexes'] ?? []);
424+
}
408425

409426
foreach ($indexes as $indexCols) {
410427
if (count($indexCols) === count($columns)) {

src/cli/SchemaAnalyzer.php

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,18 @@ public function analyze(?string $schema = null): array
123123
$tablesWithIssues++;
124124
}
125125

126-
// Info messages (low priority)
127-
$rowCount = $this->getTableRowCount($table);
128-
if ($rowCount > 1000000) {
129-
$info[] = [
130-
'table' => $table,
131-
'message' => "Table '{$table}': " . number_format($rowCount) . ' rows, consider partitioning',
132-
'type' => 'large_table',
133-
];
126+
// Info messages (low priority) - only check row count for tables that might be large
127+
// Skip row count check for tables without issues to speed up analysis
128+
// Only check if we already detected issues (likely to be more important tables)
129+
if ($hasIssues) {
130+
$rowCount = $this->getTableRowCount($table);
131+
if ($rowCount > 1000000) {
132+
$info[] = [
133+
'table' => $table,
134+
'message' => "Table '{$table}': " . number_format($rowCount) . ' rows, consider partitioning',
135+
'type' => 'large_table',
136+
];
137+
}
134138
}
135139
}
136140

@@ -157,22 +161,24 @@ public function analyze(?string $schema = null): array
157161
*/
158162
public function analyzeTable(string $table): array
159163
{
160-
if (!TableManager::tableExists($this->db, $table)) {
164+
// Skip tableExists check - if table doesn't exist, info() will fail and we'll handle it
165+
// This saves one query per table
166+
try {
167+
$info = TableManager::info($this->db, $table);
168+
} catch (\Exception $e) {
161169
return [
162170
'table' => $table,
163-
'error' => "Table '{$table}' does not exist",
171+
'error' => "Table '{$table}' does not exist or cannot be accessed: " . $e->getMessage(),
164172
];
165173
}
166-
167-
$info = TableManager::info($this->db, $table);
168174
$indexes = $this->extractIndexes($info['indexes'] ?? []);
169175
$primaryKey = $this->extractPrimaryKey($table, $info['indexes'] ?? []);
170176
$foreignKeys = $this->getForeignKeys($table);
171177
$redundant = $this->redundantIndexDetector->detect($table, $indexes);
172178
$missingFkIndexes = $this->findMissingFkIndexes($table, $foreignKeys, $indexes);
173179

174-
// Get index suggestions
175-
$suggestions = $this->indexSuggestionAnalyzer->analyze($table, []);
180+
// Get index suggestions - pass cached data to avoid duplicate queries
181+
$suggestions = $this->indexSuggestionAnalyzer->analyze($table, [], $info, $foreignKeys);
176182

177183
return [
178184
'table' => $table,

src/dialects/oracle/OracleDialect.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,7 +2696,12 @@ public function listTables(PdoDb $db, ?string $schema = null): array
26962696
{
26972697
$schemaName = $schema ?? 'USER';
26982698
if ($schemaName === 'USER') {
2699-
$sql = 'SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME';
2699+
// Exclude Oracle system tables and recycle bin objects for better performance
2700+
$sql = "SELECT TABLE_NAME FROM USER_TABLES
2701+
WHERE TABLE_NAME NOT LIKE 'BIN$%'
2702+
AND TABLE_NAME NOT LIKE 'SYS_%'
2703+
AND TABLE_NAME NOT IN ('DUAL')
2704+
ORDER BY TABLE_NAME";
27002705
$rows = $db->rawQuery($sql);
27012706
} else {
27022707
$sql = 'SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = UPPER(:schema) ORDER BY TABLE_NAME';
@@ -2722,8 +2727,21 @@ public function listTables(PdoDb $db, ?string $schema = null): array
27222727
}
27232728
return '';
27242729
}, $rows);
2725-
// Filter out empty strings
2726-
return array_filter($names, static fn (string $name): bool => $name !== '');
2730+
// Filter out empty strings and system tables
2731+
return array_filter($names, static function (string $name): bool {
2732+
if ($name === '') {
2733+
return false;
2734+
}
2735+
// Additional filtering for system tables (case-insensitive)
2736+
$nameUpper = strtoupper($name);
2737+
// Skip Oracle system tables and recycle bin
2738+
if (str_starts_with($nameUpper, 'BIN$') ||
2739+
str_starts_with($nameUpper, 'SYS_') ||
2740+
$nameUpper === 'DUAL') {
2741+
return false;
2742+
}
2743+
return true;
2744+
});
27272745
}
27282746

27292747
/**

tests/shared/AiCommandCliTests.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ public function testAiCommandSchema(): void
197197
}
198198
}
199199

200-
201200
public static function tearDownAfterClass(): void
202201
{
203202
if (self::$db !== null) {

0 commit comments

Comments
 (0)