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 the database-migrations skill which covers index creation syntax.
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
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer