Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Optimize SQL query performance through EXPLAIN analysis, indexing strategies, and query rewriting for PostgreSQL, MySQL, and SQL Server. Use when debugging slow queries, analyzing execution plans, or improving database performance.
.claude/skills/ancoleman-optimizing-sql/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-21 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 208% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 123% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 202% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 150% | 0% |
Provide tactical guidance for optimizing SQL query performance across PostgreSQL, MySQL, and SQL Server through execution plan analysis, strategic indexing, and query rewriting.
Trigger this skill when encountering:
Run execution plan analysis to identify bottlenecks:
PostgreSQL:
sqlEXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com';
MySQL:
sqlEXPLAIN FORMAT=JSON SELECT * FROM products WHERE category_id = 5;
SQL Server: Use SQL Server Management Studio: Display Estimated Execution Plan (Ctrl+L)
Key Metrics to Monitor:
For detailed execution plan interpretation, see references/explain-guide.md.
Common Red Flags:
| Indicator | Problem | Solution | |-----------|---------|----------| | Seq Scan / Table Scan | Full table scan on large table | Add index on filter columns | | High row count | Processing excessive rows | Add WHERE filter or index | | Nested Loop with large outer table | Inefficient join algorithm | Index join columns | | Correlated subquery | Subquery executes per row | Rewrite as JOIN or EXISTS | | Sort operation on large result set | Expensive sorting | Add index matching ORDER BY |
For scan type interpretation, see references/scan-types.md.
Index Decision Framework:
Is column used in WHERE, JOIN, ORDER BY, or GROUP BY?
├─ YES → Is column selective (many unique values)?
│ ├─ YES → Is table frequently queried?
│ │ ├─ YES → ADD INDEX
│ │ └─ NO → Consider based on query frequency
│ └─ NO (low selectivity) → Skip index
└─ NO → Skip indexIndex Types by Use Case:
PostgreSQL:
MySQL:
SQL Server:
For comprehensive indexing guidance, see references/indexing-decisions.md and references/index-types.md.
For queries filtering on multiple columns, use composite indexes:
Column Order Matters:
Example:
sql-- Query pattern SELECT * FROM orders WHERE customer_id = 123 AND status = 'shipped' ORDER BY created_at DESC LIMIT 10; -- Optimal composite index CREATE INDEX idx_orders_customer_status_created ON orders (customer_id, status, created_at DESC);
For composite index design patterns, see references/composite-indexes.md.
Common Anti-Patterns to Avoid:
1. SELECT (Over-fetching)
sql-- ❌ Bad: Fetches all columns SELECT * FROM users WHERE id = 1; -- ✅ Good: Fetch only needed columns SELECT id, name, email FROM users WHERE id = 1;
2. N+1 Queries
sql-- ❌ Bad: 1 + N queries SELECT * FROM users LIMIT 100; -- Then in loop: SELECT * FROM posts WHERE user_id = ?; -- ✅ Good: Single JOIN SELECT users.*, posts.id AS post_id, posts.title FROM users LEFT JOIN posts ON users.id = posts.user_id;
3. Non-Sargable Queries (functions on indexed columns)
sql-- ❌ Bad: Function prevents index usage SELECT * FROM orders WHERE YEAR(created_at) = 2025; -- ✅ Good: Sargable range condition SELECT * FROM orders WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01';
4. Correlated Subqueries
sql-- ❌ Bad: Subquery executes per row SELECT name, (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) FROM users; -- ✅ Good: JOIN with GROUP BY SELECT users.name, COUNT(orders.id) AS order_count FROM users LEFT JOIN orders ON users.id = orders.user_id GROUP BY users.id, users.name;
For complete anti-pattern reference, see references/anti-patterns.md. For efficient query patterns, see references/efficient-patterns.md.
| Query Pattern | Index Type | Example | |--------------|------------|---------| | WHERE column = value | Single-column B-tree | CREATE INDEX ON table (column) | | WHERE col1 = ? AND col2 = ? | Composite B-tree | CREATE INDEX ON table (col1, col2) | | WHERE text_col LIKE '%word%' | Full-text (GIN/Full-text) | CREATE INDEX ON table USING GIN (to_tsvector('english', text_col)) | | WHERE geom && box | Spatial (GiST) | CREATE INDEX ON table USING GIST (geom) | | WHERE json_col @> '{"key":"value"}' | JSONB (GIN) | CREATE INDEX ON table USING GIN (json_col) |
| Scan Type | Performance | When Acceptable | |-----------|-------------|-----------------| | Index-Only Scan | Best | Always preferred | | Index Scan | Excellent | Small-medium result sets | | Bitmap Heap Scan | Good | Medium result sets (PostgreSQL) | | Sequential Scan | Poor | Only for small tables (<1000 rows) or full table queries | | Table Scan | Poor | Only for small tables or unavoidable full scans |
Partial Indexes (index subset of rows):
sqlCREATE INDEX idx_active_users_login ON users (last_login) WHERE status = 'active';
Expression Indexes (index computed values):
sqlCREATE INDEX idx_users_email_lower ON users (LOWER(email));
Covering Indexes (avoid heap access):
sqlCREATE INDEX idx_users_email_covering ON users (email) INCLUDE (id, name);
For comprehensive PostgreSQL optimization, see references/postgresql.md.
Index Hints (override optimizer):
sqlSELECT * FROM orders USE INDEX (idx_orders_customer) WHERE customer_id = 123;
Storage Engine Selection:
For comprehensive MySQL optimization, see references/mysql.md.
Query Store (track query performance over time):
sqlALTER DATABASE YourDatabase SET QUERY_STORE = ON;
Execution Plan Warnings:
For comprehensive SQL Server optimization, see references/sqlserver.md.
Break complex queries into readable, maintainable parts:
sqlWITH active_customers AS ( SELECT id, name FROM customers WHERE status = 'active' ), recent_orders AS ( SELECT customer_id, COUNT(*) as order_count FROM orders WHERE created_at > NOW() - INTERVAL '30 days' GROUP BY customer_id ) SELECT ac.name, COALESCE(ro.order_count, 0) as orders FROM active_customers ac LEFT JOIN recent_orders ro ON ac.id = ro.customer_id;
Use EXISTS for better performance with large datasets:
sql-- ✅ Good: EXISTS stops at first match SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id); -- ❌ Less efficient: IN builds full list SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);
Consider denormalization when:
Denormalization Strategies:
Scenario: API endpoint taking 2 seconds to load
Step 1: Identify Slow Query
Use APM/observability tools to identify database query causing delayStep 2: Run EXPLAIN ANALYZE
sqlEXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC LIMIT 10;
Step 3: Analyze Output
Seq Scan on orders (cost=0.00..2500.00 rows=10)
Filter: (customer_id = 123)
Rows Removed by Filter: 99990Problem: Sequential scan filtering 99,990 rows
Step 4: Add Composite Index
sqlCREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC);
Step 5: Verify Improvement
sqlEXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC LIMIT 10;
Index Scan using idx_orders_customer_created (cost=0.42..12.44 rows=10)
Index Cond: (customer_id = 123)Result: 200x faster (2000ms → 10ms)
Regular Optimization Tasks:
PostgreSQL Statistics Update:
sqlANALYZE table_name;
MySQL Statistics Update:
sqlANALYZE TABLE table_name;
SQL Server Statistics Update:
sqlUPDATE STATISTICS table_name;
For comprehensive documentation, reference these files:
references/explain-guide.md - Detailed EXPLAIN plan interpretationreferences/scan-types.md - Scan type meanings and performance implicationsreferences/indexing-decisions.md - When and how to add indexesreferences/index-types.md - Database-specific index typesreferences/composite-indexes.md - Multi-column index designreferences/anti-patterns.md - Common anti-patterns with solutionsreferences/efficient-patterns.md - Efficient query patternsreferences/postgresql.md - PostgreSQL-specific optimizationsreferences/mysql.md - MySQL-specific optimizationsreferences/sqlserver.md - SQL Server-specific optimizationsFor working SQL examples, see examples/ directory.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 8,365 | 5,406 | -35% | 1 | 1 | 0% | 1,278 | 3,940 | +208% | 0 | 0 | — |
case-02 | pass→pass | 10,517 | 7,198 | -32% | 1 | 1 | 0% | 1,916 | 4,277 | +123% | 0 | 0 | — |
case-03 | pass→pass | 8,280 | 8,281 | +0% | 1 | 1 | 0% | 1,462 | 4,410 | +202% | 0 | 0 | — |
case-04 | pass→pass | 10,203 | 9,070 | -11% | 1 | 1 | 0% | 1,864 | 4,655 | +150% | 0 | 0 | — |
case-05 | pass→pass | 9,463 | 7,886 | -17% | 1 | 1 | 0% | 1,702 | 4,358 | +156% | 0 | 0 | — |
case-06 | pass→pass | 4,137 | 6,493 | +57% | 1 | 1 | 0% | 683 | 4,082 | +498% | 0 | 0 | — |
case-07 | pass→pass | 9,396 | 7,694 | -18% | 1 | 1 | 0% | 1,602 | 4,339 | +171% | 0 | 0 | — |
case-08 | pass→pass | 7,143 | 6,226 | -13% | 1 | 1 | 0% | 1,266 | 4,102 | +224% | 0 | 0 | — |
case-09 | pass→pass | 7,123 | 6,067 | -15% | 1 | 1 | 0% | 1,321 | 4,100 | +210% | 0 | 0 | — |
case-10 | pass→pass | 9,766 | 7,130 | -27% | 1 | 1 | 0% | 1,685 | 3,905 | +132% | 0 | 0 | — |
case-11 | pass→pass | 3,445 | 5,182 | +50% | 1 | 1 | 0% | 529 | 3,863 | +630% | 0 | 0 | — |
case-12 | pass→pass | 6,293 | 5,462 | -13% | 1 | 1 | 0% | 1,100 | 3,929 | +257% | 0 | 0 | — |
case-13 | pass→pass | 10,907 | 9,488 | -13% | 1 | 1 | 0% | 1,781 | 4,616 | +159% | 0 | 0 | — |
case-14 | pass→pass | 4,097 | 4,639 | +13% | 1 | 1 | 0% | 633 | 3,678 | +481% | 0 | 0 | — |
case-15 | pass→pass | 3,804 | 4,160 | +9% | 1 | 1 | 0% | 609 | 3,752 | +516% | 0 | 0 | — |
case-16 | pass→pass | 13,468 | 7,875 | -42% | 1 | 1 | 0% | 2,447 | 4,223 | +73% | 0 | 0 | — |
case-17 | pass→pass | 9,654 | 5,204 | -46% | 1 | 1 | 0% | 907 | 3,815 | +321% | 0 | 0 | — |
case-18 | pass→pass | 13,549 | 6,773 | -50% | 1 | 1 | 0% | 1,621 | 4,062 | +151% | 0 | 0 | — |
case-19 | pass→pass | 9,727 | 11,595 | +19% | 1 | 1 | 0% | 1,477 | 4,813 | +226% | 0 | 0 | — |
case-20 | pass→pass | 15,384 | 12,107 | -21% | 1 | 1 | 0% | 2,761 | 5,176 | +87% | 0 | 0 | — |
case-21 | fail→pass | 19,964 | 14,555 | -27% | 1 | 1 | 0% | 3,307 | 5,660 | +71% | 0 | 0 | — |
case-22 | pass→pass | 14,933 | 13,775 | -8% | 1 | 1 | 0% | 2,588 | 5,456 | +111% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.