| name | query-performance-analysis |
| description | Detect N+1 queries, analyze slow queries with EXPLAIN, identify missing indexes, and ensure safe online index migrations for MySQL/MariaDB. Use when optimizing query performance, preventing performance regressions, or debugging slow endpoints. Complements database-migrations skill which covers index creation syntax. |
Query Performance Analysis & Index Management
Context (Input)
Use this skill when:
- New or modified endpoints are slow
- Profiler shows many database queries for single operation
- Need to detect N+1 query problems
- Query execution time is high
- Slow query warnings in MySQL logs
- Performance regression after code changes
- Planning safe index migrations for production
- Need to verify index effectiveness
Task (Function)
Analyze query performance, detect N+1 issues, identify missing indexes, and create safe online index migrations with verification steps.
Success Criteria:
- N+1 queries detected and fixed
- Slow queries identified with EXPLAIN analysis
- Missing indexes detected and added
- Query performance meets acceptable thresholds (<100ms for reads, <500ms for writes)
- Index migrations are safe for production (minimal downtime)
- Performance regression tests added
TL;DR - Quick Performance Checklist
Before Merging Code:
When Adding Indexes:
Quick Start: 5-Step Performance Analysis
Step 1: Enable MySQL Slow Query Log
docker compose exec database mariadb -u root -proot
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
Step 2: Run Your Endpoint
curl https://localhost/api/users
Step 3: Analyze Query Patterns
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
Step 4: Check for Performance Issues
N+1 Problem Symptoms:
- Same query executed many times
- Query count grows with data size
- Queries inside
foreach loops
Slow Query Symptoms:
- Execution time >100ms
- EXPLAIN shows
type: ALL (full table scan)
- High
rows vs actual returned rows
Step 5: Disable Slow Query Log (Production)
SET GLOBAL slow_query_log = 'OFF';
Common Performance Issues
Issue 1: N+1 Queries
Detection: 100+ queries for 100 records
Fix: Use eager loading with Doctrine
$users = $repository->findAll();
foreach ($users as $user) {
$token = $user->getConfirmationToken();
}
$qb = $this->createQueryBuilder('u');
$qb->leftJoin('u.confirmationToken', 't')
->addSelect('t');
$users = $qb->getQuery()->getResult();
See: examples/n-plus-one-detection.md for complete guide
Issue 2: Slow Queries (No Index)
Detection: EXPLAIN shows type: ALL, execution time >100ms
Fix: Add index
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
Add index in Doctrine migration:
public function up(Schema $schema): void
{
$this->addSql('CREATE INDEX idx_users_email ON users (email)');
}
Or in XML mapping:
<indexes>
<index name="idx_email" columns="email"/>
</indexes>
See: examples/slow-query-analysis.md for EXPLAIN interpretation
Issue 3: Missing Indexes on Filtered Fields
Detection: Queries filter/sort on fields without indexes
Common patterns needing indexes:
- WHERE clause fields:
email = ?, status = ?
- ORDER BY fields:
created_at DESC
- Composite filters:
status = ? AND type = ?
- Foreign keys:
user_id, token_id
Cursor pagination + UUID index strategy (this repo):
This service uses cursor pagination on id (UUID). For pagination with filters, use a composite index:
<indexes>
<index name="idx_status_id" columns="status,id"/>
</indexes>
See: reference/index-strategies.md for index selection guide
Performance Thresholds
| Operation | Target | Max Acceptable |
|---|
| GET single | <50ms | 100ms |
| GET collection (100 items) | <200ms | 500ms |
| POST/PATCH/PUT | <100ms | 300ms |
| Query count per endpoint | <5 | 10 |
See: reference/performance-thresholds.md for complete thresholds
Safe Index Migrations
MariaDB 11.4+ supports online DDL:
- Most index operations are non-blocking with
ALGORITHM=INPLACE
- InnoDB allows concurrent reads and writes during index builds
- Note: Large tables may still cause brief locks at start/end
Recommendation: For production index builds, schedule during low-traffic periods for very large tables.
Production Migration Strategy
- Create Doctrine migration with index
- Use ALGORITHM=INPLACE for non-blocking creation
- Schedule during low traffic for large tables
- Run migration:
make doctrine-migrations-migrate
- Verify index created:
SHOW INDEX FROM table_name
- Verify index is used: Run EXPLAIN on queries
- Measure performance improvement
public function up(Schema $schema): void
{
$this->addSql('CREATE INDEX idx_users_email ON users (email) ALGORITHM=INPLACE LOCK=NONE');
}
Performance Testing
final class UserEndpointPerformanceTest extends ApiTestCase
{
public function testNoN1Queries(): void
{
for ($i = 0; $i < 50; $i++) {
$this->createUser();
}
$this->enableQueryCounter();
$this->client->request('GET', '/api/users');
$queryCount = $this->getQueryCount();
$this->assertLessThan(10, $queryCount, 'N+1 query detected!');
}
public function testEndpointPerformance(): void
{
$start = microtime(true);
$response = $this->client->request(, );
= (() - ) * ;
->(, , );
}
}
Quick Commands Reference
docker compose exec database mariadb -u root -proot db
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.1;
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
SHOW INDEX FROM users;
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
SET GLOBAL slow_query_log = 'OFF';
make doctrine-migrations-generate
make doctrine-migrations-migrate
docker compose exec php bin/console doctrine:schema:validate
Workflow Integration
When to Use This Skill
Use after:
Use before:
Related skills:
Reference Documentation
Examples (Detailed Scenarios)
Reference Guides
Comparison: This Skill vs database-migrations
| Aspect | query-performance-analysis | database-migrations |
|---|
| Purpose | WHAT indexes to add | HOW to create indexes |
| Focus | Performance analysis | Schema definition |
| Tools | EXPLAIN, slow query log | Doctrine migrations, XML |
| When | Debugging slow queries | Creating entities/migrations |
| Output | Performance insights | Migration files, XML config |
Workflow: Use this skill to identify needed indexes, then use database-migrations for migration syntax.
Troubleshooting
Issue: Can't enable slow query log
Solution: Verify MySQL permissions, ensure connected to correct database
Issue: EXPLAIN shows ALL but index exists
Solution:
- Verify index covers your query pattern
- Check composite index field order
- Ensure query uses indexed fields exactly
- Check if optimizer chooses full scan for small tables
Issue: Container name error
Solution: Use database as the service name:
docker compose exec database mariadb -u root -proot
docker compose exec mysql mariadb -u root -proot
Issue: Symfony Profiler not showing queries
Solution: Enable profiler in dev mode:
web_profiler:
toolbar: true
intercept_redirects: false
External Resources
Best Practices
DO ✅
- Use Symfony Profiler in development for every new feature
- Analyze queries before deploying to production
- Add performance tests to prevent regressions
- Use eager loading to prevent N+1 queries
- Create indexes for frequently filtered/sorted fields
- Use EXPLAIN ANALYZE for detailed query plans
DON'T ❌
- Leave slow query log enabled in production at low threshold
- Add indexes without analyzing query patterns
- Ignore N+1 warnings (they compound quickly)
- Skip EXPLAIN analysis before adding indexes
- Forget to verify index is actually used after creation
- Add indexes on every column (write overhead)