Database Query Optimization
Purpose
Slow databases kill applications. This skill replaces guesswork with systematic performance analysis, using EXPLAIN plans and profiling to eliminate N+1 queries, eliminate unnecessary scans, and add targeted indexes so the database bears the computational load, not the application.
When to use
- Writing complex SQL queries or ORM access functions
- Resolving performance bottlenecks on read-heavy endpoints
- Designing schema migrations for growing datasets
- Refactoring loops that make repeated database calls
When NOT to use
- Schema design (different concern)
- Database selection (architectural decision)
- Caching strategies (use Caching Strategies skill)
- Application-level performance (profilers, algorithms)
Inputs required
- Slow query logs or endpoint metrics
- ORM code accessing the database
- Database schema (tables, columns, indexes)
- EXPLAIN ANALYZE capability (test environment)
Workflow
- Profile the Bottleneck: Run EXPLAIN ANALYZE on slow queries to identify sequential scans and high-cost operations
- Identify N+1: Locate loops making repetitive database calls for the same entity type
- Measure Selectivity: Analyze WHERE clause filters and add indexes to highly selective columns
- Replace Loops: Replace N+1 with single IN queries or ORM eager-loading (JOINs)
- Remove Over-Fetching: Replace SELECT * with explicit column names
- Add Indexes: Add B-Tree indexes to WHERE, JOIN, and ORDER BY columns in slow queries
- Paginate: Enforce LIMIT and OFFSET (or cursor pagination) on all collection queries
- Verify Performance: Re-run EXPLAIN ANALYZE and benchmark end-to-end latency
Rules
- MUST explicitly define selected columns (NEVER use SELECT * in production)
- MUST NEVER have database operations inside loops
- MUST perform filtering and aggregation in the database, not application memory
- MUST enforce LIMIT and OFFSET on collection queries
- MUST EXPLAIN before adding indexes (verify they reduce cost)
- MUST rollback indexes if they degrade INSERT/UPDATE performance
- MUST NOT over-index (each index has maintenance cost)
Anti-patterns
- The N+1 Problem: Fetching 50 users, then making 50 individual queries to fetch each user's profile
- Over-Indexing: Adding an index to every single column (degrades INSERT/UPDATE performance)
- Application-Side Filtering: Fetching 10,000 rows from DB and using
filter() in JavaScript
- SELECT * in Production: Fetching all columns including BLOBs when needing only
id and name
- Unbounded Queries: Collection endpoints without LIMIT/OFFSET returning millions of rows
- Ignoring Selectivity: Adding indexes to low-cardinality columns (gender, status)
Failure conditions
- Database unavailable for profiling
- No query metrics/logs available
- EXPLAIN ANALYZE not supported by database
- Migration lacks rollback strategy
- Index changes cause lock timeouts on large tables
Validation checklist
Output format
- SQL format: ANSI standard, explicitly selecting columns, using parameterized queries
- ORM calls: Using eager-load or relationship methods (not loops)
- Schema changes: Migration files with forward and rollback steps
- Indexes: B-Tree on high-selectivity columns, documented in schema
- Validation: EXPLAIN ANALYZE output showing acceptable costs
Security considerations
- All user input MUST be parameterized (prevent SQL injection)
- Query results MUST be accessible by user (respect permissions)
- Large result sets MUST be paginated (prevent DOS)
- Timing attacks: ensure query timing doesn't leak existence of data
Agent execution notes
- Agent MAY: Add indexes, create ORM relationships, replace loops with joins, add LIMIT/OFFSET
- Agent MUST NEVER: Use SELECT *, create loops with queries, add untested indexes, bypass pagination
- Agent MUST ASK: Before dropping existing indexes, before major query rewrites, before schema changes
- Agent MUST VALIDATE: EXPLAIN ANALYZE shows improvement, no N+1 remaining, pagination enforced
Example
**❌ Anti-pattern (N+1, SELECT , no pagination, missing indexes):*
// N+1 problem: 1 query for users + 50 queries for posts
const users = await User.findAll(); // SELECT * (over-fetching)
for (const user of users) {
const posts = await Post.find({ userId: user.id }); // 50 individual queries
console.log(user, posts);
}
// Application-side filtering
const allUsers = await User.find({});
const filtered = allUsers.filter(u => u.status === 'active');
✅ Correct pattern (Join, eager-load, explicit columns, paginated):
// Single optimized query with JOIN and explicit columns
const usersWithPosts = await User.findAll({
attributes: ['id', 'username', 'email'],
include: [{
model: Post,
attributes: ['id', 'title', 'createdAt'],
required: true
}],
limit: 20,
offset: 0,
order: [['createdAt', 'DESC']]
});
// Or raw SQL with pagination
const query = `
SELECT u.id, u.username, p.id, p.title
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.status = $1
ORDER BY u.created_at DESC
LIMIT $2 OFFSET $3
`;
const result = await db.query(query, ['active', 20, 0]);
// Indexes created
// CREATE INDEX idx_posts_user_id ON posts(user_id);
// CREATE INDEX idx_users_status ON users(status) WHERE status = 'active';
1---2name: database-query-optimization3description: When addressing slow application endpoints, high database CPU usage, or standardizing data access patterns.4license: MIT5---67# Database Query Optimization89## Purpose10Slow databases kill applications. This skill replaces guesswork with systematic performance analysis, using EXPLAIN plans and profiling to eliminate N+1 queries, eliminate unnecessary scans, and add targeted indexes so the database bears the computational load, not the application.1112## When to use13- Writing complex SQL queries or ORM access functions14- Resolving performance bottlenecks on read-heavy endpoints15- Designing schema migrations for growing datasets16- Refactoring loops that make repeated database calls1718## When NOT to use19- Schema design (different concern)20- Database selection (architectural decision)21- Caching strategies (use Caching Strategies skill)22- Application-level performance (profilers, algorithms)2324## Inputs required25- Slow query logs or endpoint metrics26- ORM code accessing the database27- Database schema (tables, columns, indexes)28- EXPLAIN ANALYZE capability (test environment)2930## Workflow311. **Profile the Bottleneck**: Run EXPLAIN ANALYZE on slow queries to identify sequential scans and high-cost operations322. **Identify N+1**: Locate loops making repetitive database calls for the same entity type333. **Measure Selectivity**: Analyze WHERE clause filters and add indexes to highly selective columns344. **Replace Loops**: Replace N+1 with single IN queries or ORM eager-loading (JOINs)355. **Remove Over-Fetching**: Replace SELECT * with explicit column names366. **Add Indexes**: Add B-Tree indexes to WHERE, JOIN, and ORDER BY columns in slow queries377. **Paginate**: Enforce LIMIT and OFFSET (or cursor pagination) on all collection queries388. **Verify Performance**: Re-run EXPLAIN ANALYZE and benchmark end-to-end latency3940## Rules41- MUST explicitly define selected columns (NEVER use SELECT * in production)42- MUST NEVER have database operations inside loops43- MUST perform filtering and aggregation in the database, not application memory44- MUST enforce LIMIT and OFFSET on collection queries45- MUST EXPLAIN before adding indexes (verify they reduce cost)46- MUST rollback indexes if they degrade INSERT/UPDATE performance47- MUST NOT over-index (each index has maintenance cost)4849## Anti-patterns50- **The N+1 Problem**: Fetching 50 users, then making 50 individual queries to fetch each user's profile51- **Over-Indexing**: Adding an index to every single column (degrades INSERT/UPDATE performance)52- **Application-Side Filtering**: Fetching 10,000 rows from DB and using `filter()` in JavaScript53- **SELECT * in Production**: Fetching all columns including BLOBs when needing only `id` and `name`54- **Unbounded Queries**: Collection endpoints without LIMIT/OFFSET returning millions of rows55- **Ignoring Selectivity**: Adding indexes to low-cardinality columns (gender, status)5657## Failure conditions58- Database unavailable for profiling59- No query metrics/logs available60- EXPLAIN ANALYZE not supported by database61- Migration lacks rollback strategy62- Index changes cause lock timeouts on large tables6364## Validation checklist65- [ ] EXPLAIN ANALYZE shows acceptable query cost (< 1000 for simple queries)66- [ ] No sequential scans on large tables (use indexes)67- [ ] SELECT explicitly lists columns (no SELECT *)68- [ ] No loops making repeated database calls69- [ ] N+1 problems replaced with JOIN or ORM eager-load70- [ ] WHERE/JOIN/ORDER BY columns are indexed71- [ ] Collection queries enforce LIMIT and OFFSET72- [ ] No unused indexes (maintenance burden)73- [ ] Low-cardinality columns (gender, status) are NOT indexed74- [ ] Benchmark shows latency improvement (confirm end-to-end)7576## Output format77- **SQL format**: ANSI standard, explicitly selecting columns, using parameterized queries78- **ORM calls**: Using eager-load or relationship methods (not loops)79- **Schema changes**: Migration files with forward and rollback steps80- **Indexes**: B-Tree on high-selectivity columns, documented in schema81- **Validation**: EXPLAIN ANALYZE output showing acceptable costs8283## Security considerations84- All user input MUST be parameterized (prevent SQL injection)85- Query results MUST be accessible by user (respect permissions)86- Large result sets MUST be paginated (prevent DOS)87- Timing attacks: ensure query timing doesn't leak existence of data8889## Agent execution notes90- Agent MAY: Add indexes, create ORM relationships, replace loops with joins, add LIMIT/OFFSET91- Agent MUST NEVER: Use SELECT *, create loops with queries, add untested indexes, bypass pagination92- Agent MUST ASK: Before dropping existing indexes, before major query rewrites, before schema changes93- Agent MUST VALIDATE: EXPLAIN ANALYZE shows improvement, no N+1 remaining, pagination enforced9495## Example9697**❌ Anti-pattern (N+1, SELECT *, no pagination, missing indexes):**98```javascript99// N+1 problem: 1 query for users + 50 queries for posts100const users = await User.findAll(); // SELECT * (over-fetching)101for (const user of users) {102 const posts = await Post.find({ userId: user.id }); // 50 individual queries103 console.log(user, posts);104}105106// Application-side filtering107const allUsers = await User.find({});108const filtered = allUsers.filter(u => u.status === 'active');109```110111**✅ Correct pattern (Join, eager-load, explicit columns, paginated):**112```javascript113// Single optimized query with JOIN and explicit columns114const usersWithPosts = await User.findAll({115 attributes: ['id', 'username', 'email'],116 include: [{117 model: Post,118 attributes: ['id', 'title', 'createdAt'],119 required: true120 }],121 limit: 20,122 offset: 0,123 order: [['createdAt', 'DESC']]124});125126// Or raw SQL with pagination127const query = `128 SELECT u.id, u.username, p.id, p.title129 FROM users u130 LEFT JOIN posts p ON u.id = p.user_id131 WHERE u.status = $1132 ORDER BY u.created_at DESC133 LIMIT $2 OFFSET $3134`;135const result = await db.query(query, ['active', 20, 0]);136137// Indexes created138// CREATE INDEX idx_posts_user_id ON posts(user_id);139// CREATE INDEX idx_users_status ON users(status) WHERE status = 'active';140```