By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
Hyper-practical guide for debugging slow queries, optimizing performance, and acing hands-on SQL exams
You’re a data analyst at a mid-sized e-commerce company. Your dashboard, which used to load in 2 seconds, now takes 30 seconds—and your boss is breathing down your neck. The query looks fine, but PostgreSQL (or MySQL, or SQL Server) is choking on it. How do you fix it?
This is where EXPLAIN and EXPLAIN ANALYZE come in. They’re your X-ray machine for SQL queries—letting you see exactly how the database executes a query, where it’s wasting time, and how to optimize it.
EXPLAIN
EXPLAIN ANALYZE
Why this matters in production:- Slow queries = unhappy users. A 5-second query run 10,000 times/day costs CPU, memory, and money (especially in cloud databases like AWS RDS or Google BigQuery).- Cost-based optimization (CBO) is how modern databases automatically pick the fastest way to run a query—but it only works if you feed it the right data (indexes, statistics, etc.).- Ignoring EXPLAIN = flying blind. You’ll waste hours tweaking queries that look slow but are actually bottlenecked by missing indexes, full table scans, or inefficient joins.
Real-world scenario:You inherit a legacy SQL query that aggregates sales data. It runs in 12 minutes on a 10GB table. Your boss wants it under 30 seconds. Without EXPLAIN, you’re guessing. With it, you’ll see the bottleneck in 60 seconds (e.g., a missing index on order_date or a nested loop join killing performance).
order_date
sql EXPLAIN SELECT * FROM orders WHERE customer_id = 100;
Seq Scan on orders (cost=0.00..15.00 rows=5 width=36) Filter: (customer_id = 100)
Seq Scan
orders
sql EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 100;
Seq Scan on orders (cost=0.00..15.00 rows=5 width=36) (actual time=0.123..1.456 rows=3 loops=1) Filter: (customer_id = 100) Rows Removed by Filter: 999997 Planning Time: 0.059 ms Execution Time: 1.478 ms
Rows Removed by Filter: 999997
customer_id
sql EXPLAIN SELECT * FROM orders WHERE order_date > '2023-01-01';
Bitmap Heap Scan on orders (cost=12.34..56.78 rows=1000 width=36) Recheck Cond: (order_date > '2023-01-01'::date) -> Bitmap Index Scan on idx_orders_date (cost=0.00..12.09 rows=1000 width=0) Index Cond: (order_date > '2023-01-01'::date)
Bitmap Index Scan
WHERE id = 100
WHERE date BETWEEN ...
Hash Join
Merge Join
ORDER BY
GROUP BY
COUNT()
-- MySQL ANALYZE TABLE orders;
-- SQL Server UPDATE STATISTICS orders; ```
gender
-- MySQL: Force an index SELECT * FROM orders FORCE INDEX (idx_customer_id) WHERE customer_id = 100; ```
generate_series
SELECT
JOIN
Scenario:You have a query that aggregates daily sales:
SELECT date_trunc('day', order_date) AS day, COUNT(*) AS orders, SUM(amount) AS revenue FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31' GROUP BY day ORDER BY day;
It takes 15 seconds to run. Your goal: Get it under 1 second.
EXPLAIN SELECT date_trunc('day', order_date) AS day, COUNT(*) AS orders, SUM(amount) AS revenue FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31' GROUP BY day ORDER BY day;
Expected output (PostgreSQL):
Sort (cost=1234.56..1234.57 rows=1 width=24) Sort Key: (date_trunc('day'::text, order_date)) -> HashAggregate (cost=1234.00..1234.50 rows=1 width=24) Group Key: (date_trunc('day'::text, order_date)) -> Seq Scan on orders (cost=0.00..1000.00 rows=10000 width=12) Filter: ((order_date >= '2023-01-01'::date) AND (order_date <= '2023-12-31'::date))
⚠️ Red flags:1. Seq Scan = full table scan (slow on large tables).2. Filter = no index used for order_date.
Filter
EXPLAIN ANALYZE SELECT date_trunc('day', order_date) AS day, COUNT(*) AS orders, SUM(amount) AS revenue FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31' GROUP BY day ORDER BY day;
Expected output:
Sort (cost=1234.56..1234.57 rows=1 width=24) (actual time=14567.890..14567.891 rows=365 loops=1) Sort Key: (date_trunc('day'::text, order_date)) Sort Method: external merge Disk: 24kB -> HashAggregate (cost=1234.00..1234.50 rows=1 width=24) (actual time=14567.800..14567.850 rows=365 loops=1) Group Key: (date_trunc('day'::text, order_date)) -> Seq Scan on orders (cost=0.00..1000.00 rows=10000 width=12) (actual time=0.123..12345.678 rows=1000000 loops=1) Filter: ((order_date >= '2023-01-01'::date) AND (order_date <= '2023-12-31'::date)) Rows Removed by Filter: 0 Planning Time: 0.123 ms Execution Time: 14568.012 ms
⚠️ Key insights:1. actual time=14567.890..14567.891 = 14.5 seconds spent sorting.2. Seq Scan took 12.3 seconds and scanned 1M rows.3. Rows Removed by Filter: 0 = no rows were filtered out (the WHERE clause matched all rows).
actual time=14567.890..14567.891
Rows Removed by Filter: 0
WHERE
CREATE INDEX idx_orders_order_date ON orders(order_date);
Now re-run EXPLAIN ANALYZE:
New output:
Sort (cost=567.89..567.90 rows=1 width=24) (actual time=45.678..45.679 rows=365 loops=1) Sort Key: (date_trunc('day'::text, order_date)) Sort Method: quicksort Memory: 45kB -> HashAggregate (cost=567.00..567.50 rows=1 width=24) (actual time=45.600..45.650 rows=365 loops=1) Group Key: (date_trunc('day'::text, order_date)) -> Bitmap Heap Scan on orders (cost=12.34..500.00 rows=10000 width=12) (actual time=0.123..23.456 rows=1000000 loops=1) Recheck Cond: ((order_date >= '2023-01-01'::date) AND (order_date <= '2023-12-31'::date)) -> Bitmap Index Scan on idx_orders_order_date (cost=0.00..12.09 rows=10000 width=0) (actual time=0.050..0.051 rows=1000000 loops=1) Index Cond: ((order_date >= '2023-01-01'::date) AND (order_date <= '2023-12-31'::date)) Planning Time: 0.100 ms Execution Time: 45.800 ms
✅ Improvements:1. Bitmap Heap Scan + Bitmap Index Scan = uses the index (no full scan).2. Execution time dropped from 14.5s → 45ms (300x faster!).3. Sort Method: quicksort = in-memory sort (faster than external merge).
Bitmap Heap Scan
Sort Method: quicksort
external merge
If the query is still slow, try: 1. Pre-aggregating data (e.g., materialized views).2. Covering index (include amount in the index to avoid table access): sql CREATE INDEX idx_orders_covering ON orders(order_date) INCLUDE (amount); 3. Partitioning (if the table is huge, e.g., 100M+ rows).
amount
sql CREATE INDEX idx_orders_covering ON orders(order_date) INCLUDE (amount);
Filter: (ssn = '123-45-6789')
status
-- MySQL: Run this weekly ANALYZE TABLE orders; `` - AvoidSELECT `:* It prevents index-only scans.
`` - Avoid
sql -- This query uses idx_orders_order_date. If it slows down, check: -- 1. Is the index still there? (DROP INDEX can happen accidentally) -- 2. Are statistics up to date? (ANALYZE orders) SELECT ... FROM orders WHERE order_date BETWEEN ...;
Full Table Scan
-- MySQL: Log slow queries SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; `` - Track plan changes: If a query suddenly slows down, compareEXPLAIN` plans before/after.
`` - Track plan changes: If a query suddenly slows down, compare
WHERE customer_id = 100
ANALYZE
INSERT/UPDATE/DELETE
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.