Detect N+1 queries, analyze slow queries with EXPLAIN, identify missing indexes, and ensure safe online index migrations for MySQL/MariaDB...
Use this skill when:
Analyze query performance, detect N+1 issues, identify missing indexes, and create safe online index migrations with verification steps.
Success Criteria:
Before Merging Code:
When Adding Indexes:
docker compose exec database mariadb -u root -proot
-- Enable slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.1; -- Log queries slower than 100ms
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- Verify settings
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
curl https://localhost/api/users
-- View recent slow queries (MariaDB)
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
-- Or use Symfony Profiler for detailed query analysis
-- Open: https://localhost/_profiler
N+1 Problem Symptoms:
foreach loopsSlow Query Symptoms:
type: ALL (full table scan)rows vs actual returned rowsSET GLOBAL slow_query_log = 'OFF';
Detection: 100+ queries for 100 records
Fix: Use eager loading with Doctrine
// ❌ BAD: N+1 problem
$users = $repository->findAll(); // 1 query
foreach ($users as $user) {
$token = $user->getConfirmationToken(); // N queries if lazy loaded!
}
// ✅ GOOD: Eager loading with QueryBuilder
$qb = $this->createQueryBuilder('u');
$qb->leftJoin('u.confirmationToken', 't')
->addSelect('t'); // Eager load tokens
$users = $qb->getQuery()->getResult();
See: examples/n-plus-one-detection.md for complete guide
Detection: EXPLAIN shows type: ALL, execution time >100ms
Fix: Add index
-- Check query performance
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- If type: ALL → add index
Add index in Doctrine migration:
// migrations/VersionXXX.php
public function up(Schema $schema): void
{
$this->addSql('CREATE INDEX idx_users_email ON users (email)');
}
Or in XML mapping:
<!-- config/doctrine/User.orm.xml -->
<indexes>
<index name="idx_email" columns="email"/>
</indexes>
See: examples/slow-query-analysis.md for EXPLAIN interpretation
Detection: Queries filter/sort on fields without indexes
Common patterns needing indexes:
email = ?, status = ?created_at DESCstatus = ? AND type = ?user_id, token_idCursor 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
| 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
MariaDB 11.4+ supports online DDL:
ALGORITHM=INPLACERecommendation: For production index builds, schedule during low-traffic periods for very large tables.
make doctrine-migrations-migrateSHOW INDEX FROM table_name// Migration example with online DDL
public function up(Schema $schema): void
{
$this->addSql('CREATE INDEX idx_users_email ON users (email) ALGORITHM=INPLACE LOCK=NONE');
}
final class UserEndpointPerformanceTest extends ApiTestCase
{
public function testNoN1Queries(): void
{
// Arrange: Create test data
for ($i = 0; $i < 50; $i++) {
$this->createUser();
}
// Act: Enable query counter
$this->enableQueryCounter();
$this->client->request('GET', '/api/users');
// Assert: Should have minimal queries
$queryCount = $this->getQueryCount();
$this->assertLessThan(10, $queryCount, 'N+1 query detected!');
}
public function testEndpointPerformance(): void
{
// Measure response time
$start = microtime(true);
$response = $this->client->request('GET', '/api/users');
$duration = (microtime(true) - $start) * 1000;
// Assert: Should be fast
$this->assertLessThan(200, $duration, "Too slow: {$duration}ms");
}
}
# Connect to MySQL
docker compose exec database mariadb -u root -proot db
-- Enable slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.1;
-- View slow queries
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
-- Check indexes
SHOW INDEX FROM users;
-- EXPLAIN query
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- EXPLAIN with extended info
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
-- IMPORTANT: Disable slow query log in production
SET GLOBAL slow_query_log = 'OFF';
# Create migration
make doctrine-migrations-generate
# Run migration
make doctrine-migrations-migrate
# Validate schema
docker compose exec php bin/console doctrine:schema:validate
Use after:
Use before:
Related skills:
| 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.
Issue: Can't enable slow query log
Solution: Verify MySQL permissions, ensure connected to correct database
Issue: EXPLAIN shows ALL but index exists
Solution:
Issue: Container name error
Solution: Use database as the service name:
docker compose exec database mariadb -u root -proot # ✅ Correct
docker compose exec mysql mariadb -u root -proot # ❌ Wrong
Issue: Symfony Profiler not showing queries
Solution: Enable profiler in dev mode:
# config/packages/dev/web_profiler.yaml
web_profiler:
toolbar: true
intercept_redirects: false