| name | query-performance-analysis |
| description | Detect N+1 queries, analyze slow queries with EXPLAIN, identify missing indexes, and ensure safe online index migrations. 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
- Missing index warnings in MongoDB 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 (no 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 MongoDB Profiler
docker compose exec database mongosh -u root -p secret --authenticationDatabase admin
show dbs
use app
db.setProfilingLevel(2, { slowms: 100 })
db.getProfilingStatus()
Note: Use show dbs to see all databases. The application database is typically the one with data (not 'admin', 'config', or 'local'). This project uses app as the database name.
Step 2: Run Your Endpoint
curl http://localhost/api/customers
Step 3: Analyze Query Patterns
db.system.profile.find().sort({ ts: -1 }).limit(10).pretty();
db.system.profile.aggregate([
{ $group: { _id: '$command.filter', count: { $sum: 1 } } },
{ $match: { count: { $gt: 10 } } },
{ $sort: { count: -1 } },
]);
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:
millis field >100ms
planSummary: "COLLSCAN" (collection scan)
- High
docsExamined vs nReturned
Step 5: Disable Profiler (Important!)
db.setProfilingLevel(0);
db.setProfilingLevel(1, { slowms: 200 });
Common Performance Issues
Issue 1: N+1 Queries
Detection: 100+ queries for 100 records
Fix: Use eager loading
$customers = $repository->findAll();
foreach ($customers as $customer) {
$type = $typeRepository->find($customer->getTypeId());
}
$qb = $this->createQueryBuilder(Customer::class);
$qb->field('type')->prime(true);
$customers = $qb->getQuery()->execute();
See: examples/n-plus-one-detection.md for complete guide
Issue 2: Slow Queries (No Index)
Detection: EXPLAIN shows COLLSCAN, execution time >100ms
Fix: Add index
db.customers.find({ email: 'test@example.com' }).explain('executionStats');
Add index in XML mapping:
<indexes>
<index><key name="email" order="asc"/></index>
</indexes>
Apply schema update:
docker compose exec php bin/console doctrine:mongodb:schema:update
See: examples/slow-query-analysis.md for EXPLAIN interpretation
Issue 3: Missing Indexes on Filtered Fields
Cursor pagination + ULID (_id) index strategy (this repo)
This service uses cursor pagination on ulid via API Platform, and Doctrine ODM maps ulid as the MongoDB document identifier (_id) via:
<id field-name="ulid" type="ulid" strategy="NONE" />
Best practice for cursor pagination with filters is a compound index that starts with the filter field(s) and ends with the cursor field:
{ phone: 1, _id: 1 } (API Platform filter by phone, cursor by ulid/_id)
{ createdAt: 1, _id: 1 } (date filter + cursor)
In XML mappings, you should still declare the cursor part as ulid (Doctrine will materialize it as _id in Mongo):
<index>
<key name="phone" order="asc" />
<key name="ulid" order="asc" />
</index>
Detection: Queries filter/sort on fields without indexes
Common patterns needing indexes:
- WHERE clause fields:
status = 'active'
- ORDER BY fields:
createdAt DESC
- Compound filters:
status = 'active' AND type = 'premium'
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
MongoDB 4.2+ uses an optimized hybrid index build approach:
- Obtains exclusive locks only briefly at start and end of the build
- Allows concurrent reads and writes during most of the build process
- Note: Brief locking at start/end can still impact write latency
Recommendation: For all production index builds, schedule index creation during low-traffic periods or maintenance windows to avoid latency spikes. This is important regardless of collection size, but exercise extra caution with large collections, as longer build times increase the risk and duration of impact.
Production Migration Strategy
- Add index to XML mapping
- Schedule during low traffic (or maintenance window for large collections)
- Run schema update:
doctrine:mongodb:schema:update
- Verify index created:
db.collection.getIndexes()
- Verify index is used: Run EXPLAIN on queries
- Measure performance improvement
Performance Testing
final class CustomerEndpointTest extends ApiTestCase
{
public function testNoN1Queries(): void
{
for ($i = 0; $i < 50; $i++) {
$this->createCustomer();
}
$this->enableQueryCounter();
$this->client->request('GET', '/api/customers');
$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 mongosh -u root -p secret --authenticationDatabase admin
show dbs
use app
db.setProfilingLevel(2, { slowms: 100 })
db.system.profile.find({ millis: { $gt: 100 } }).sort({ millis: -1 })
db.customers.getIndexes()
db.customers.find({ email: "test@example.com" }).explain("executionStats")
db.setProfilingLevel(0)
docker compose exec php bin/console doctrine:mongodb:schema:update
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, profiler | Doctrine ODM, XML mappings |
| When | Debugging slow queries | Creating entities/migrations |
| Output | Performance insights | XML configuration |
Workflow: Use this skill to identify needed indexes, then use database-migrations for XML syntax.
Troubleshooting
Issue: Can't enable MongoDB profiler
Solution: Verify MongoDB version (4.0+), check permissions, ensure connected to correct database
Issue: EXPLAIN shows COLLSCAN but index exists
Solution:
- Verify index covers your query pattern
- Check compound index field order
- Ensure query uses indexed fields exactly
Issue: Container name error: "service 'mongodb' not found"
Solution: Use database as the service name (not mongodb):
docker compose exec database mongosh
docker compose exec mongodb mongosh
Issue: Database name unknown
Solution: List databases to find the application database:
docker compose exec database mongosh -u root -p secret --authenticationDatabase admin --eval "db.getMongo().getDBNames()"
docker compose exec database mongosh -u root -p secret --authenticationDatabase admin
show dbs
use app
External Resources
Best Practices
DO ✅
- Enable 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
- Disable profiler after analysis (avoid overhead)
DON'T ❌
- Leave profiler level 2 enabled in production
- 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
- Hardcode database names (use
DB_NAME variable or identify from show dbs first)
- Assume index creation is completely non-blocking (brief locks still occur at start/end)