Files
michaelschiemer/src/Framework/Database/Indexing/CompositeIndexGenerator.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

284 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Database\Indexing;
use App\Framework\Database\Indexing\ValueObjects\IndexRecommendation;
use App\Framework\Database\Indexing\ValueObjects\IndexType;
use App\Framework\Database\Indexing\ValueObjects\RecommendationPriority;
use App\Framework\Database\Profiling\SlowQueryDetector;
use App\Framework\Database\Profiling\SlowQueryPattern;
/**
* Generates smart composite index recommendations based on query patterns
*
* Analyzes slow queries and suggests optimal composite indexes
*/
final readonly class CompositeIndexGenerator
{
public function __construct(
private IndexAnalyzer $analyzer,
private IndexUsageTracker $usageTracker,
private SlowQueryDetector $slowQueryDetector
) {}
/**
* Generate index recommendations for a table
*
* @return array<IndexRecommendation>
*/
public function generateRecommendations(string $tableName): array
{
$recommendations = [];
// Get slow queries affecting this table
$slowQueries = $this->getSlowQueriesForTable($tableName);
foreach ($slowQueries as $query) {
$analysis = $this->analyzer->analyzeQuery($query['sql']);
// If query uses filesort or temporary table, suggest index
if ($analysis['using_filesort'] || $analysis['using_temporary']) {
$recommendation = $this->analyzeQueryForIndex($query, $tableName, $analysis);
if ($recommendation) {
$recommendations[] = $recommendation;
}
}
// If full table scan detected
if ($analysis['key_type'] === 'ALL' || $analysis['key_type'] === 'scan') {
$recommendation = $this->suggestIndexForTableScan($query, $tableName, $analysis);
if ($recommendation) {
$recommendations[] = $recommendation;
}
}
}
// Deduplicate and prioritize recommendations
return $this->deduplicateRecommendations($recommendations);
}
/**
* Suggest composite index based on WHERE and ORDER BY clauses
*/
private function analyzeQueryForIndex(array $query, string $tableName, array $analysis): ?IndexRecommendation
{
$whereColumns = $this->extractWhereColumns($query['sql']);
$orderByColumns = $this->extractOrderByColumns($query['sql']);
if (empty($whereColumns) && empty($orderByColumns)) {
return null;
}
// Composite index: WHERE columns first, then ORDER BY columns
$columns = array_unique(array_merge($whereColumns, $orderByColumns));
if (empty($columns)) {
return null;
}
$estimatedSpeedup = $this->estimateSpeedup($analysis);
$affectedQueries = $this->countAffectedQueries($columns, $tableName);
return new IndexRecommendation(
tableName: $tableName,
columns: $columns,
indexType: IndexType::BTREE,
reason: $this->generateRecommendationReason($whereColumns, $orderByColumns, $analysis),
priority: RecommendationPriority::fromMetrics($estimatedSpeedup, $affectedQueries),
estimatedSpeedup: $estimatedSpeedup,
affectedQueries: $affectedQueries
);
}
/**
* Suggest index for full table scan
*/
private function suggestIndexForTableScan(array $query, string $tableName, array $analysis): ?IndexRecommendation
{
$whereColumns = $this->extractWhereColumns($query['sql']);
if (empty($whereColumns)) {
return null;
}
$estimatedSpeedup = max(10.0, $analysis['rows_examined'] / 100);
$affectedQueries = $this->countAffectedQueries($whereColumns, $tableName);
return new IndexRecommendation(
tableName: $tableName,
columns: $whereColumns,
indexType: IndexType::BTREE,
reason: "Full table scan detected ({$analysis['rows_examined']} rows examined)",
priority: RecommendationPriority::fromMetrics($estimatedSpeedup, $affectedQueries),
estimatedSpeedup: $estimatedSpeedup,
affectedQueries: $affectedQueries
);
}
/**
* Extract columns from WHERE clause
*/
private function extractWhereColumns(string $sql): array
{
$columns = [];
// Simple regex-based extraction (can be improved with SQL parser)
if (preg_match('/WHERE\s+(.*?)(?:ORDER BY|GROUP BY|LIMIT|$)/is', $sql, $matches)) {
$whereClause = $matches[1];
// Extract column names from conditions like "column = value" or "column IN (...)"
if (preg_match_all('/(\w+)\s*(?:=|IN|>|<|>=|<=|LIKE)/i', $whereClause, $columnMatches)) {
$columns = $columnMatches[1];
}
}
return array_unique(array_filter($columns, fn($col) => !in_array(strtoupper($col), ['AND', 'OR', 'NOT'])));
}
/**
* Extract columns from ORDER BY clause
*/
private function extractOrderByColumns(string $sql): array
{
$columns = [];
if (preg_match('/ORDER BY\s+(.*?)(?:LIMIT|$)/is', $sql, $matches)) {
$orderByClause = $matches[1];
// Extract column names, ignoring ASC/DESC
if (preg_match_all('/(\w+)\s*(?:ASC|DESC)?/i', $orderByClause, $columnMatches)) {
$columns = array_filter($columnMatches[1], fn($col) => $col !== 'ASC' && $col !== 'DESC');
}
}
return array_unique($columns);
}
/**
* Estimate query speedup from adding index
*/
private function estimateSpeedup(array $analysis): float
{
$rowsExamined = $analysis['rows_examined'];
if ($rowsExamined === 0) {
return 1.0;
}
// Rough speedup estimation based on rows examined
if ($rowsExamined > 100000) {
return 20.0;
}
if ($rowsExamined > 10000) {
return 10.0;
}
if ($rowsExamined > 1000) {
return 5.0;
}
if ($rowsExamined > 100) {
return 2.0;
}
return 1.5;
}
/**
* Count queries that would benefit from this index
*/
private function countAffectedQueries(array $columns, string $tableName): int
{
// Simplified: count slow queries with these columns
// In production, would analyze query log more thoroughly
return count($this->getSlowQueriesForTable($tableName));
}
/**
* Generate human-readable recommendation reason
*/
private function generateRecommendationReason(array $whereColumns, array $orderByColumns, array $analysis): string
{
$reasons = [];
if (!empty($whereColumns)) {
$whereStr = implode(', ', $whereColumns);
$reasons[] = "WHERE clause on columns: {$whereStr}";
}
if (!empty($orderByColumns)) {
$orderStr = implode(', ', $orderByColumns);
$reasons[] = "ORDER BY columns: {$orderStr}";
}
if ($analysis['using_filesort']) {
$reasons[] = "Query uses filesort";
}
if ($analysis['using_temporary']) {
$reasons[] = "Query uses temporary table";
}
return implode('; ', $reasons);
}
/**
* Get slow queries affecting a specific table
*/
private function getSlowQueriesForTable(string $tableName): array
{
// This would integrate with SlowQueryDetector
// For now, return empty array (would need slow query log access)
return [];
}
/**
* Deduplicate recommendations and keep highest priority
*/
private function deduplicateRecommendations(array $recommendations): array
{
$unique = [];
foreach ($recommendations as $recommendation) {
$key = $recommendation->tableName . ':' . implode(',', $recommendation->columns);
if (!isset($unique[$key]) ||
$this->isPriorityHigher($recommendation->priority, $unique[$key]->priority)) {
$unique[$key] = $recommendation;
}
}
// Sort by priority (CRITICAL > HIGH > MEDIUM > LOW)
usort($unique, function(IndexRecommendation $a, IndexRecommendation $b) {
$priorityOrder = [
RecommendationPriority::CRITICAL->value => 4,
RecommendationPriority::HIGH->value => 3,
RecommendationPriority::MEDIUM->value => 2,
RecommendationPriority::LOW->value => 1
];
return ($priorityOrder[$b->priority->value] ?? 0) - ($priorityOrder[$a->priority->value] ?? 0);
});
return array_values($unique);
}
/**
* Compare priority levels
*/
private function isPriorityHigher(RecommendationPriority $priority1, RecommendationPriority $priority2): bool
{
$priorityOrder = [
RecommendationPriority::CRITICAL->value => 4,
RecommendationPriority::HIGH->value => 3,
RecommendationPriority::MEDIUM->value => 2,
RecommendationPriority::LOW->value => 1
];
return ($priorityOrder[$priority1->value] ?? 0) > ($priorityOrder[$priority2->value] ?? 0);
}
}