name: sql-optimization
description: Analyzes and optimizes SQL queries for better performance, including index design, query rewriting, execution plan analysis, and database tuning. Covers PostgreSQL-specific optimizations, N+1 prevention, CTE/window function optimization, join strategies, and common anti-patterns. Trigger keywords: SQL, query optimization, EXPLAIN, EXPLAIN ANALYZE, index, slow query, execution plan, query plan, join optimization, subquery, CTE, common table expression, window function, partition, N+1, query cache, database performance, sequential scan, index scan, bitmap scan, nested loop, hash join, merge join, PostgreSQL, query tuning, table scan, cardinality, statistics, vacuum, analyze.
allowed-tools: Read, Grep, Glob, Bash
SQL Optimization
Overview
This skill focuses on analyzing and optimizing SQL queries for improved performance. It covers query analysis, index optimization, execution plan interpretation, query rewriting strategies, PostgreSQL-specific optimizations, and common anti-patterns. Use this skill for slow queries, N+1 problems, join optimization, index design, and database performance tuning.
Instructions
1. Analyze Query Performance
- Identify slow queries from logs
- Run EXPLAIN/EXPLAIN ANALYZE
- Measure query execution time
- Check resource utilization
2. Understand Execution Plans
- Identify scan types (Sequential Scan, Index Scan, Bitmap Scan)
- Check join algorithms (Nested Loop, Hash Join, Merge Join)
- Analyze index usage and selectivity
- Find bottleneck operations (sorts, filters, aggregations)
- Understand cost estimates vs actual rows
- Check buffer usage and I/O patterns
3. Apply Optimizations
- Design appropriate indexes (B-tree, Hash, GiST, GIN)
- Rewrite inefficient queries (subqueries to JOINs, CTEs)
- Optimize join order and algorithms
- Use window functions for complex aggregations
- Leverage partial indexes and covering indexes
- Consider denormalization for read-heavy workloads
- Update table statistics (ANALYZE)
- Tune PostgreSQL configuration parameters
4. Validate Improvements
- Compare before/after metrics
- Test with production-like data
- Verify correctness
- Monitor after deployment
Best Practices
- Index Strategically: Index columns in WHERE, JOIN, ORDER BY
- Avoid SELECT *: Select only needed columns
- Use EXPLAIN ANALYZE: Always analyze execution plans with actual timing
- Limit Results: Use pagination for large datasets
- Avoid N+1: Use JOINs or batch queries
- Prefer EXISTS over IN: For subqueries with large result sets
- Update Statistics: Run ANALYZE after bulk operations
- Use CTEs for Readability: But watch for optimization fences
- Avoid Functions on Indexed Columns: Prevents index usage
- Monitor Continuously: Track query performance over time
PostgreSQL-Specific Optimizations
Execution Plan Operators
Scan Types:
- Sequential Scan: Full table scan (slow for large tables)
- Index Scan: Uses index + table lookups (good for low selectivity)
- Index Only Scan: Uses covering index (fastest)
- Bitmap Index Scan: Multiple index scans combined (good for OR conditions)
Join Algorithms:
- Nested Loop: Best for small tables or index lookups
- Hash Join: Best for medium-sized tables with equality joins
- Merge Join: Best for large pre-sorted tables
Statistics and Maintenance
-- Update table statistics for better query plans
ANALYZE table_name;
-- Check statistics freshness
SELECT schemaname, tablename, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY last_analyze NULLS FIRST;
-- Find bloated tables
SELECT schemaname, tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
n_dead_tup, n_live_tup,
round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
-- Vacuum bloated tables
VACUUM ANALYZE table_name;
Configuration Tuning
-- Key parameters to check
SHOW shared_buffers; -- Should be 25% of RAM
SHOW effective_cache_size; -- Should be 50-75% of RAM
SHOW work_mem; -- Per-operation memory
SHOW random_page_cost; -- Lower for SSDs (1.1-2.0)
Common Anti-Patterns
1. SELECT * in Application Code
-- BAD: Fetches unnecessary columns
SELECT * FROM users WHERE id = 1;
-- GOOD: Fetch only needed columns
SELECT id, email, name FROM users WHERE id = 1;
2. Implicit Type Conversion
-- BAD: Can't use index if id is integer
SELECT * FROM users WHERE id = '123';
-- GOOD: Match column type
SELECT * FROM users WHERE id = 123;
3. OR Conditions Without Indexes
-- BAD: May not use indexes efficiently
SELECT * FROM orders WHERE status = 'pending' OR status = 'processing';
-- GOOD: Use IN or create partial index
SELECT * FROM orders WHERE status IN ('pending', 'processing');
4. Correlated Subqueries
-- BAD: Executes subquery for each row
SELECT p.name,
(SELECT COUNT(*) FROM order_items WHERE product_id = p.id) AS order_count
FROM products p;
-- GOOD: Use JOIN with aggregation
SELECT p.name, COUNT(oi.id) AS order_count
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id, p.name;
5. Missing WHERE Clauses
-- BAD: Updates entire table
UPDATE products SET updated_at = NOW();
-- GOOD: Update only what changed
UPDATE products SET updated_at = NOW()
WHERE id IN (SELECT product_id FROM price_changes);
Advanced Patterns
CTEs (Common Table Expressions)
-- CTEs for readability and reusability
WITH recent_orders AS (
SELECT customer_id, COUNT(*) AS order_count, SUM(total) AS total_spent
FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY customer_id
),
high_value_customers AS (
SELECT customer_id
FROM recent_orders
WHERE total_spent > 1000
)
SELECT c.name, c.email, ro.order_count, ro.total_spent
FROM customers c
INNER JOIN high_value_customers hvc ON c.id = hvc.customer_id
INNER JOIN recent_orders ro ON c.id = ro.customer_id;
-- Recursive CTEs for hierarchical data
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 1 AS level
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY level, name;
Window Functions
-- Ranking and row numbers
SELECT
product_id,
category_id,
price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS price_rank,
RANK() OVER (ORDER BY price DESC) AS overall_rank,
DENSE_RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS dense_rank
FROM products;
-- Running totals and moving averages
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total,
AVG(revenue) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7day
FROM daily_sales
ORDER BY date;
-- Lead/Lag for time-series analysis
SELECT
customer_id,
order_date,
total,
LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS prev_order_date,
LEAD(total) OVER (PARTITION BY customer_id ORDER BY order_date) AS next_order_total,
total - LAG(total) OVER (PARTITION BY customer_id ORDER BY order_date) AS total_diff
FROM orders;
Examples
Example 1: Query Optimization with EXPLAIN
-- Original slow query
SELECT o.*, c.name, c.email
FROM orders o, customers c
WHERE o.customer_id = c.id
AND o.status = 'pending'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC;
-- Step 1: Analyze with EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.*, c.name, c.email
FROM orders o, customers c
WHERE o.customer_id = c.id
AND o.status = 'pending'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC;
-- Output analysis:
-- Seq Scan on orders (cost=0.00..15420.00 rows=50000)
-- Filter: (status = 'pending' AND created_at > '2024-01-01')
-- Rows Removed by Filter: 450000
-- Problem: Sequential scan on large table!
-- Step 2: Create composite index
CREATE INDEX idx_orders_status_created
ON orders(status, created_at DESC)
WHERE status IN ('pending', 'processing');
-- Step 3: Rewrite with explicit JOIN
SELECT o.id, o.total, o.created_at, c.name, c.email
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'pending'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 100;
-- After optimization:
-- Index Scan using idx_orders_status_created (cost=0.42..125.50 rows=100)
-- 99% reduction in query time!
Example 2: N+1 Query Problem
-- Problem: N+1 queries
-- Application code:
-- orders = SELECT * FROM orders WHERE user_id = 1
-- for order in orders:
-- items = SELECT * FROM order_items WHERE order_id = order.id
-- Solution: Single query with JOIN
SELECT
o.id AS order_id,
o.total,
o.created_at,
oi.product_id,
oi.quantity,
oi.unit_price,
p.name AS product_name
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.id
WHERE o.user_id = 1
ORDER BY o.created_at DESC, oi.id;
-- Alternative: Batch query
SELECT * FROM orders WHERE user_id = 1;
-- Get order IDs: [1, 2, 3, 4, 5]
SELECT * FROM order_items WHERE order_id IN (1, 2, 3, 4, 5);
Example 3: Index Design Strategies
-- Single column index for equality checks
CREATE INDEX idx_users_email ON users(email);
-- Composite index for multiple conditions
-- Order columns: equality first, then range, then sort
CREATE INDEX idx_orders_user_status_date
ON orders(user_id, status, created_at DESC);
-- Partial index for filtered queries
CREATE INDEX idx_orders_pending
ON orders(created_at DESC)
WHERE status = 'pending';
-- Covering index to avoid table lookups
CREATE INDEX idx_orders_summary
ON orders(user_id, status)
INCLUDE (total, created_at);
-- Expression index for computed conditions
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- Check existing indexes
SELECT
indexname,
indexdef,
pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = 'orders';
-- Find unused indexes
SELECT
schemaname, tablename, indexname,
idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
Example 4: Query Rewriting Patterns
-- Pattern 1: Replace subquery with JOIN
-- Before
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE region = 'US');
-- After
SELECT o.* FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'US';
-- Pattern 2: Use EXISTS instead of IN for large subqueries
-- Before
SELECT * FROM products
WHERE id IN (SELECT product_id FROM order_items);
-- After
SELECT * FROM products p
WHERE EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);
-- Pattern 3: Avoid functions on indexed columns
-- Before (can't use index)
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- After (uses index)
SELECT * FROM users
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- Pattern 4: Optimize pagination
-- Before (slow for large offsets)
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 10000;
-- After (keyset pagination)
SELECT * FROM products
WHERE id > 10000
ORDER BY id
LIMIT 20;
-- Pattern 5: Batch operations
-- Before (row-by-row)
UPDATE products SET price = price * 1.1 WHERE id = 1;
UPDATE products SET price = price * 1.1 WHERE id = 2;
-- ... repeated 1000 times
-- After (single batch)
UPDATE products SET price = price * 1.1
WHERE id = ANY(ARRAY[1, 2, 3, ..., 1000]);
-- Or use CTE for complex batches
WITH price_updates AS (
SELECT id, new_price FROM temp_price_updates
)
UPDATE products p
SET price = pu.new_price
FROM price_updates pu
WHERE p.id = pu.id;
1---2name: sql-optimization-33description: This skill focuses on analyzing and optimizing SQL queries for improved performance. It covers query analysis, index optimization, execution plan interpretation, query rewriting strategies, PostgreSQL-specific optimizations, and common anti-patterns. Use this skill for slow queries, N+1 problems, join optimization, index design, and database performance tuning.4---5
6---
7name: sql-optimization
8description: Analyzes and optimizes SQL queries for better performance, including index design, query rewriting, execution plan analysis, and database tuning. Covers PostgreSQL-specific optimizations, N+1 prevention, CTE/window function optimization, join strategies, and common anti-patterns. Trigger keywords: SQL, query optimization, EXPLAIN, EXPLAIN ANALYZE, index, slow query, execution plan, query plan, join optimization, subquery, CTE, common table expression, window function, partition, N+1, query cache, database performance, sequential scan, index scan, bitmap scan, nested loop, hash join, merge join, PostgreSQL, query tuning, table scan, cardinality, statistics, vacuum, analyze.
9allowed-tools: Read, Grep, Glob, Bash
10---
11
12# SQL Optimization
13
14## Overview
15
16This skill focuses on analyzing and optimizing SQL queries for improved performance. It covers query analysis, index optimization, execution plan interpretation, query rewriting strategies, PostgreSQL-specific optimizations, and common anti-patterns. Use this skill for slow queries, N+1 problems, join optimization, index design, and database performance tuning.
17
18## Instructions
19
20### 1. Analyze Query Performance
21
22- Identify slow queries from logs
23- Run EXPLAIN/EXPLAIN ANALYZE
24- Measure query execution time
25- Check resource utilization
26
27### 2. Understand Execution Plans
28
29- Identify scan types (Sequential Scan, Index Scan, Bitmap Scan)
30- Check join algorithms (Nested Loop, Hash Join, Merge Join)
31- Analyze index usage and selectivity
32- Find bottleneck operations (sorts, filters, aggregations)
33- Understand cost estimates vs actual rows
34- Check buffer usage and I/O patterns
35
36### 3. Apply Optimizations
37
38- Design appropriate indexes (B-tree, Hash, GiST, GIN)
39- Rewrite inefficient queries (subqueries to JOINs, CTEs)
40- Optimize join order and algorithms
41- Use window functions for complex aggregations
42- Leverage partial indexes and covering indexes
43- Consider denormalization for read-heavy workloads
44- Update table statistics (ANALYZE)
45- Tune PostgreSQL configuration parameters
46
47### 4. Validate Improvements
48
49- Compare before/after metrics
50- Test with production-like data
51- Verify correctness
52- Monitor after deployment
53
54## Best Practices
55
561. **Index Strategically**: Index columns in WHERE, JOIN, ORDER BY
572. **Avoid SELECT \***: Select only needed columns
583. **Use EXPLAIN ANALYZE**: Always analyze execution plans with actual timing
594. **Limit Results**: Use pagination for large datasets
605. **Avoid N+1**: Use JOINs or batch queries
616. **Prefer EXISTS over IN**: For subqueries with large result sets
627. **Update Statistics**: Run ANALYZE after bulk operations
638. **Use CTEs for Readability**: But watch for optimization fences
649. **Avoid Functions on Indexed Columns**: Prevents index usage
6510. **Monitor Continuously**: Track query performance over time
66
67## PostgreSQL-Specific Optimizations
68
69### Execution Plan Operators
70
71**Scan Types:**
72- **Sequential Scan**: Full table scan (slow for large tables)
73- **Index Scan**: Uses index + table lookups (good for low selectivity)
74- **Index Only Scan**: Uses covering index (fastest)
75- **Bitmap Index Scan**: Multiple index scans combined (good for OR conditions)
76
77**Join Algorithms:**
78- **Nested Loop**: Best for small tables or index lookups
79- **Hash Join**: Best for medium-sized tables with equality joins
80- **Merge Join**: Best for large pre-sorted tables
81
82### Statistics and Maintenance
83
84```sql
85-- Update table statistics for better query plans
86ANALYZE table_name;
87
88-- Check statistics freshness
89SELECT schemaname, tablename, last_analyze, last_autoanalyze
90FROM pg_stat_user_tables
91WHERE schemaname = 'public'
92ORDER BY last_analyze NULLS FIRST;
93
94-- Find bloated tables
95SELECT schemaname, tablename,
96 pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
97 n_dead_tup, n_live_tup,
98 round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct
99FROM pg_stat_user_tables
100WHERE n_dead_tup > 1000
101ORDER BY n_dead_tup DESC;
102
103-- Vacuum bloated tables
104VACUUM ANALYZE table_name;
105```
106
107### Configuration Tuning
108
109```sql
110-- Key parameters to check
111SHOW shared_buffers; -- Should be 25% of RAM
112SHOW effective_cache_size; -- Should be 50-75% of RAM
113SHOW work_mem; -- Per-operation memory
114SHOW random_page_cost; -- Lower for SSDs (1.1-2.0)
115```
116
117## Common Anti-Patterns
118
119### 1. SELECT * in Application Code
120```sql
121-- BAD: Fetches unnecessary columns
122SELECT * FROM users WHERE id = 1;
123
124-- GOOD: Fetch only needed columns
125SELECT id, email, name FROM users WHERE id = 1;
126```
127
128### 2. Implicit Type Conversion
129```sql
130-- BAD: Can't use index if id is integer
131SELECT * FROM users WHERE id = '123';
132
133-- GOOD: Match column type
134SELECT * FROM users WHERE id = 123;
135```
136
137### 3. OR Conditions Without Indexes
138```sql
139-- BAD: May not use indexes efficiently
140SELECT * FROM orders WHERE status = 'pending' OR status = 'processing';
141
142-- GOOD: Use IN or create partial index
143SELECT * FROM orders WHERE status IN ('pending', 'processing');
144```
145
146### 4. Correlated Subqueries
147```sql
148-- BAD: Executes subquery for each row
149SELECT p.name,
150 (SELECT COUNT(*) FROM order_items WHERE product_id = p.id) AS order_count
151FROM products p;
152
153-- GOOD: Use JOIN with aggregation
154SELECT p.name, COUNT(oi.id) AS order_count
155FROM products p
156LEFT JOIN order_items oi ON oi.product_id = p.id
157GROUP BY p.id, p.name;
158```
159
160### 5. Missing WHERE Clauses
161```sql
162-- BAD: Updates entire table
163UPDATE products SET updated_at = NOW();
164
165-- GOOD: Update only what changed
166UPDATE products SET updated_at = NOW()
167WHERE id IN (SELECT product_id FROM price_changes);
168```
169
170## Advanced Patterns
171
172### CTEs (Common Table Expressions)
173
174```sql
175-- CTEs for readability and reusability
176WITH recent_orders AS (
177 SELECT customer_id, COUNT(*) AS order_count, SUM(total) AS total_spent
178 FROM orders
179 WHERE created_at > NOW() - INTERVAL '30 days'
180 GROUP BY customer_id
181),
182high_value_customers AS (
183 SELECT customer_id
184 FROM recent_orders
185 WHERE total_spent > 1000
186)
187SELECT c.name, c.email, ro.order_count, ro.total_spent
188FROM customers c
189INNER JOIN high_value_customers hvc ON c.id = hvc.customer_id
190INNER JOIN recent_orders ro ON c.id = ro.customer_id;
191
192-- Recursive CTEs for hierarchical data
193WITH RECURSIVE category_tree AS (
194 SELECT id, name, parent_id, 1 AS level
195 FROM categories
196 WHERE parent_id IS NULL
197 UNION ALL
198 SELECT c.id, c.name, c.parent_id, ct.level + 1
199 FROM categories c
200 INNER JOIN category_tree ct ON c.parent_id = ct.id
201)
202SELECT * FROM category_tree ORDER BY level, name;
203```
204
205### Window Functions
206
207```sql
208-- Ranking and row numbers
209SELECT
210 product_id,
211 category_id,
212 price,
213 ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS price_rank,
214 RANK() OVER (ORDER BY price DESC) AS overall_rank,
215 DENSE_RANK() OVER (PARTITION BY category_id ORDER BY price DESC) AS dense_rank
216FROM products;
217
218-- Running totals and moving averages
219SELECT
220 date,
221 revenue,
222 SUM(revenue) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total,
223 AVG(revenue) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7day
224FROM daily_sales
225ORDER BY date;
226
227-- Lead/Lag for time-series analysis
228SELECT
229 customer_id,
230 order_date,
231 total,
232 LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) AS prev_order_date,
233 LEAD(total) OVER (PARTITION BY customer_id ORDER BY order_date) AS next_order_total,
234 total - LAG(total) OVER (PARTITION BY customer_id ORDER BY order_date) AS total_diff
235FROM orders;
236```
237
238## Examples
239
240### Example 1: Query Optimization with EXPLAIN
241
242```sql
243-- Original slow query
244SELECT o.*, c.name, c.email
245FROM orders o, customers c
246WHERE o.customer_id = c.id
247AND o.status = 'pending'
248AND o.created_at > '2024-01-01'
249ORDER BY o.created_at DESC;
250
251-- Step 1: Analyze with EXPLAIN ANALYZE
252EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
253SELECT o.*, c.name, c.email
254FROM orders o, customers c
255WHERE o.customer_id = c.id
256AND o.status = 'pending'
257AND o.created_at > '2024-01-01'
258ORDER BY o.created_at DESC;
259
260-- Output analysis:
261-- Seq Scan on orders (cost=0.00..15420.00 rows=50000)
262-- Filter: (status = 'pending' AND created_at > '2024-01-01')
263-- Rows Removed by Filter: 450000
264-- Problem: Sequential scan on large table!
265
266-- Step 2: Create composite index
267CREATE INDEX idx_orders_status_created
268ON orders(status, created_at DESC)
269WHERE status IN ('pending', 'processing');
270
271-- Step 3: Rewrite with explicit JOIN
272SELECT o.id, o.total, o.created_at, c.name, c.email
273FROM orders o
274INNER JOIN customers c ON o.customer_id = c.id
275WHERE o.status = 'pending'
276AND o.created_at > '2024-01-01'
277ORDER BY o.created_at DESC
278LIMIT 100;
279
280-- After optimization:
281-- Index Scan using idx_orders_status_created (cost=0.42..125.50 rows=100)
282-- 99% reduction in query time!
283```
284
285### Example 2: N+1 Query Problem
286
287```sql
288-- Problem: N+1 queries
289-- Application code:
290-- orders = SELECT * FROM orders WHERE user_id = 1
291-- for order in orders:
292-- items = SELECT * FROM order_items WHERE order_id = order.id
293
294-- Solution: Single query with JOIN
295SELECT
296 o.id AS order_id,
297 o.total,
298 o.created_at,
299 oi.product_id,
300 oi.quantity,
301 oi.unit_price,
302 p.name AS product_name
303FROM orders o
304LEFT JOIN order_items oi ON o.id = oi.order_id
305LEFT JOIN products p ON oi.product_id = p.id
306WHERE o.user_id = 1
307ORDER BY o.created_at DESC, oi.id;
308
309-- Alternative: Batch query
310SELECT * FROM orders WHERE user_id = 1;
311-- Get order IDs: [1, 2, 3, 4, 5]
312SELECT * FROM order_items WHERE order_id IN (1, 2, 3, 4, 5);
313```
314
315### Example 3: Index Design Strategies
316
317```sql
318-- Single column index for equality checks
319CREATE INDEX idx_users_email ON users(email);
320
321-- Composite index for multiple conditions
322-- Order columns: equality first, then range, then sort
323CREATE INDEX idx_orders_user_status_date
324ON orders(user_id, status, created_at DESC);
325
326-- Partial index for filtered queries
327CREATE INDEX idx_orders_pending
328ON orders(created_at DESC)
329WHERE status = 'pending';
330
331-- Covering index to avoid table lookups
332CREATE INDEX idx_orders_summary
333ON orders(user_id, status)
334INCLUDE (total, created_at);
335
336-- Expression index for computed conditions
337CREATE INDEX idx_users_email_lower ON users(LOWER(email));
338
339-- Check existing indexes
340SELECT
341 indexname,
342 indexdef,
343 pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
344FROM pg_indexes
345WHERE tablename = 'orders';
346
347-- Find unused indexes
348SELECT
349 schemaname, tablename, indexname,
350 idx_scan, idx_tup_read, idx_tup_fetch
351FROM pg_stat_user_indexes
352WHERE idx_scan = 0
353ORDER BY pg_relation_size(indexrelid) DESC;
354```
355
356### Example 4: Query Rewriting Patterns
357
358```sql
359-- Pattern 1: Replace subquery with JOIN
360-- Before
361SELECT * FROM orders
362WHERE customer_id IN (SELECT id FROM customers WHERE region = 'US');
363
364-- After
365SELECT o.* FROM orders o
366INNER JOIN customers c ON o.customer_id = c.id
367WHERE c.region = 'US';
368
369-- Pattern 2: Use EXISTS instead of IN for large subqueries
370-- Before
371SELECT * FROM products
372WHERE id IN (SELECT product_id FROM order_items);
373
374-- After
375SELECT * FROM products p
376WHERE EXISTS (
377 SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
378);
379
380-- Pattern 3: Avoid functions on indexed columns
381-- Before (can't use index)
382SELECT * FROM users WHERE YEAR(created_at) = 2024;
383
384-- After (uses index)
385SELECT * FROM users
386WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
387
388-- Pattern 4: Optimize pagination
389-- Before (slow for large offsets)
390SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 10000;
391
392-- After (keyset pagination)
393SELECT * FROM products
394WHERE id > 10000
395ORDER BY id
396LIMIT 20;
397
398-- Pattern 5: Batch operations
399-- Before (row-by-row)
400UPDATE products SET price = price * 1.1 WHERE id = 1;
401UPDATE products SET price = price * 1.1 WHERE id = 2;
402-- ... repeated 1000 times
403
404-- After (single batch)
405UPDATE products SET price = price * 1.1
406WHERE id = ANY(ARRAY[1, 2, 3, ..., 1000]);
407
408-- Or use CTE for complex batches
409WITH price_updates AS (
410 SELECT id, new_price FROM temp_price_updates
411)
412UPDATE products p
413SET price = pu.new_price
414FROM price_updates pu
415WHERE p.id = pu.id;
416```