| name | Query Optimization |
| description | Techniques for optimizing database queries, indexing strategies, and performance tuning for SQL databases |
Query Optimization
You are a database performance expert specializing in query optimization, indexing strategies, and database performance tuning. You help identify and resolve performance bottlenecks in database queries and operations.
Understanding Query Execution
1. Reading EXPLAIN Plans
PostgreSQL EXPLAIN:
EXPLAIN SELECT * FROM orders WHERE customer_id = 123;
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123;
EXPLAIN (FORMAT JSON) SELECT * FROM orders WHERE customer_id = 123;
Key Metrics to Watch:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT o.*, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2024-01-01'
AND o.status = 'completed';
Interpreting Costs:
Cost Structure: cost=0.42..8.44 rows=1 width=136
- First number (0.42): Startup cost
- Second number (8.44): Total cost
- rows: Estimated rows returned
- width: Average row size in bytes
LOWER COST = BETTER PERFORMANCE
2. Query Execution Order
SQL Execution Flow:
SELECT customer_name, SUM(total) as revenue
FROM orders
WHERE status = 'completed'
GROUP BY customer_name
HAVING SUM(total) > 1000
ORDER BY revenue DESC
LIMIT 10;
Indexing Strategies
1. When to Create Indexes
Good Index Candidates:
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_sales_product_id ON sales(product_id);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
Bad Index Candidates:
2. Composite Indexes
Column Order Matters:
CREATE INDEX idx_orders_status_customer ON orders(status, customer_id);
SELECT * FROM orders WHERE status = 'pending';
SELECT * FROM orders WHERE status = 'pending' AND customer_id = 123;
SELECT * FROM orders WHERE customer_id = 123;
Optimal Composite Index Strategy:
SELECT * FROM orders
WHERE status = 'pending'
AND created_at > '2024-01-01'
ORDER BY created_at DESC;
CREATE INDEX idx_orders_status_created_at ON orders(status, created_at DESC);
3. Partial Indexes
Index Only What You Need:
CREATE INDEX idx_active_orders ON orders(created_at)
WHERE status IN ('pending', 'processing');
CREATE INDEX idx_recent_orders ON orders(customer_id)
WHERE created_at > '2024-01-01';
4. Covering Indexes
Include All Required Columns:
SELECT order_id, customer_id, total
FROM orders
WHERE status = 'completed'
AND created_at > '2024-01-01';
CREATE INDEX idx_orders_covering ON orders(status, created_at)
INCLUDE (order_id, customer_id, total);
Query Optimization Techniques
1. Avoid SELECT *
Problem:
SELECT * FROM orders WHERE customer_id = 123;
Solution:
SELECT order_id, total, created_at FROM orders WHERE customer_id = 123;
2. Filter Early, Filter Often
Suboptimal:
SELECT o.order_id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2024-01-01';
Optimized:
SELECT o.order_id, c.name
FROM (
SELECT order_id, customer_id
FROM orders
WHERE created_at > '2024-01-01'
) o
JOIN customers c ON o.customer_id = c.id;
WITH recent_orders AS (
SELECT order_id, customer_id
FROM orders
WHERE created_at > '2024-01-01'
)
SELECT o.order_id, c.name
FROM recent_orders o
JOIN customers c ON o.customer_id = c.id;
3. Optimize JOINs
JOIN Order Matters:
EXPLAIN ANALYZE
SELECT o.*, p.name, c.name
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2024-01-01'
AND p.category = 'electronics';
Avoid Implicit Cross Joins:
SELECT *
FROM orders o, customers c
WHERE o.customer_id = c.id;
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.id;
4. Use EXISTS Instead of IN for Large Datasets
Suboptimal:
SELECT *
FROM customers
WHERE customer_id IN (
SELECT customer_id FROM orders WHERE total > 1000
);
Optimized:
SELECT *
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id
AND o.total > 1000
);
5. Avoid Functions on Indexed Columns
Prevents Index Usage:
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
Index-Friendly:
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01';
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
The N+1 Query Problem
1. Identifying N+1 Queries
Problem Pattern:
orders = Order.objects.all()
for order in orders:
print(order.customer.name)
Detection:
from django.db import connection
from django.test.utils import override_settings
@override_settings(DEBUG=True)
def detect_n_plus_one():
queries_before = len(connection.queries)
orders = Order.objects.all()
for order in orders:
_ = order.customer.name
queries_after = len(connection.queries)
query_count = queries_after - queries_before
if query_count > 10:
print(f"WARNING: Possible N+1 problem! {query_count} queries executed")
2. Solutions
Eager Loading (Prefetch):
orders = Order.objects.select_related('customer').all()
for order in orders:
print(order.customer.name)
orders = Order.objects.prefetch_related('items').all()
for order in orders:
print([item.name for item in order.items.all()])
SQL Equivalent:
SELECT o.*, c.*
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id;
Caching Strategies
1. Query Result Caching
import redis
import json
from functools import wraps
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def cache_query(timeout=300):
"""Cache query results in Redis"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = f"query:{func.__name__}:{hash(str(args) + str(kwargs))}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
result = func(*args, **kwargs)
redis_client.setex(
cache_key,
timeout,
json.dumps(result)
)
return result
return wrapper
return decorator
@cache_query(timeout=600)
def get_popular_products():
return Product.objects.filter(
sales_count__gt=1000
).values_list('id', 'name', 'price')
2. Database Query Cache
CREATE MATERIALIZED VIEW sales_summary AS
SELECT
product_id,
DATE_TRUNC('day', created_at) as day,
COUNT(*) as order_count,
SUM(total) as revenue
FROM orders
GROUP BY product_id, DATE_TRUNC('day', created_at);
REFRESH MATERIALIZED VIEW sales_summary;
SELECT * FROM sales_summary WHERE day = '2024-01-01';
Connection Pooling
1. Why Connection Pooling Matters
import psycopg2
def bad_approach():
conn = psycopg2.connect(
dbname="mydb",
user="user",
password="password",
host="localhost"
)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
result = cursor.fetchall()
conn.close()
return result
from psycopg2 import pool
connection_pool = pool.SimpleConnectionPool(
minconn=1,
maxconn=10,
dbname="mydb",
user="user",
password="password",
host="localhost"
)
def good_approach():
conn = connection_pool.getconn()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
result = cursor.fetchall()
connection_pool.putconn(conn)
return result
2. Optimal Pool Settings
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'CONN_MAX_AGE': 600,
'OPTIONS': {
'connect_timeout': 10,
'options': '-c statement_timeout=30000'
},
}
}
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
engine = create_engine(
'postgresql://user:password@localhost/mydb',
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_timeout=30,
pool_recycle=3600,
pool_pre_ping=True,
)
Batch Operations
1. Batch Inserts
Slow:
for item in items:
cursor.execute(
"INSERT INTO products (name, price) VALUES (%s, %s)",
(item.name, item.price)
)
Fast:
values = [(item.name, item.price) for item in items]
from psycopg2.extras import execute_values
execute_values(
cursor,
"INSERT INTO products (name, price) VALUES %s",
values
)
Product.objects.bulk_create([
Product(name=item.name, price=item.price)
for item in items
], batch_size=1000)
2. Batch Updates
from django.db import transaction
with transaction.atomic():
for product in products_to_update:
product.price *= 1.1
Product.objects.bulk_update(products_to_update, ['price'], batch_size=500)
Advanced Optimization Techniques
1. Partitioning
Range Partitioning (PostgreSQL 10+):
CREATE TABLE orders (
order_id BIGSERIAL,
customer_id INTEGER,
created_at DATE NOT NULL,
total DECIMAL(10,2)
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2024_q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_2024_q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
SELECT * FROM orders WHERE created_at = '2024-02-15';
2. Denormalization
When Reads >> Writes:
SELECT o.order_id, c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id;
ALTER TABLE orders
ADD COLUMN customer_name VARCHAR(255),
ADD COLUMN customer_email VARCHAR(255);
CREATE OR REPLACE FUNCTION update_customer_info()
RETURNS TRIGGER AS $$
BEGIN
SELECT name, email INTO NEW.customer_name, NEW.customer_email
FROM customers WHERE customer_id = NEW.customer_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_order_customer
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION update_customer_info();
SELECT order_id, customer_name, customer_email FROM orders;
3. Database-Specific Optimizations
PostgreSQL:
CREATE TABLE events (
event_id SERIAL PRIMARY KEY,
event_data JSONB
);
CREATE INDEX idx_events_data ON events USING GIN (event_data);
SELECT * FROM events WHERE event_data @> '{"user_id": 123}';
MySQL:
ALTER TABLE orders
ADD INDEX idx_covering (customer_id, status, created_at, total);
SELECT * FROM orders
FORCE INDEX (idx_customer_status)
WHERE customer_id = 123 AND status = 'pending';
Performance Monitoring
1. Slow Query Log
PostgreSQL Configuration:
ALTER SYSTEM SET log_min_duration_statement = 1000;
ALTER SYSTEM SET log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h ';
SELECT pg_reload_conf();
SELECT
query,
calls,
total_time,
mean_time,
max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
2. Query Performance Metrics
import time
from contextlib import contextmanager
@contextmanager
def query_timer(query_name):
"""Measure query execution time"""
start = time.time()
try:
yield
finally:
duration = time.time() - start
if duration > 1.0:
print(f"SLOW QUERY [{query_name}]: {duration:.2f}s")
with query_timer("fetch_user_orders"):
orders = Order.objects.filter(customer_id=123).all()
Optimization Checklist
Before Deploying:
Red Flags:
- ⚠️ Queries taking > 1 second
- ⚠️ Full table scans on large tables
- ⚠️ Missing indexes on foreign keys
- ⚠️ DISTINCT or GROUP BY without indexes
- ⚠️ Subqueries in SELECT clause
- ⚠️ Cartesian products (cross joins)
- ⚠️ OR clauses (consider UNION instead)
- ⚠️ Functions on indexed columns
Related Skills
- Database Design: Schema design and normalization
- Index Management: Advanced indexing techniques
- SQL Fundamentals: Core SQL knowledge
- ORM Usage: Efficient ORM query patterns
- Caching: Application-level caching strategies
- Monitoring: Database performance monitoring
- Scaling: Database scaling and sharding