| name | query-optimizer |
| description | Analyze and optimize SQL queries for better performance and efficiency. |
Query Optimizer Skill
Analyze and optimize SQL queries for better performance and efficiency.
Instructions
You are a database performance optimization expert. When invoked:
-
Analyze Query Performance:
- Use EXPLAIN/EXPLAIN ANALYZE to understand execution plan
- Identify slow queries from logs
- Measure query execution time
- Detect full table scans and missing indexes
-
Identify Bottlenecks:
- Find N+1 query problems
- Detect inefficient JOINs
- Identify missing or unused indexes
- Spot suboptimal WHERE clauses
-
Optimize Queries:
- Add appropriate indexes
- Rewrite queries for better performance
- Suggest caching strategies
- Recommend query restructuring
-
Provide Recommendations:
- Index creation suggestions
- Query rewriting alternatives
- Database configuration tuning
- Monitoring and alerting setup
Supported Databases
- SQL: PostgreSQL, MySQL, MariaDB, SQL Server, SQLite
- Analysis Tools: EXPLAIN, EXPLAIN ANALYZE, Query Profiler
- Monitoring: pg_stat_statements, slow query log, performance schema
Usage Examples
@query-optimizer
@query-optimizer --analyze-slow-queries
@query-optimizer --suggest-indexes
@query-optimizer --explain SELECT * FROM users WHERE email = 'test@example.com'
@query-optimizer --fix-n-plus-one
Query Analysis Tools
PostgreSQL - EXPLAIN ANALYZE
EXPLAIN
SELECT u.id, u.username, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.username;
EXPLAIN ANALYZE
SELECT u.id, u.username, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.username;
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON)
SELECT * FROM orders
WHERE user_id = 123
AND created_at >= '2024-01-01';
Reading EXPLAIN Output:
Seq Scan on users (cost=0.00..1234.56 rows=10000 width=32)
Filter: (active = true)
-- Seq Scan = Sequential Scan (full table scan) - BAD for large tables
-- cost=0.00..1234.56 = startup cost..total cost
-- rows=10000 = estimated rows
-- width=32 = average row size in bytes
Index Scan using idx_users_email on users (cost=0.29..8.30 rows=1 width=32)
Index Cond: (email = 'test@example.com'::text)
-- Index Scan = Using index - GOOD
-- Much lower cost than Seq Scan
-- rows=1 = accurate estimate
MySQL - EXPLAIN
EXPLAIN
SELECT u.id, u.username, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.username;
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 123;
EXPLAIN
SELECT * FROM users WHERE email = 'test@example.com';
SHOW WARNINGS;
MySQL EXPLAIN Output:
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
| 1 | SIMPLE | users | ALL | NULL | NULL | NULL | NULL | 1000 | Using where |
+----+-------------+-------+------+---------------+---------+---------+-------+------+-------------+
-- type=ALL means full table scan - BAD
-- key=NULL means no index used
+----+-------------+-------+------+---------------+----------------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+----------------+---------+-------+------+-------+
| 1 | SIMPLE | users | ref | idx_users_email| idx_users_email| 767 | const | 1 | NULL |
+----+-------------+-------+------+---------------+----------------+---------+-------+------+-------+
-- type=ref means index lookup - GOOD
-- key shows index being used
Common Performance Issues
1. Missing Indexes
Problem:
SELECT * FROM users WHERE email = 'john@example.com';
Solution:
CREATE INDEX idx_users_email ON users(email);
2. N+1 Query Problem
Problem:
const users = await User.findAll();
for (const user of users) {
const orders = await Order.findAll({
where: { userId: user.id }
});
console.log(`${user.name}: ${orders.length} orders`);
}
Solution:
const users = await User.findAll({
include: [{
model: Order,
attributes: ['id', 'total_amount']
}]
});
for (const user of users) {
console.log(`${user.name}: ${user.orders.length} orders`);
}
SQL Equivalent:
SELECT * FROM users;
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 2;
SELECT
u.id,
u.name,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
3. SELECT * Inefficiency
Problem:
SELECT * FROM products
WHERE category_id = 5;
Solution:
SELECT id, name, price, stock
FROM products
WHERE category_id = 5;
4. Inefficient Pagination
Problem:
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 10000;
Solution:
SELECT * FROM users
WHERE created_at < '2024-01-01 12:00:00'
AND (created_at < '2024-01-01 12:00:00' OR id < 12345)
ORDER BY created_at DESC, id DESC
LIMIT 20;
SELECT * FROM users
WHERE id < 10000
ORDER BY id DESC
LIMIT 20;
5. Function on Indexed Column
Problem:
SELECT * FROM users
WHERE LOWER(email) = 'john@example.com';
Solution 1 - Store lowercase:
ALTER TABLE users ADD COLUMN email_lower VARCHAR(255)
GENERATED ALWAYS AS (LOWER(email)) STORED;
CREATE INDEX idx_users_email_lower ON users(email_lower);
SELECT * FROM users
WHERE email_lower = 'john@example.com';
Solution 2 - Functional index (PostgreSQL):
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
SELECT * FROM users
WHERE LOWER(email) = 'john@example.com';
Solution 3 - Case-insensitive collation:
ALTER TABLE users ALTER COLUMN email TYPE citext;
SELECT * FROM users WHERE email = 'john@example.com';
6. Inefficient JOINs
Problem:
SELECT
u.username,
o.id as order_id,
p.name as product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE u.email = 'john@example.com';
Solution:
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
7. OR Conditions
Problem:
SELECT * FROM users
WHERE username = 'john' OR email = 'john@example.com';
Solution:
SELECT * FROM users WHERE username = 'john'
UNION
SELECT * FROM users WHERE email = 'john@example.com';
8. NOT IN with Subquery
Problem:
SELECT * FROM users
WHERE id NOT IN (
SELECT user_id FROM banned_users
);
Solution:
SELECT u.*
FROM users u
LEFT JOIN banned_users bu ON u.id = bu.user_id
WHERE bu.user_id IS NULL;
SELECT u.*
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM banned_users bu
WHERE bu.user_id = u.id
);
Index Optimization
When to Add Indexes
Add indexes for:
- Primary keys (automatic in most databases)
- Foreign keys (critical for JOINs)
- Columns in WHERE clauses
- Columns in ORDER BY clauses
- Columns in GROUP BY clauses
- Columns in JOIN conditions
- Columns with high cardinality (many unique values)
Index Types
B-Tree Index (Default):
CREATE INDEX idx_users_created_at ON users(created_at);
SELECT * FROM users WHERE created_at > '2024-01-01';
SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
Composite Index:
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
SELECT * FROM orders WHERE user_id = 123;
SELECT * FROM orders WHERE status = 'pending';
Partial Index (PostgreSQL):
CREATE INDEX idx_active_users ON users(email)
WHERE active = true;
SELECT * FROM users WHERE email = 'john@example.com' AND active = true;
GIN Index (PostgreSQL - for arrays, JSONB, full-text):
CREATE INDEX idx_products_metadata ON products USING GIN(metadata);
SELECT * FROM products
WHERE metadata @> '{"brand": "Apple"}';
CREATE INDEX idx_tags ON posts USING GIN(tags);
SELECT * FROM posts WHERE tags @> ARRAY['postgresql'];
Full-Text Search Index:
CREATE INDEX idx_products_search ON products
USING GIN(to_tsvector('english', name || ' ' || description));
SELECT * FROM products
WHERE to_tsvector('english', name || ' ' || description)
@@ to_tsquery('english', 'laptop & gaming');
Covering Index
Concept:
CREATE INDEX idx_users_email_username ON users(email, username);
SELECT username FROM users WHERE email = 'john@example.com';
With INCLUDE (PostgreSQL 11+):
CREATE INDEX idx_users_email ON users(email)
INCLUDE (username, created_at);
SELECT username, created_at
FROM users
WHERE email = 'john@example.com';
Index Maintenance
Find Unused Indexes (PostgreSQL):
SELECT
schemaname,
tablename,
indexname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;
Find Duplicate Indexes:
SELECT
indrelid::regclass AS table_name,
array_agg(indexrelid::regclass) AS indexes
FROM pg_index
GROUP BY indrelid, indkey
HAVING COUNT(*) > 1;
Rebuild Fragmented Indexes:
REINDEX INDEX idx_users_email;
REINDEX TABLE users;
OPTIMIZE TABLE users;
Query Rewriting Examples
Example 1: Aggregation Optimization
Before:
SELECT
u.id,
u.username,
(SELECT COUNT(*) FROM orders WHERE user_id = u.id) as order_count,
(SELECT SUM(total_amount) FROM orders WHERE user_id = u.id) as total_spent
FROM users u
WHERE u.active = true;
After:
SELECT
u.id,
u.username,
COUNT(o.id) as order_count,
COALESCE(SUM(o.total_amount), 0) as total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.username;
Example 2: EXISTS vs IN
Before:
SELECT * FROM products
WHERE id IN (
SELECT product_id FROM order_items
WHERE created_at >= '2024-01-01'
);
After:
SELECT p.* FROM products p
WHERE EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.product_id = p.id
AND oi.created_at >= '2024-01-01'
);
Example 3: Avoid Cartesian Products
Before:
SELECT *
FROM users u, orders o
WHERE u.active = true
AND o.status = 'completed';
After:
SELECT u.*, o.*
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.active = true
AND o.status = 'completed';
Example 4: Optimize DISTINCT
Before:
SELECT DISTINCT user_id
FROM orders
WHERE status = 'completed';
After:
SELECT user_id
FROM orders
WHERE status = 'completed'
GROUP BY user_id;
SELECT DISTINCT ON (user_id) user_id, created_at
FROM orders
WHERE status = 'completed'
ORDER BY user_id, created_at DESC;
Monitoring Slow Queries
PostgreSQL - pg_stat_statements
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT
substring(query, 1, 50) AS short_query,
round(total_exec_time::numeric, 2) AS total_time,
calls,
round(mean_exec_time::numeric, 2) AS mean_time,
round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 2) AS percentage
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
SELECT
substring(query, 1, 50) AS short_query,
calls,
round(mean_exec_time::numeric, 2) AS mean_time
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 10;
SELECT pg_stat_statements_reset();
MySQL - Slow Query Log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
SET GLOBAL log_queries_not_using_indexes = 'ON';
Performance Schema (MySQL)
SET GLOBAL performance_schema = ON;
SELECT
DIGEST_TEXT,
COUNT_STAR AS executions,
ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms,
ROUND(SUM_TIMER_WAIT / 1000000000, 2) AS total_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
Best Practices
DO ✓
- Use EXPLAIN before and after optimization
- Add indexes on foreign keys - Critical for JOINs
- Use covering indexes when possible
- Paginate large result sets - Avoid loading all data
- Monitor query performance - Use pg_stat_statements or slow query log
- Test on production-like data - Performance differs with data volume
- Use connection pooling - Reduce connection overhead
- Cache frequently accessed data - Redis, Memcached
- Archive old data - Keep active tables smaller
- Regular VACUUM/ANALYZE (PostgreSQL) - Update statistics
DON'T ✗
- **Don't use SELECT *** - Fetch only needed columns
- Don't over-index - Each index slows down writes
- Don't ignore EXPLAIN warnings - They indicate problems
- Don't use functions on indexed columns - Prevents index usage
- Don't fetch more data than needed - Use LIMIT
- Don't use OFFSET for deep pagination - Use cursor-based instead
- Don't ignore database logs - Monitor for errors
- Don't optimize prematurely - Profile first, optimize bottlenecks
- Don't forget about write performance - Indexes slow down INSERTs
- Don't skip testing - Verify optimizations actually help
Query Optimization Checklist
## Query Optimization Checklist
- [ ] Run EXPLAIN/EXPLAIN ANALYZE on query
- [ ] Check if query uses indexes (no Seq Scan on large tables)
- [ ] Verify indexes exist on:
- [ ] Foreign key columns
- [ ] WHERE clause columns
- [ ] JOIN condition columns
- [ ] ORDER BY columns
- [ ] SELECT only needed columns (avoid SELECT *)
- [ ] Use appropriate JOIN type (INNER vs LEFT)
- [ ] Avoid N+1 queries (use JOINs or eager loading)
- [ ] Use pagination for large result sets
- [ ] Check for unused indexes (slow down writes)
- [ ] Consider query caching for frequent queries
- [ ] Test with production-like data volumes
- [ ] Monitor query performance over time
Notes
- Always measure before and after optimization
- Index creation can take time on large tables
- Too many indexes slow down INSERT/UPDATE/DELETE
- Keep database statistics up to date (ANALYZE)
- Consider read replicas for read-heavy workloads
- Use database-specific features when beneficial
- Document optimization decisions for team
- Regular performance audits prevent degradation