SQL Optimization
What This Does
Analyzes and optimizes slow SQL queries by examining execution plans, recommending indexes, rewriting queries for better performance, and configuring database settings. Covers PostgreSQL, MySQL, and cloud warehouses (BigQuery, Snowflake, Redshift) with specific optimization techniques for each.
Instructions
Identify the slow queries. Gather:
- The SQL query text
- Current execution time
- Expected execution time / SLA
- Table sizes (row counts)
- Existing indexes
- Database engine and version
- How often the query runs (once, hourly, per-request)
Get the execution plan.
-- PostgreSQL
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...;
-- MySQL
EXPLAIN ANALYZE SELECT ...;
-- Look for:
-- Seq Scan (PostgreSQL) / Full Table Scan (MySQL) on large tables
-- Nested Loop joins on large datasets
-- Sort operations without index support
-- High row estimates vs actual rows (statistics issue)
Common optimization techniques (ordered by impact):
A. Add missing indexes.
-- Index for WHERE clause columns
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
-- Composite index for multi-column queries (order matters)
CREATE INDEX idx_orders_status_date ON orders (status, order_date);
-- Covering index (includes all needed columns, avoids table lookup)
CREATE INDEX idx_orders_covering ON orders (customer_id)
INCLUDE (order_date, total_amount, status);
-- Partial index (smaller, faster — only index what you query)
CREATE INDEX idx_active_orders ON orders (customer_id)
WHERE status = 'active';
-- PostgreSQL: always CONCURRENTLY for production tables
CREATE INDEX CONCURRENTLY idx_orders_customer ON orders (customer_id);
B. Rewrite the query.
-- BAD: Subquery in WHERE (executes per row)
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE region = 'US');
-- BETTER: JOIN (optimizer can choose best strategy)
SELECT o.* FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'US';
-- BAD: SELECT * (reads all columns from disk)
SELECT * FROM orders WHERE status = 'active';
-- BETTER: Select only needed columns
SELECT id, customer_id, total_amount FROM orders WHERE status = 'active';
-- BAD: OR conditions (often prevents index use)
SELECT * FROM orders WHERE status = 'active' OR status = 'pending';
-- BETTER: IN list (optimizer can use index)
SELECT * FROM orders WHERE status IN ('active', 'pending');
-- BAD: Function on indexed column (prevents index use)
SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- BETTER: Range comparison (uses index)
SELECT * FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';
C. Fix N+1 queries (application level).
-- BAD: N+1 — one query per order to get items
-- Application: for each order, SELECT * FROM order_items WHERE order_id = ?
-- BETTER: Batch load
SELECT * FROM order_items WHERE order_id = ANY($1);
-- Or use JOIN in the original query
D. Optimize JOINs.
-- Ensure join columns are indexed on both sides
-- Put the most selective filter first (reduces intermediate result set)
-- Use INNER JOIN instead of LEFT JOIN when nulls aren't needed
-- For very large joins, consider partitioning
E. Use materialized views for expensive aggregations.
-- PostgreSQL
CREATE MATERIALIZED VIEW mv_daily_revenue AS
SELECT
date_trunc('day', order_date) as day,
count(*) as order_count,
sum(total_amount) as revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1;
-- Refresh on schedule
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_revenue;
Database-specific optimizations.
PostgreSQL:
- Run
ANALYZE after large data changes (updates statistics)
- Use
pg_stat_statements to find slow queries automatically
- Tune
work_mem for sort/hash operations
- Use
EXPLAIN (BUFFERS) to see I/O patterns
MySQL:
- Check
slow_query_log for queries exceeding threshold
- Use
FORCE INDEX sparingly and only after verifying
- InnoDB buffer pool sizing is critical for read performance
- Use
covering indexes to avoid random I/O on secondary indexes
BigQuery/Snowflake:
- Partition tables by date for time-series queries
- Cluster tables by frequently filtered columns
- Avoid
SELECT * — column-store billing charges per column scanned
- Use approximate functions (
APPROX_COUNT_DISTINCT) when exact isn't needed
Verify the improvement. After optimization:
- Re-run EXPLAIN ANALYZE and compare
- Measure wall-clock time on production-like data
- Check that the optimization doesn't slow down other queries (index maintenance cost)
- Monitor for regression over time as data grows
Output Format
# SQL Optimization Report
## Query
```sql
{The original query}
Current Performance
- Execution time: {ms}
- Rows scanned: {count}
- Index usage: {which indexes used, or seq scan}
Optimizations Applied
1. {Optimization name}
- Before: {explain plan excerpt}
- After: {explain plan excerpt}
- Impact: {X}x improvement
2. {Optimization name}
...
Indexes Created
| Index |
Table |
Columns |
Type |
Size Impact |
| {name} |
{table} |
{columns} |
{btree/gin/gist} |
{estimated} |
Result
- Before: {time}ms
- After: {time}ms
- Improvement: {X}x faster
## Tips
- Always EXPLAIN ANALYZE first — don't guess where the bottleneck is
- The most common issue is a missing index — check for sequential scans on large tables
- Index order matters in composite indexes — put equality conditions first, range conditions last
- Too many indexes slow down writes — only index columns you actually query
- If a query is too complex to optimize, consider breaking it into CTEs or temp tables
- Statistics being outdated is a common hidden cause — run ANALYZE after bulk inserts
- For PostgreSQL, `pg_stat_statements` is the single most useful extension for finding slow queries
1---2name: sql-optimization3description: Optimize SQL queries, indexes, and execution plans for PostgreSQL, MySQL, and cloud data warehouses.4---56# SQL Optimization78## What This Does910Analyzes and optimizes slow SQL queries by examining execution plans, recommending indexes, rewriting queries for better performance, and configuring database settings. Covers PostgreSQL, MySQL, and cloud warehouses (BigQuery, Snowflake, Redshift) with specific optimization techniques for each.1112## Instructions13141. **Identify the slow queries.** Gather:15 - The SQL query text16 - Current execution time17 - Expected execution time / SLA18 - Table sizes (row counts)19 - Existing indexes20 - Database engine and version21 - How often the query runs (once, hourly, per-request)22232. **Get the execution plan.**24 ```sql25 -- PostgreSQL26 EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...;2728 -- MySQL29 EXPLAIN ANALYZE SELECT ...;3031 -- Look for:32 -- Seq Scan (PostgreSQL) / Full Table Scan (MySQL) on large tables33 -- Nested Loop joins on large datasets34 -- Sort operations without index support35 -- High row estimates vs actual rows (statistics issue)36 ```37383. **Common optimization techniques (ordered by impact):**3940 **A. Add missing indexes.**41 ```sql42 -- Index for WHERE clause columns43 CREATE INDEX idx_orders_customer_id ON orders (customer_id);4445 -- Composite index for multi-column queries (order matters)46 CREATE INDEX idx_orders_status_date ON orders (status, order_date);4748 -- Covering index (includes all needed columns, avoids table lookup)49 CREATE INDEX idx_orders_covering ON orders (customer_id)50 INCLUDE (order_date, total_amount, status);5152 -- Partial index (smaller, faster — only index what you query)53 CREATE INDEX idx_active_orders ON orders (customer_id)54 WHERE status = 'active';5556 -- PostgreSQL: always CONCURRENTLY for production tables57 CREATE INDEX CONCURRENTLY idx_orders_customer ON orders (customer_id);58 ```5960 **B. Rewrite the query.**61 ```sql62 -- BAD: Subquery in WHERE (executes per row)63 SELECT * FROM orders64 WHERE customer_id IN (SELECT id FROM customers WHERE region = 'US');6566 -- BETTER: JOIN (optimizer can choose best strategy)67 SELECT o.* FROM orders o68 JOIN customers c ON o.customer_id = c.id69 WHERE c.region = 'US';7071 -- BAD: SELECT * (reads all columns from disk)72 SELECT * FROM orders WHERE status = 'active';7374 -- BETTER: Select only needed columns75 SELECT id, customer_id, total_amount FROM orders WHERE status = 'active';7677 -- BAD: OR conditions (often prevents index use)78 SELECT * FROM orders WHERE status = 'active' OR status = 'pending';7980 -- BETTER: IN list (optimizer can use index)81 SELECT * FROM orders WHERE status IN ('active', 'pending');8283 -- BAD: Function on indexed column (prevents index use)84 SELECT * FROM orders WHERE YEAR(order_date) = 2024;8586 -- BETTER: Range comparison (uses index)87 SELECT * FROM orders88 WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';89 ```9091 **C. Fix N+1 queries (application level).**92 ```sql93 -- BAD: N+1 — one query per order to get items94 -- Application: for each order, SELECT * FROM order_items WHERE order_id = ?9596 -- BETTER: Batch load97 SELECT * FROM order_items WHERE order_id = ANY($1);98 -- Or use JOIN in the original query99 ```100101 **D. Optimize JOINs.**102 ```sql103 -- Ensure join columns are indexed on both sides104 -- Put the most selective filter first (reduces intermediate result set)105 -- Use INNER JOIN instead of LEFT JOIN when nulls aren't needed106 -- For very large joins, consider partitioning107 ```108109 **E. Use materialized views for expensive aggregations.**110 ```sql111 -- PostgreSQL112 CREATE MATERIALIZED VIEW mv_daily_revenue AS113 SELECT114 date_trunc('day', order_date) as day,115 count(*) as order_count,116 sum(total_amount) as revenue117 FROM orders118 WHERE status = 'completed'119 GROUP BY 1;120121 -- Refresh on schedule122 REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_revenue;123 ```1241254. **Database-specific optimizations.**126127 **PostgreSQL:**128 - Run `ANALYZE` after large data changes (updates statistics)129 - Use `pg_stat_statements` to find slow queries automatically130 - Tune `work_mem` for sort/hash operations131 - Use `EXPLAIN (BUFFERS)` to see I/O patterns132133 **MySQL:**134 - Check `slow_query_log` for queries exceeding threshold135 - Use `FORCE INDEX` sparingly and only after verifying136 - InnoDB buffer pool sizing is critical for read performance137 - Use `covering indexes` to avoid random I/O on secondary indexes138139 **BigQuery/Snowflake:**140 - Partition tables by date for time-series queries141 - Cluster tables by frequently filtered columns142 - Avoid `SELECT *` — column-store billing charges per column scanned143 - Use approximate functions (`APPROX_COUNT_DISTINCT`) when exact isn't needed1441455. **Verify the improvement.** After optimization:146 - Re-run EXPLAIN ANALYZE and compare147 - Measure wall-clock time on production-like data148 - Check that the optimization doesn't slow down other queries (index maintenance cost)149 - Monitor for regression over time as data grows150151## Output Format152153```markdown154# SQL Optimization Report155156## Query157```sql158{The original query}159```160161## Current Performance162- Execution time: {ms}163- Rows scanned: {count}164- Index usage: {which indexes used, or seq scan}165166## Optimizations Applied167168### 1. {Optimization name}169- **Before:** {explain plan excerpt}170- **After:** {explain plan excerpt}171- **Impact:** {X}x improvement172173### 2. {Optimization name}174...175176## Indexes Created177| Index | Table | Columns | Type | Size Impact |178|-------|-------|---------|------|-------------|179| {name} | {table} | {columns} | {btree/gin/gist} | {estimated} |180181## Result182- Before: {time}ms183- After: {time}ms184- Improvement: {X}x faster185```186187## Tips188189- Always EXPLAIN ANALYZE first — don't guess where the bottleneck is190- The most common issue is a missing index — check for sequential scans on large tables191- Index order matters in composite indexes — put equality conditions first, range conditions last192- Too many indexes slow down writes — only index columns you actually query193- If a query is too complex to optimize, consider breaking it into CTEs or temp tables194- Statistics being outdated is a common hidden cause — run ANALYZE after bulk inserts195- For PostgreSQL, `pg_stat_statements` is the single most useful extension for finding slow queries