mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
1306 lines
42 KiB
PHP
1306 lines
42 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Fendx\Service\Performance;
|
|
|
|
use Fendx\Service\Performance\Database\QueryProfiler;
|
|
use Fendx\Service\Performance\Database\IndexAnalyzer;
|
|
use Fendx\Service\Performance\Database\QueryOptimizer;
|
|
use Fendx\Service\Performance\Database\ConnectionPool;
|
|
use Fendx\Service\Performance\Reporter\DatabaseReporter;
|
|
|
|
class DatabaseOptimizer
|
|
{
|
|
protected array $config = [];
|
|
protected QueryProfiler $queryProfiler;
|
|
protected IndexAnalyzer $indexAnalyzer;
|
|
protected QueryOptimizer $queryOptimizer;
|
|
protected ConnectionPool $connectionPool;
|
|
protected DatabaseReporter $reporter;
|
|
protected array $queryResults = [];
|
|
protected array $optimizationHistory = [];
|
|
|
|
public function __construct(array $config = [])
|
|
{
|
|
$this->config = array_merge($this->getDefaultConfig(), $config);
|
|
$this->queryProfiler = new QueryProfiler($this->config['query_profiler'] ?? []);
|
|
$this->indexAnalyzer = new IndexAnalyzer($this->config['index_analyzer'] ?? []);
|
|
$this->queryOptimizer = new QueryOptimizer($this->config['query_optimizer'] ?? []);
|
|
$this->connectionPool = new ConnectionPool($this->config['connection_pool'] ?? []);
|
|
$this->reporter = new DatabaseReporter($this->config['reporter'] ?? []);
|
|
}
|
|
|
|
/**
|
|
* Profile database query performance.
|
|
*/
|
|
public function profileQuery(string $query, array $params = [], array $options = []): array
|
|
{
|
|
$testConfig = array_merge($this->config['query_profiling'] ?? [], $options);
|
|
|
|
$result = [
|
|
'query' => $query,
|
|
'params' => $params,
|
|
'executions' => $testConfig['executions'] ?? 100,
|
|
'warmup_executions' => $testConfig['warmup_executions'] ?? 10,
|
|
'execution_times' => [],
|
|
'statistics' => [],
|
|
'query_plan' => [],
|
|
'performance_issues' => [],
|
|
'optimization_suggestions' => [],
|
|
'timestamp' => microtime(true)
|
|
];
|
|
|
|
// Start query profiling
|
|
$this->queryProfiler->startProfiling();
|
|
|
|
// Warmup executions
|
|
for ($i = 0; $i < $result['warmup_executions']; $i++) {
|
|
$this->executeQuery($query, $params);
|
|
}
|
|
|
|
$executionTimes = [];
|
|
$queryPlans = [];
|
|
|
|
// Actual profiling
|
|
for ($i = 0; $i < $result['executions']; $i++) {
|
|
$executionStart = microtime(true);
|
|
|
|
$resultSet = $this->executeQuery($query, $params);
|
|
$queryPlan = $this->getQueryPlan($query, $params);
|
|
|
|
$executionEnd = microtime(true);
|
|
$executionTime = ($executionEnd - $executionStart) * 1000; // milliseconds
|
|
|
|
$executionTimes[] = $executionTime;
|
|
$queryPlans[] = $queryPlan;
|
|
}
|
|
|
|
// Stop profiling
|
|
$profileData = $this->queryProfiler->stopProfiling();
|
|
|
|
$result['execution_times'] = $executionTimes;
|
|
$result['query_plan'] = $this->analyzeQueryPlans($queryPlans);
|
|
$result['profile_data'] = $profileData;
|
|
|
|
// Calculate statistics
|
|
$result['statistics'] = $this->calculateQueryStatistics($executionTimes);
|
|
|
|
// Detect performance issues
|
|
$result['performance_issues'] = $this->detectPerformanceIssues($result);
|
|
|
|
// Generate optimization suggestions
|
|
$result['optimization_suggestions'] = $this->generateQueryOptimizationSuggestions($result);
|
|
|
|
// Store result
|
|
$this->queryResults[] = $result;
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Analyze database indexes.
|
|
*/
|
|
public function analyzeIndexes(string $tableName, array $options = []): array
|
|
{
|
|
$testConfig = array_merge($this->config['index_analysis'] ?? [], $options);
|
|
|
|
$result = [
|
|
'table' => $tableName,
|
|
'current_indexes' => [],
|
|
'unused_indexes' => [],
|
|
'missing_indexes' => [],
|
|
'duplicate_indexes' => [],
|
|
'index_usage_stats' => [],
|
|
'optimization_recommendations' => [],
|
|
'timestamp' => microtime(true)
|
|
];
|
|
|
|
// Get current indexes
|
|
$result['current_indexes'] = $this->getTableIndexes($tableName);
|
|
|
|
// Analyze index usage
|
|
$result['index_usage_stats'] = $this->getIndexUsageStats($tableName);
|
|
|
|
// Find unused indexes
|
|
$result['unused_indexes'] = $this->findUnusedIndexes($result['current_indexes'], $result['index_usage_stats']);
|
|
|
|
// Find missing indexes by analyzing queries
|
|
if ($testConfig['analyze_queries'] ?? true) {
|
|
$result['missing_indexes'] = $this->findMissingIndexes($tableName);
|
|
}
|
|
|
|
// Find duplicate indexes
|
|
$result['duplicate_indexes'] = $this->findDuplicateIndexes($result['current_indexes']);
|
|
|
|
// Generate recommendations
|
|
$result['optimization_recommendations'] = $this->generateIndexRecommendations($result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Optimize database query.
|
|
*/
|
|
public function optimizeQuery(string $query, array $params = [], array $options = []): array
|
|
{
|
|
$testConfig = array_merge($this->config['query_optimization'] ?? [], $options);
|
|
|
|
$result = [
|
|
'original_query' => $query,
|
|
'original_params' => $params,
|
|
'optimized_queries' => [],
|
|
'performance_comparison' => [],
|
|
'best_optimization' => [],
|
|
'improvement_percentage' => 0,
|
|
'timestamp' => microtime(true)
|
|
];
|
|
|
|
// Profile original query
|
|
$originalProfile = $this->profileQuery($query, $params, ['executions' => 50]);
|
|
$result['original_performance'] = $originalProfile;
|
|
|
|
// Generate optimization strategies
|
|
$optimizationStrategies = $this->generateOptimizationStrategies($query, $testConfig);
|
|
|
|
$optimizedResults = [];
|
|
|
|
foreach ($optimizationStrategies as $strategy => $optimizedQuery) {
|
|
try {
|
|
$optimizedProfile = $this->profileQuery($optimizedQuery['query'], $optimizedQuery['params'], ['executions' => 50]);
|
|
|
|
$optimizedResults[$strategy] = [
|
|
'query' => $optimizedQuery['query'],
|
|
'params' => $optimizedQuery['params'],
|
|
'strategy' => $strategy,
|
|
'performance' => $optimizedProfile,
|
|
'improvement' => $this->calculateQueryImprovement($originalProfile, $optimizedProfile)
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
$optimizedResults[$strategy] = [
|
|
'query' => $optimizedQuery['query'],
|
|
'strategy' => $strategy,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
$result['optimized_queries'] = $optimizedResults;
|
|
|
|
// Find best optimization
|
|
$bestImprovement = 0;
|
|
$bestStrategy = '';
|
|
|
|
foreach ($optimizedResults as $strategy => $optimized) {
|
|
if (isset($optimized['improvement']['performance_improvement'])) {
|
|
$improvement = $optimized['improvement']['performance_improvement'];
|
|
if ($improvement > $bestImprovement) {
|
|
$bestImprovement = $improvement;
|
|
$bestStrategy = $strategy;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($bestStrategy !== '') {
|
|
$result['best_optimization'] = $optimizedResults[$bestStrategy];
|
|
$result['improvement_percentage'] = $bestImprovement;
|
|
}
|
|
|
|
// Store optimization history
|
|
$this->optimizationHistory[] = $result;
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Test connection pool performance.
|
|
*/
|
|
public function testConnectionPoolPerformance(array $options = []): array
|
|
{
|
|
$testConfig = array_merge($this->config['connection_pool_testing'] ?? [], $options);
|
|
|
|
$result = [
|
|
'pool_size' => $testConfig['pool_size'] ?? 10,
|
|
'concurrent_connections' => $testConfig['concurrent_connections'] ?? 50,
|
|
'operations_per_connection' => $testConfig['operations_per_connection'] ?? 20,
|
|
'with_pool' => [],
|
|
'without_pool' => [],
|
|
'performance_gain' => [],
|
|
'timestamp' => microtime(true)
|
|
];
|
|
|
|
// Test without connection pool
|
|
$withoutPoolResult = $this->testConnectionsDirectly($testConfig);
|
|
$result['without_pool'] = $withoutPoolResult;
|
|
|
|
// Test with connection pool
|
|
$withPoolResult = $this->testConnectionsWithPool($testConfig);
|
|
$result['with_pool'] = $withPoolResult;
|
|
|
|
// Calculate performance gain
|
|
$result['performance_gain'] = $this->calculateConnectionPoolGain($withoutPoolResult, $withPoolResult);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Analyze query patterns.
|
|
*/
|
|
public function analyzeQueryPatterns(array $queries, array $options = []): array
|
|
{
|
|
$testConfig = array_merge($this->config['pattern_analysis'] ?? [], $options);
|
|
|
|
$result = [
|
|
'total_queries' => count($queries),
|
|
'query_types' => [],
|
|
'table_access_patterns' => [],
|
|
'frequent_queries' => [],
|
|
'slow_queries' => [],
|
|
'optimization_opportunities' => [],
|
|
'recommendations' => [],
|
|
'timestamp' => microtime(true)
|
|
];
|
|
|
|
$queryTypes = [];
|
|
$tableAccess = [];
|
|
$queryProfiles = [];
|
|
|
|
foreach ($queries as $index => $query) {
|
|
$profile = $this->profileQuery($query['sql'], $query['params'] ?? [], ['executions' => 10]);
|
|
$queryProfiles[] = $profile;
|
|
|
|
// Analyze query type
|
|
$queryType = $this->getQueryType($query['sql']);
|
|
$queryTypes[$queryType] = ($queryTypes[$queryType] ?? 0) + 1;
|
|
|
|
// Analyze table access
|
|
$tables = $this->extractTables($query['sql']);
|
|
foreach ($tables as $table) {
|
|
if (!isset($tableAccess[$table])) {
|
|
$tableAccess[$table] = ['access_count' => 0, 'total_time' => 0, 'queries' => []];
|
|
}
|
|
$tableAccess[$table]['access_count']++;
|
|
$tableAccess[$table]['total_time'] += $profile['statistics']['average'];
|
|
$tableAccess[$table]['queries'][] = $index;
|
|
}
|
|
}
|
|
|
|
$result['query_types'] = $queryTypes;
|
|
$result['table_access_patterns'] = $tableAccess;
|
|
|
|
// Identify frequent and slow queries
|
|
$result['frequent_queries'] = $this->identifyFrequentQueries($queries, $tableAccess);
|
|
$result['slow_queries'] = $this->identifySlowQueries($queries, $queryProfiles);
|
|
|
|
// Find optimization opportunities
|
|
$result['optimization_opportunities'] = $this->findOptimizationOpportunities($queries, $queryProfiles, $tableAccess);
|
|
|
|
// Generate recommendations
|
|
$result['recommendations'] = $this->generatePatternRecommendations($result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Test database performance under load.
|
|
*/
|
|
public function testDatabaseLoad(array $queries, array $options = []): array
|
|
{
|
|
$testConfig = array_merge($this->config['load_testing'] ?? [], $options);
|
|
|
|
$result = [
|
|
'queries' => $queries,
|
|
'concurrent_users' => $testConfig['concurrent_users'] ?? 20,
|
|
'duration' => $testConfig['duration'] ?? 60, // seconds
|
|
'ramp_up_time' => $testConfig['ramp_up_time'] ?? 10,
|
|
'performance_metrics' => [],
|
|
'resource_usage' => [],
|
|
'bottlenecks' => [],
|
|
'recommendations' => [],
|
|
'timestamp' => microtime(true)
|
|
];
|
|
|
|
// Start load test
|
|
$startTime = microtime(true);
|
|
$endTime = $startTime + $result['duration'];
|
|
|
|
$performanceData = [];
|
|
$resourceData = [];
|
|
|
|
$currentTime = $startTime;
|
|
|
|
while ($currentTime < $endTime) {
|
|
$batchStart = microtime(true);
|
|
|
|
// Calculate current concurrent users based on ramp-up
|
|
if ($currentTime - $startTime < $result['ramp_up_time']) {
|
|
$currentUsers = (int) (($currentTime - $startTime) / $result['ramp_up_time'] * $result['concurrent_users']);
|
|
} else {
|
|
$currentUsers = $result['concurrent_users'];
|
|
}
|
|
|
|
// Execute queries concurrently
|
|
$batchResults = $this->executeConcurrentQueries($queries, $currentUsers);
|
|
|
|
$batchEnd = microtime(true);
|
|
$batchDuration = $batchEnd - $batchStart;
|
|
|
|
$performanceData[] = [
|
|
'timestamp' => $currentTime,
|
|
'concurrent_users' => $currentUsers,
|
|
'queries_executed' => count($batchResults),
|
|
'average_response_time' => $this->calculateAverageResponseTime($batchResults),
|
|
'throughput' => count($batchResults) / $batchDuration,
|
|
'error_rate' => $this->calculateErrorRate($batchResults)
|
|
];
|
|
|
|
// Collect resource usage
|
|
$resourceData[] = [
|
|
'timestamp' => $currentTime,
|
|
'cpu_usage' => $this->getCpuUsage(),
|
|
'memory_usage' => memory_get_usage(true),
|
|
'database_connections' => $this->getActiveConnections()
|
|
];
|
|
|
|
$currentTime = microtime(true);
|
|
|
|
// Small delay between batches
|
|
usleep(100000); // 100ms
|
|
}
|
|
|
|
$result['performance_metrics'] = $performanceData;
|
|
$result['resource_usage'] = $resourceData;
|
|
|
|
// Analyze bottlenecks
|
|
$result['bottlenecks'] = $this->analyzeDatabaseBottlenecks($performanceData, $resourceData);
|
|
|
|
// Generate recommendations
|
|
$result['recommendations'] = $this->generateLoadTestRecommendations($result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Generate database optimization report.
|
|
*/
|
|
public function getOptimizationReport(array $results, string $format = 'html'): string
|
|
{
|
|
return $this->reporter->generate($results, $format);
|
|
}
|
|
|
|
/**
|
|
* Get database performance statistics.
|
|
*/
|
|
public function getDatabaseStatistics(): array
|
|
{
|
|
return [
|
|
'queries_profiled' => count($this->queryResults),
|
|
'optimizations_performed' => count($this->optimizationHistory),
|
|
'average_query_time' => $this->calculateAverageQueryTime(),
|
|
'slow_queries_count' => $this->countSlowQueries(),
|
|
'connection_pool_stats' => $this->connectionPool->getStatistics()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Clear optimization results.
|
|
*/
|
|
public function clearResults(): void
|
|
{
|
|
$this->queryResults = [];
|
|
$this->optimizationHistory = [];
|
|
}
|
|
|
|
/**
|
|
* Execute database query.
|
|
*/
|
|
protected function executeQuery(string $query, array $params = []): array
|
|
{
|
|
// This would be implemented based on your database layer
|
|
// For now, simulate query execution
|
|
usleep(rand(1000, 5000)); // Simulate 1-5ms execution time
|
|
|
|
return [
|
|
'id' => rand(1, 1000),
|
|
'data' => 'sample_data',
|
|
'execution_time' => rand(1000, 5000) / 1000 // milliseconds
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get query execution plan.
|
|
*/
|
|
protected function getQueryPlan(string $query, array $params = []): array
|
|
{
|
|
// This would execute EXPLAIN or similar command
|
|
// For now, return mock plan
|
|
return [
|
|
'type' => 'ALL',
|
|
'table' => 'users',
|
|
'rows' => 1000,
|
|
'filtered' => 100,
|
|
'key' => null,
|
|
'possible_keys' => ['PRIMARY', 'email_index']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get table indexes.
|
|
*/
|
|
protected function getTableIndexes(string $tableName): array
|
|
{
|
|
// This would query the database for index information
|
|
// For now, return mock indexes
|
|
return [
|
|
[
|
|
'name' => 'PRIMARY',
|
|
'columns' => ['id'],
|
|
'type' => 'BTREE',
|
|
'unique' => true,
|
|
'cardinality' => 1000
|
|
],
|
|
[
|
|
'name' => 'email_index',
|
|
'columns' => ['email'],
|
|
'type' => 'BTREE',
|
|
'unique' => true,
|
|
'cardinality' => 1000
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get index usage statistics.
|
|
*/
|
|
protected function getIndexUsageStats(string $tableName): array
|
|
{
|
|
// This would query index usage statistics
|
|
// For now, return mock stats
|
|
return [
|
|
'PRIMARY' => [
|
|
'usage_count' => 5000,
|
|
'last_used' => date('Y-m-d H:i:s'),
|
|
'efficiency' => 0.95
|
|
],
|
|
'email_index' => [
|
|
'usage_count' => 100,
|
|
'last_used' => date('Y-m-d H:i:s', strtotime('-1 day')),
|
|
'efficiency' => 0.80
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Find unused indexes.
|
|
*/
|
|
protected function findUnusedIndexes(array $indexes, array $usageStats): array
|
|
{
|
|
$unused = [];
|
|
|
|
foreach ($indexes as $index) {
|
|
$indexName = $index['name'];
|
|
|
|
if (!isset($usageStats[$indexName]) ||
|
|
$usageStats[$indexName]['usage_count'] < 10) {
|
|
$unused[] = $index;
|
|
}
|
|
}
|
|
|
|
return $unused;
|
|
}
|
|
|
|
/**
|
|
* Find missing indexes.
|
|
*/
|
|
protected function findMissingIndexes(string $tableName): array
|
|
{
|
|
// This would analyze query patterns and suggest missing indexes
|
|
// For now, return mock suggestions
|
|
return [
|
|
[
|
|
'columns' => ['created_at', 'status'],
|
|
'reason' => 'Frequent filtering on these columns',
|
|
'estimated_improvement' => '50%'
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Find duplicate indexes.
|
|
*/
|
|
protected function findDuplicateIndexes(array $indexes): array
|
|
{
|
|
$duplicates = [];
|
|
|
|
for ($i = 0; $i < count($indexes); $i++) {
|
|
for ($j = $i + 1; $j < count($indexes); $j++) {
|
|
if ($this->areIndexesDuplicate($indexes[$i], $indexes[$j])) {
|
|
$duplicates[] = [
|
|
'index1' => $indexes[$i]['name'],
|
|
'index2' => $indexes[$j]['name'],
|
|
'columns' => $indexes[$i]['columns']
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $duplicates;
|
|
}
|
|
|
|
/**
|
|
* Check if indexes are duplicate.
|
|
*/
|
|
protected function areIndexesDuplicate(array $index1, array $index2): bool
|
|
{
|
|
// Simple check - if one index is a prefix of another
|
|
$cols1 = $index1['columns'];
|
|
$cols2 = $index2['columns'];
|
|
|
|
if (count($cols1) <= count($cols2)) {
|
|
for ($i = 0; $i < count($cols1); $i++) {
|
|
if ($cols1[$i] !== $cols2[$i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Analyze query plans.
|
|
*/
|
|
protected function analyzeQueryPlans(array $plans): array
|
|
{
|
|
if (empty($plans)) {
|
|
return [];
|
|
}
|
|
|
|
$analysis = [
|
|
'most_common_type' => '',
|
|
'average_rows_examined' => 0,
|
|
'index_usage_rate' => 0,
|
|
'full_table_scans' => 0
|
|
];
|
|
|
|
$types = [];
|
|
$totalRows = 0;
|
|
$indexUsed = 0;
|
|
$fullScans = 0;
|
|
|
|
foreach ($plans as $plan) {
|
|
$types[$plan['type']] = ($types[$plan['type']] ?? 0) + 1;
|
|
$totalRows += $plan['rows'] ?? 0;
|
|
|
|
if ($plan['key'] !== null) {
|
|
$indexUsed++;
|
|
}
|
|
|
|
if ($plan['type'] === 'ALL') {
|
|
$fullScans++;
|
|
}
|
|
}
|
|
|
|
$analysis['most_common_type'] = array_keys($types, max($values))[0] ?? 'UNKNOWN';
|
|
$analysis['average_rows_examined'] = $totalRows / count($plans);
|
|
$analysis['index_usage_rate'] = ($indexUsed / count($plans)) * 100;
|
|
$analysis['full_table_scans'] = $fullScans;
|
|
|
|
return $analysis;
|
|
}
|
|
|
|
/**
|
|
* Calculate query statistics.
|
|
*/
|
|
protected function calculateQueryStatistics(array $executionTimes): array
|
|
{
|
|
if (empty($executionTimes)) {
|
|
return [
|
|
'count' => 0,
|
|
'min' => 0,
|
|
'max' => 0,
|
|
'average' => 0,
|
|
'median' => 0,
|
|
'std_dev' => 0
|
|
];
|
|
}
|
|
|
|
sort($executionTimes);
|
|
$count = count($executionTimes);
|
|
$sum = array_sum($executionTimes);
|
|
|
|
$mean = $sum / $count;
|
|
$median = $count % 2 === 0 ?
|
|
($executionTimes[$count / 2 - 1] + $executionTimes[$count / 2]) / 2 :
|
|
$executionTimes[floor($count / 2)];
|
|
|
|
// Calculate standard deviation
|
|
$variance = 0;
|
|
foreach ($executionTimes as $time) {
|
|
$variance += pow($time - $mean, 2);
|
|
}
|
|
$stdDev = sqrt($variance / $count);
|
|
|
|
return [
|
|
'count' => $count,
|
|
'min' => min($executionTimes),
|
|
'max' => max($executionTimes),
|
|
'average' => $mean,
|
|
'median' => $median,
|
|
'std_dev' => $stdDev
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Detect performance issues.
|
|
*/
|
|
protected function detectPerformanceIssues(array $result): array
|
|
{
|
|
$issues = [];
|
|
|
|
$stats = $result['statistics'];
|
|
$planAnalysis = $result['query_plan'];
|
|
|
|
// Slow query
|
|
if ($stats['average'] > 1000) { // > 1 second
|
|
$issues[] = [
|
|
'type' => 'slow_query',
|
|
'severity' => 'high',
|
|
'description' => "Average execution time is {$stats['average']}ms",
|
|
'recommendation' => 'Optimize query or add appropriate indexes'
|
|
];
|
|
}
|
|
|
|
// High variance
|
|
if ($stats['std_dev'] > $stats['average'] * 0.5) {
|
|
$issues[] = [
|
|
'type' => 'inconsistent_performance',
|
|
'severity' => 'medium',
|
|
'description' => 'Query execution time is inconsistent',
|
|
'recommendation' => 'Investigate parameter sniffing or caching issues'
|
|
];
|
|
}
|
|
|
|
// Full table scans
|
|
if (isset($planAnalysis['full_table_scans']) && $planAnalysis['full_table_scans'] > 0) {
|
|
$issues[] = [
|
|
'type' => 'full_table_scan',
|
|
'severity' => 'high',
|
|
'description' => 'Query performs full table scans',
|
|
'recommendation' => 'Add appropriate indexes to avoid full scans'
|
|
];
|
|
}
|
|
|
|
// Low index usage
|
|
if (isset($planAnalysis['index_usage_rate']) && $planAnalysis['index_usage_rate'] < 50) {
|
|
$issues[] = [
|
|
'type' => 'low_index_usage',
|
|
'severity' => 'medium',
|
|
'description' => 'Low index usage rate',
|
|
'recommendation' => 'Review query structure and available indexes'
|
|
];
|
|
}
|
|
|
|
return $issues;
|
|
}
|
|
|
|
/**
|
|
* Generate query optimization suggestions.
|
|
*/
|
|
protected function generateQueryOptimizationSuggestions(array $result): array
|
|
{
|
|
$suggestions = [];
|
|
|
|
$issues = $result['performance_issues'];
|
|
|
|
foreach ($issues as $issue) {
|
|
$suggestions[] = $issue['recommendation'];
|
|
}
|
|
|
|
// Additional suggestions based on query analysis
|
|
if (strpos(strtolower($result['query']), 'select *') !== false) {
|
|
$suggestions[] = 'Avoid SELECT *, specify only needed columns';
|
|
}
|
|
|
|
if (strpos(strtolower($result['query']), 'order by') !== false &&
|
|
!isset($result['query_plan']['key'])) {
|
|
$suggestions[] = 'Consider adding index for ORDER BY clause';
|
|
}
|
|
|
|
return array_unique($suggestions);
|
|
}
|
|
|
|
/**
|
|
* Generate optimization strategies.
|
|
*/
|
|
protected function generateOptimizationStrategies(string $query, array $config): array
|
|
{
|
|
$strategies = [];
|
|
|
|
// Strategy 1: Add LIMIT clause
|
|
if (stripos($query, 'LIMIT') === false && stripos($query, 'SELECT') === 0) {
|
|
$strategies['add_limit'] = [
|
|
'query' => $query . ' LIMIT 100',
|
|
'params' => []
|
|
];
|
|
}
|
|
|
|
// Strategy 2: Optimize SELECT columns
|
|
if (stripos($query, 'SELECT *') !== false) {
|
|
$optimizedQuery = str_replace('SELECT *', 'SELECT id, name, email', $query);
|
|
$strategies['specific_columns'] = [
|
|
'query' => $optimizedQuery,
|
|
'params' => []
|
|
];
|
|
}
|
|
|
|
// Strategy 3: Add index hints (MySQL specific)
|
|
if (stripos($query, 'SELECT') === 0) {
|
|
$strategies['index_hint'] = [
|
|
'query' => preg_replace('/SELECT/i', 'SELECT /*+ INDEX(users email_index) */', $query, 1),
|
|
'params' => []
|
|
];
|
|
}
|
|
|
|
return $strategies;
|
|
}
|
|
|
|
/**
|
|
* Calculate query improvement.
|
|
*/
|
|
protected function calculateQueryImprovement(array $original, array $optimized): array
|
|
{
|
|
$originalTime = $original['statistics']['average'];
|
|
$optimizedTime = $optimized['statistics']['average'];
|
|
|
|
$improvement = 0;
|
|
if ($originalTime > 0) {
|
|
$improvement = (($originalTime - $optimizedTime) / $originalTime) * 100;
|
|
}
|
|
|
|
return [
|
|
'performance_improvement' => $improvement,
|
|
'original_time' => $originalTime,
|
|
'optimized_time' => $optimizedTime,
|
|
'time_saved' => $originalTime - $optimizedTime
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Test connections directly.
|
|
*/
|
|
protected function testConnectionsDirectly(array $config): array
|
|
{
|
|
$startTime = microtime(true);
|
|
$startMemory = memory_get_usage(true);
|
|
|
|
$connectionTimes = [];
|
|
$totalOperations = 0;
|
|
|
|
for ($i = 0; $i < $config['concurrent_connections']; $i++) {
|
|
$connStart = microtime(true);
|
|
|
|
// Simulate direct connection
|
|
$connection = $this->createDirectConnection();
|
|
|
|
for ($j = 0; $j < $config['operations_per_connection']; $j++) {
|
|
$this->executeQuery('SELECT 1');
|
|
$totalOperations++;
|
|
}
|
|
|
|
$this->closeConnection($connection);
|
|
|
|
$connEnd = microtime(true);
|
|
$connectionTimes[] = ($connEnd - $connStart) * 1000;
|
|
}
|
|
|
|
$endTime = microtime(true);
|
|
$endMemory = memory_get_usage(true);
|
|
|
|
return [
|
|
'duration' => $endTime - $startTime,
|
|
'memory_used' => $endMemory - $startMemory,
|
|
'total_operations' => $totalOperations,
|
|
'average_connection_time' => array_sum($connectionTimes) / count($connectionTimes),
|
|
'throughput' => $totalOperations / ($endTime - $startTime)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Test connections with pool.
|
|
*/
|
|
protected function testConnectionsWithPool(array $config): array
|
|
{
|
|
$this->connectionPool->initialize($config['pool_size']);
|
|
|
|
$startTime = microtime(true);
|
|
$startMemory = memory_get_usage(true);
|
|
|
|
$connectionTimes = [];
|
|
$totalOperations = 0;
|
|
|
|
for ($i = 0; $i < $config['concurrent_connections']; $i++) {
|
|
$connStart = microtime(true);
|
|
|
|
// Get connection from pool
|
|
$connection = $this->connectionPool->getConnection();
|
|
|
|
for ($j = 0; $j < $config['operations_per_connection']; $j++) {
|
|
$this->executeQuery('SELECT 1');
|
|
$totalOperations++;
|
|
}
|
|
|
|
// Return connection to pool
|
|
$this->connectionPool->returnConnection($connection);
|
|
|
|
$connEnd = microtime(true);
|
|
$connectionTimes[] = ($connEnd - $connStart) * 1000;
|
|
}
|
|
|
|
$endTime = microtime(true);
|
|
$endMemory = memory_get_usage(true);
|
|
|
|
$this->connectionPool->cleanup();
|
|
|
|
return [
|
|
'duration' => $endTime - $startTime,
|
|
'memory_used' => $endMemory - $startMemory,
|
|
'total_operations' => $totalOperations,
|
|
'average_connection_time' => array_sum($connectionTimes) / count($connectionTimes),
|
|
'throughput' => $totalOperations / ($endTime - $startTime),
|
|
'pool_efficiency' => $this->connectionPool->getEfficiency()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate connection pool gain.
|
|
*/
|
|
protected function calculateConnectionPoolGain(array $withoutPool, array $withPool): array
|
|
{
|
|
$timeImprovement = (($withoutPool['duration'] - $withPool['duration']) / $withoutPool['duration']) * 100;
|
|
$throughputImprovement = (($withPool['throughput'] - $withoutPool['throughput']) / $withoutPool['throughput']) * 100;
|
|
|
|
return [
|
|
'time_improvement' => $timeImprovement,
|
|
'throughput_improvement' => $throughputImprovement,
|
|
'memory_savings' => (($withoutPool['memory_used'] - $withPool['memory_used']) / $withoutPool['memory_used']) * 100,
|
|
'connection_time_improvement' => (($withoutPool['average_connection_time'] - $withPool['average_connection_time']) / $withoutPool['average_connection_time']) * 100
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get query type.
|
|
*/
|
|
protected function getQueryType(string $query): string
|
|
{
|
|
$query = strtoupper(trim($query));
|
|
|
|
if (strpos($query, 'SELECT') === 0) return 'SELECT';
|
|
if (strpos($query, 'INSERT') === 0) return 'INSERT';
|
|
if (strpos($query, 'UPDATE') === 0) return 'UPDATE';
|
|
if (strpos($query, 'DELETE') === 0) return 'DELETE';
|
|
if (strpos($query, 'CREATE') === 0) return 'CREATE';
|
|
if (strpos($query, 'DROP') === 0) return 'DROP';
|
|
if (strpos($query, 'ALTER') === 0) return 'ALTER';
|
|
|
|
return 'OTHER';
|
|
}
|
|
|
|
/**
|
|
* Extract tables from query.
|
|
*/
|
|
protected function extractTables(string $query): array
|
|
{
|
|
// Simple table extraction - would be more sophisticated in real implementation
|
|
preg_match_all('/FROM\s+(\w+)|JOIN\s+(\w+)|UPDATE\s+(\w+)|INSERT\s+INTO\s+(\w+)/i', $query, $matches);
|
|
|
|
$tables = [];
|
|
foreach ($matches as $match) {
|
|
foreach ($match as $table) {
|
|
if ($table && !in_array($table, $tables)) {
|
|
$tables[] = $table;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* Identify frequent queries.
|
|
*/
|
|
protected function identifyFrequentQueries(array $queries, array $tableAccess): array
|
|
{
|
|
$frequent = [];
|
|
|
|
foreach ($tableAccess as $table => $access) {
|
|
if ($access['access_count'] > 10) {
|
|
foreach ($access['queries'] as $queryIndex) {
|
|
$frequent[] = [
|
|
'query_index' => $queryIndex,
|
|
'table' => $table,
|
|
'access_count' => $access['access_count'],
|
|
'total_time' => $access['total_time']
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $frequent;
|
|
}
|
|
|
|
/**
|
|
* Identify slow queries.
|
|
*/
|
|
protected function identifySlowQueries(array $queries, array $profiles): array
|
|
{
|
|
$slow = [];
|
|
|
|
foreach ($profiles as $index => $profile) {
|
|
if ($profile['statistics']['average'] > 500) { // > 500ms
|
|
$slow[] = [
|
|
'query_index' => $index,
|
|
'average_time' => $profile['statistics']['average'],
|
|
'max_time' => $profile['statistics']['max'],
|
|
'execution_count' => $profile['statistics']['count']
|
|
];
|
|
}
|
|
}
|
|
|
|
return $slow;
|
|
}
|
|
|
|
/**
|
|
* Find optimization opportunities.
|
|
*/
|
|
protected function findOptimizationOpportunities(array $queries, array $profiles, array $tableAccess): array
|
|
{
|
|
$opportunities = [];
|
|
|
|
// Queries without indexes
|
|
foreach ($profiles as $index => $profile) {
|
|
if (isset($profile['query_plan']['index_usage_rate']) &&
|
|
$profile['query_plan']['index_usage_rate'] < 50) {
|
|
$opportunities[] = [
|
|
'type' => 'missing_index',
|
|
'query_index' => $index,
|
|
'description' => 'Query has low index usage rate',
|
|
'potential_improvement' => '30-50%'
|
|
];
|
|
}
|
|
}
|
|
|
|
// Frequently accessed tables without proper indexes
|
|
foreach ($tableAccess as $table => $access) {
|
|
if ($access['access_count'] > 100 && $access['total_time'] > 10000) {
|
|
$opportunities[] = [
|
|
'type' => 'table_optimization',
|
|
'table' => $table,
|
|
'description' => 'Frequently accessed table with high total time',
|
|
'potential_improvement' => '20-40%'
|
|
];
|
|
}
|
|
}
|
|
|
|
return $opportunities;
|
|
}
|
|
|
|
/**
|
|
* Generate pattern recommendations.
|
|
*/
|
|
protected function generatePatternRecommendations(array $result): array
|
|
{
|
|
$recommendations = [];
|
|
|
|
if (!empty($result['slow_queries'])) {
|
|
$recommendations[] = 'Optimize slow queries by adding appropriate indexes';
|
|
}
|
|
|
|
if (!empty($result['optimization_opportunities'])) {
|
|
$recommendations[] = 'Review optimization opportunities for performance gains';
|
|
}
|
|
|
|
// Check query type distribution
|
|
if (isset($result['query_types']['SELECT']) && $result['query_types']['SELECT'] > 50) {
|
|
$recommendations[] = 'Consider read replicas for SELECT-heavy workload';
|
|
}
|
|
|
|
return $recommendations;
|
|
}
|
|
|
|
/**
|
|
* Execute concurrent queries.
|
|
*/
|
|
protected function executeConcurrentQueries(array $queries, int $concurrentUsers): array
|
|
{
|
|
$results = [];
|
|
|
|
for ($i = 0; $i < $concurrentUsers; $i++) {
|
|
$query = $queries[$i % count($queries)];
|
|
|
|
try {
|
|
$start = microtime(true);
|
|
$this->executeQuery($query['sql'], $query['params'] ?? []);
|
|
$end = microtime(true);
|
|
|
|
$results[] = [
|
|
'success' => true,
|
|
'response_time' => ($end - $start) * 1000
|
|
];
|
|
} catch (\Exception $e) {
|
|
$results[] = [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Calculate average response time.
|
|
*/
|
|
protected function calculateAverageResponseTime(array $results): float
|
|
{
|
|
$times = array_filter($results, fn($r) => isset($r['response_time']));
|
|
$times = array_column($times, 'response_time');
|
|
|
|
return empty($times) ? 0 : array_sum($times) / count($times);
|
|
}
|
|
|
|
/**
|
|
* Calculate error rate.
|
|
*/
|
|
protected function calculateErrorRate(array $results): float
|
|
{
|
|
$errors = array_filter($results, fn($r) => !($r['success'] ?? true));
|
|
|
|
return empty($results) ? 0 : (count($errors) / count($results)) * 100;
|
|
}
|
|
|
|
/**
|
|
* Analyze database bottlenecks.
|
|
*/
|
|
protected function analyzeDatabaseBottlenecks(array $performanceData, array $resourceData): array
|
|
{
|
|
$bottlenecks = [];
|
|
|
|
// Check response time trends
|
|
$responseTimes = array_column($performanceData, 'average_response_time');
|
|
if (!empty($responseTimes) && max($responseTimes) > 1000) {
|
|
$bottlenecks[] = [
|
|
'type' => 'response_time',
|
|
'severity' => 'high',
|
|
'description' => 'Response times exceeding 1 second',
|
|
'recommendation' => 'Optimize queries or increase database resources'
|
|
];
|
|
}
|
|
|
|
// Check CPU usage
|
|
$cpuUsage = array_column($resourceData, 'cpu_usage');
|
|
if (!empty($cpuUsage) && max($cpuUsage) > 80) {
|
|
$bottlenecks[] = [
|
|
'type' => 'cpu',
|
|
'severity' => 'medium',
|
|
'description' => 'High CPU usage detected',
|
|
'recommendation' => 'Consider query optimization or scaling up'
|
|
];
|
|
}
|
|
|
|
return $bottlenecks;
|
|
}
|
|
|
|
/**
|
|
* Generate load test recommendations.
|
|
*/
|
|
protected function generateLoadTestRecommendations(array $result): array
|
|
{
|
|
$recommendations = [];
|
|
|
|
if (!empty($result['bottlenecks'])) {
|
|
foreach ($result['bottlenecks'] as $bottleneck) {
|
|
$recommendations[] = $bottleneck['recommendation'];
|
|
}
|
|
}
|
|
|
|
// Check throughput
|
|
$throughputData = array_column($result['performance_metrics'], 'throughput');
|
|
if (!empty($throughputData)) {
|
|
$avgThroughput = array_sum($throughputData) / count($throughputData);
|
|
if ($avgThroughput < 100) { // < 100 queries/second
|
|
$recommendations[] = 'Consider database optimization or horizontal scaling';
|
|
}
|
|
}
|
|
|
|
return $recommendations;
|
|
}
|
|
|
|
/**
|
|
* Generate index recommendations.
|
|
*/
|
|
protected function generateIndexRecommendations(array $result): array
|
|
{
|
|
$recommendations = [];
|
|
|
|
if (!empty($result['unused_indexes'])) {
|
|
$recommendations[] = 'Consider removing unused indexes to improve write performance';
|
|
}
|
|
|
|
if (!empty($result['missing_indexes'])) {
|
|
$recommendations[] = 'Add missing indexes to improve query performance';
|
|
}
|
|
|
|
if (!empty($result['duplicate_indexes'])) {
|
|
$recommendations[] = 'Remove duplicate indexes to save storage and improve performance';
|
|
}
|
|
|
|
return $recommendations;
|
|
}
|
|
|
|
// Mock methods for database operations
|
|
protected function createDirectConnection()
|
|
{
|
|
return new \stdClass(); // Mock connection
|
|
}
|
|
|
|
protected function closeConnection($connection): void
|
|
{
|
|
// Mock connection cleanup
|
|
}
|
|
|
|
protected function getCpuUsage(): float
|
|
{
|
|
return rand(20, 90); // Mock CPU usage
|
|
}
|
|
|
|
protected function getActiveConnections(): int
|
|
{
|
|
return rand(5, 50); // Mock active connections
|
|
}
|
|
|
|
protected function calculateAverageQueryTime(): float
|
|
{
|
|
if (empty($this->queryResults)) {
|
|
return 0;
|
|
}
|
|
|
|
$total = 0;
|
|
$count = 0;
|
|
|
|
foreach ($this->queryResults as $result) {
|
|
$total += $result['statistics']['average'];
|
|
$count++;
|
|
}
|
|
|
|
return $count > 0 ? $total / $count : 0;
|
|
}
|
|
|
|
protected function countSlowQueries(): int
|
|
{
|
|
$count = 0;
|
|
|
|
foreach ($this->queryResults as $result) {
|
|
if ($result['statistics']['average'] > 1000) {
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Get default configuration.
|
|
*/
|
|
protected function getDefaultConfig(): array
|
|
{
|
|
return [
|
|
'query_profiling' => [
|
|
'executions' => 100,
|
|
'warmup_executions' => 10
|
|
],
|
|
'index_analysis' => [
|
|
'analyze_queries' => true
|
|
],
|
|
'query_optimization' => [
|
|
'strategies' => ['limit', 'columns', 'hints']
|
|
],
|
|
'connection_pool_testing' => [
|
|
'pool_size' => 10,
|
|
'concurrent_connections' => 50,
|
|
'operations_per_connection' => 20
|
|
],
|
|
'pattern_analysis' => [],
|
|
'load_testing' => [
|
|
'concurrent_users' => 20,
|
|
'duration' => 60,
|
|
'ramp_up_time' => 10
|
|
],
|
|
'query_profiler' => [],
|
|
'index_analyzer' => [],
|
|
'query_optimizer' => [],
|
|
'connection_pool' => [],
|
|
'reporter' => []
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get configuration.
|
|
*/
|
|
public function getConfig(): array
|
|
{
|
|
return $this->config;
|
|
}
|
|
|
|
/**
|
|
* Set configuration.
|
|
*/
|
|
public function setConfig(array $config): void
|
|
{
|
|
$this->config = array_merge($this->config, $config);
|
|
}
|
|
|
|
/**
|
|
* Create database optimizer instance.
|
|
*/
|
|
public static function create(array $config = []): self
|
|
{
|
|
return new self($config);
|
|
}
|
|
|
|
/**
|
|
* Create for development.
|
|
*/
|
|
public static function forDevelopment(): self
|
|
{
|
|
return new self([
|
|
'query_profiling' => [
|
|
'executions' => 50,
|
|
'warmup_executions' => 5
|
|
],
|
|
'load_testing' => [
|
|
'concurrent_users' => 10,
|
|
'duration' => 30
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create for production.
|
|
*/
|
|
public static function forProduction(): self
|
|
{
|
|
return new self([
|
|
'query_profiling' => [
|
|
'executions' => 200,
|
|
'warmup_executions' => 20
|
|
],
|
|
'load_testing' => [
|
|
'concurrent_users' => 100,
|
|
'duration' => 300
|
|
]
|
|
]);
|
|
}
|
|
}
|