Iron Law
PROFILE BEFORE OPTIMIZING — RUN EXPLAIN (ANALYZE, BUFFERS) ON THE ACTUAL SLOW QUERY BEFORE WRITING A SINGLE INDEX
When to Use This Skill
- Debugging slow-running queries in PostgreSQL
- Endpoint response times exceeding target SLA
- Designing performant database schemas for high-traffic tables
- Reducing database load and infrastructure costs
- Resolving N+1 query problems from ORM usage (SQLAlchemy, Prisma, R2DBC)
- Analyzing EXPLAIN query plans from production
- Implementing efficient composite and partial indexes
- Migrating from OFFSET pagination to cursor-based pagination
Do Not Use This Skill When
- You need cloud-native SQL or analytics platforms (BigQuery, Snowflake) — use
sql-pro
- You need schema migration design — use
database-schema-designer
- The database is not PostgreSQL (patterns are PostgreSQL-specific)
Quick Reference
| Problem |
Diagnosis |
Fix |
| Slow endpoint |
EXPLAIN (ANALYZE, BUFFERS) — look for Seq Scan |
Add index on WHERE/JOIN columns |
| N+1 queries |
Count DB calls per request > 1 |
JOIN + eager load or batch query |
| Slow pagination |
OFFSET on large table |
Cursor-based pagination |
| Slow aggregation |
GROUP BY without index |
Partial index + filter before GROUP BY |
| Slow COUNT(*) |
Full table count |
pg_class.reltuples estimate or index-only count |
| Repeated expensive query |
Same query hits DB repeatedly |
Materialized view + scheduled refresh |
| Large table slow scans |
Millions of rows, date filter |
Table partitioning by date range |
Process
Step 1 — Identify the Slow Query
-- Find the top-10 slowest queries by mean execution time
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
Step 2 — Profile with EXPLAIN
-- Always use ANALYZE + BUFFERS for real data
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT u.id, u.email, COUNT(o.id) AS order_count
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id, u.email;
Key nodes to watch:
Seq Scan on large table → add index
Index Scan → good; Index Only Scan → best
Nested Loop on large tables → may need Hash Join
Hash Batches > 1 → memory spill, increase work_mem
Rows estimate far from Actual Rows → run ANALYZE <table>
Step 3 — Apply the Right Fix
See resources/implementation-playbook.md for SQL code for each pattern.
Step 4 — Verify Improvement
-- Re-run EXPLAIN ANALYZE after index/change
-- Compare: Execution Time before vs after
-- Compare: Seq Scan → Index Only Scan
Optimization Patterns (Summary)
Load resources/implementation-playbook.md for full SQL code examples.
| Pattern |
When |
Speedup |
| Composite index |
Multi-column WHERE/ORDER BY |
10x–100x |
| Partial index |
Filtered subset (e.g., active users) |
5x–50x |
| Covering index |
Avoid table heap access |
2x–10x |
| Cursor pagination |
Replace OFFSET 100000 |
100x+ |
| N+1 elimination |
ORM loop → single JOIN |
N× → 1 query |
| Materialized view |
Repeated expensive aggregation |
100x+ |
| Batch operations |
Individual inserts/updates in loop |
10x–50x |
| Table partitioning |
Time-series or date-range tables >10M rows |
5x–20x |
Stack Integration
Python / FastAPI (asyncpg + SQLAlchemy)
- N+1 → use
selectinload or joinedload in SQLAlchemy async
- Batch insert →
execute_many with asyncpg
- Slow query → enable
echo=True on engine to capture SQL, then EXPLAIN
NestJS / Prisma
- N+1 → use Prisma
include: { orders: true } instead of loop queries
- Raw SQL →
prisma.$queryRaw for complex optimized queries
- Slow endpoint → enable Prisma query logging, identify the statement
Java / Spring Boot WebFlux (R2DBC)
- N+1 → use
DatabaseClient with JOIN query instead of reactive loop
- Batch → R2DBC
executeBatch() method
- Slow query → enable R2DBC logging, capture SQL for EXPLAIN
Anti-Patterns
- Creating indexes without running
EXPLAIN ANALYZE first
- Function in WHERE clause preventing index use:
WHERE LOWER(email) = ? — use expression index
LIKE '%term' with leading wildcard — cannot use B-Tree index; use GIN + pg_trgm
- Over-indexing: each index slows INSERT/UPDATE/DELETE — only index proven slow paths
OFFSET 100000 pagination on large tables — always replace with cursor
- Individual row inserts in a loop — always batch
Documentation Sources
- PostgreSQL EXPLAIN: Query MCP context7 with library ID
/postgresql/postgresql
pg_stat_statements: Query MCP context7
Reference Files
resources/implementation-playbook.md — Full SQL code for all 8 optimization patterns, EXPLAIN annotation guide, monitoring queries
Related Skills
sql-pro — Cloud analytics, HTAP, dimensional modeling, BigQuery/Snowflake
database-schema-designer — Schema design, migration patterns, FK/index rules
vector-database — pgvector HNSW/IVFFlat index alignment
python-dev — SQLAlchemy async, asyncpg patterns
java-spring-api — R2DBC reactive database access
nestjs-api — Prisma ORM, raw query patterns
1---2name: sql-optimization-patterns3description: Transforms slow PostgreSQL queries into fast operations through systematic EXPLAIN ANALYZE, indexing strategies, N+1 elimination, cursor pagination, and materialized views. Use when diagnosing slow queries, optimizing database performance, or designing index strategies — always profile before optimizing.4---56## Iron Law7PROFILE BEFORE OPTIMIZING — RUN `EXPLAIN (ANALYZE, BUFFERS)` ON THE ACTUAL SLOW QUERY BEFORE WRITING A SINGLE INDEX89## When to Use This Skill1011- Debugging slow-running queries in PostgreSQL12- Endpoint response times exceeding target SLA13- Designing performant database schemas for high-traffic tables14- Reducing database load and infrastructure costs15- Resolving N+1 query problems from ORM usage (SQLAlchemy, Prisma, R2DBC)16- Analyzing EXPLAIN query plans from production17- Implementing efficient composite and partial indexes18- Migrating from OFFSET pagination to cursor-based pagination1920## Do Not Use This Skill When2122- You need cloud-native SQL or analytics platforms (BigQuery, Snowflake) — use `sql-pro`23- You need schema migration design — use `database-schema-designer`24- The database is not PostgreSQL (patterns are PostgreSQL-specific)2526## Quick Reference2728| Problem | Diagnosis | Fix |29|---------|-----------|-----|30| Slow endpoint | `EXPLAIN (ANALYZE, BUFFERS)` — look for Seq Scan | Add index on WHERE/JOIN columns |31| N+1 queries | Count DB calls per request > 1 | JOIN + eager load or batch query |32| Slow pagination | `OFFSET` on large table | Cursor-based pagination |33| Slow aggregation | GROUP BY without index | Partial index + filter before GROUP BY |34| Slow COUNT(*) | Full table count | `pg_class.reltuples` estimate or index-only count |35| Repeated expensive query | Same query hits DB repeatedly | Materialized view + scheduled refresh |36| Large table slow scans | Millions of rows, date filter | Table partitioning by date range |3738## Process3940### Step 1 — Identify the Slow Query41```sql42-- Find the top-10 slowest queries by mean execution time43SELECT query, calls, total_exec_time, mean_exec_time44FROM pg_stat_statements45ORDER BY mean_exec_time DESC46LIMIT 10;47```4849### Step 2 — Profile with EXPLAIN50```sql51-- Always use ANALYZE + BUFFERS for real data52EXPLAIN (ANALYZE, BUFFERS, VERBOSE)53SELECT u.id, u.email, COUNT(o.id) AS order_count54FROM users u55JOIN orders o ON u.id = o.user_id56WHERE u.created_at > NOW() - INTERVAL '30 days'57GROUP BY u.id, u.email;58```5960**Key nodes to watch:**61- `Seq Scan` on large table → add index62- `Index Scan` → good; `Index Only Scan` → best63- `Nested Loop` on large tables → may need Hash Join64- `Hash Batches > 1` → memory spill, increase `work_mem`65- `Rows` estimate far from `Actual Rows` → run `ANALYZE <table>`6667### Step 3 — Apply the Right Fix68See `resources/implementation-playbook.md` for SQL code for each pattern.6970### Step 4 — Verify Improvement71```sql72-- Re-run EXPLAIN ANALYZE after index/change73-- Compare: Execution Time before vs after74-- Compare: Seq Scan → Index Only Scan75```7677## Optimization Patterns (Summary)7879Load `resources/implementation-playbook.md` for full SQL code examples.8081| Pattern | When | Speedup |82|---------|------|---------|83| Composite index | Multi-column WHERE/ORDER BY | 10x–100x |84| Partial index | Filtered subset (e.g., active users) | 5x–50x |85| Covering index | Avoid table heap access | 2x–10x |86| Cursor pagination | Replace `OFFSET 100000` | 100x+ |87| N+1 elimination | ORM loop → single JOIN | N× → 1 query |88| Materialized view | Repeated expensive aggregation | 100x+ |89| Batch operations | Individual inserts/updates in loop | 10x–50x |90| Table partitioning | Time-series or date-range tables >10M rows | 5x–20x |9192## Stack Integration9394### Python / FastAPI (asyncpg + SQLAlchemy)95- N+1 → use `selectinload` or `joinedload` in SQLAlchemy async96- Batch insert → `execute_many` with asyncpg97- Slow query → enable `echo=True` on engine to capture SQL, then EXPLAIN9899### NestJS / Prisma100- N+1 → use Prisma `include: { orders: true }` instead of loop queries101- Raw SQL → `prisma.$queryRaw` for complex optimized queries102- Slow endpoint → enable Prisma query logging, identify the statement103104### Java / Spring Boot WebFlux (R2DBC)105- N+1 → use `DatabaseClient` with JOIN query instead of reactive loop106- Batch → R2DBC `executeBatch()` method107- Slow query → enable R2DBC logging, capture SQL for EXPLAIN108109## Anti-Patterns110111- Creating indexes without running `EXPLAIN ANALYZE` first112- Function in WHERE clause preventing index use: `WHERE LOWER(email) = ?` — use expression index113- `LIKE '%term'` with leading wildcard — cannot use B-Tree index; use GIN + `pg_trgm`114- Over-indexing: each index slows INSERT/UPDATE/DELETE — only index proven slow paths115- `OFFSET 100000` pagination on large tables — always replace with cursor116- Individual row inserts in a loop — always batch117118## Documentation Sources119120- PostgreSQL EXPLAIN: Query MCP context7 with library ID `/postgresql/postgresql`121- `pg_stat_statements`: Query MCP context7122123## Reference Files124125- `resources/implementation-playbook.md` — Full SQL code for all 8 optimization patterns, EXPLAIN annotation guide, monitoring queries126127## Related Skills128129- `sql-pro` — Cloud analytics, HTAP, dimensional modeling, BigQuery/Snowflake130- `database-schema-designer` — Schema design, migration patterns, FK/index rules131- `vector-database` — pgvector HNSW/IVFFlat index alignment132- `python-dev` — SQLAlchemy async, asyncpg patterns133- `java-spring-api` — R2DBC reactive database access134- `nestjs-api` — Prisma ORM, raw query patterns