By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
A Hyper-Practical, Zero-Fluff Guide for Data Analysts
An execution plan is the step-by-step blueprint your database engine follows to retrieve data for your query. Think of it like a GPS route for SQL: it tells you exactly how the database will fetch your results—whether it’s scanning every row in a table (slow) or using an index (fast).
Why this matters in production:- Performance: A bad execution plan can turn a 100ms query into a 10-minute disaster, especially on large datasets.- Cost: In cloud databases (Redshift, BigQuery, Snowflake), inefficient queries = higher bills.- Debugging: When a dashboard times out or a report runs forever, the execution plan is your first diagnostic tool.
Real-world scenario:You’re analyzing customer churn for a SaaS company. Your query:
SELECT user_id, last_login_date FROM users WHERE signup_date > '2023-01-01' ORDER BY last_login_date DESC;
Takes 30 seconds to run on a 10M-row table. Your manager needs the report in 5 minutes. The execution plan will show you why it’s slow—and how to fix it.
WHERE
JOIN
ORDER BY
INSERT
UPDATE
EXPLAIN SELECT ...
EXPLAIN ANALYZE
EXPLAIN
WHERE status = 'active'
WHERE id = 123
Buffers: shared hit=100 read=50
read
hit
pgbench
SELECT
-- Create a test table CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100), email VARCHAR(100), signup_date DATE, last_login_date TIMESTAMP, status VARCHAR(20) DEFAULT 'active' ); -- Insert 1M rows (PostgreSQL) INSERT INTO users (name, email, signup_date, last_login_date, status) SELECT 'user_' || i, 'user_' || i || '@example.com', DATE '2020-01-01' + (random() * 1000)::int, NOW() - (random() * 1000)::int * INTERVAL '1 day', CASE WHEN random() > 0.5 THEN 'active' ELSE 'inactive' END FROM generate_series(1, 1000000) i;
EXPLAIN ANALYZE SELECT id, name, last_login_date FROM users WHERE signup_date > '2023-01-01' ORDER BY last_login_date DESC LIMIT 100;
Here’s a simplified execution plan (PostgreSQL):
Limit (cost=12345.67..12345.92 rows=100 width=24) (actual time=500.123..500.456 rows=100 loops=1) -> Sort (cost=12345.67..12678.90 rows=133333 width=24) (actual time=500.120..500.345 rows=100 loops=1) Sort Key: last_login_date DESC Sort Method: top-N heapsort Memory: 35kB -> Seq Scan on users (cost=0.00..10000.00 rows=133333 width=24) (actual time=0.012..450.678 rows=133333 loops=1) Filter: (signup_date > '2023-01-01'::date) Rows Removed by Filter: 866667
Breakdown:1. Seq Scan on users - The database reads all 1M rows (rows=133333 after filtering). - Problem: No index on signup_date, so it scans the entire table. - Actual time: 450ms (slow!).
rows=133333
signup_date
Applied after fetching all rows (inefficient).
Sort (last_login_date DESC)
top-N heapsort
Cost: High because it’s sorting a large dataset.
Limit 100
Add an index:
CREATE INDEX idx_users_signup_date ON users(signup_date); CREATE INDEX idx_users_last_login_date ON users(last_login_date);
Re-run EXPLAIN ANALYZE:
Limit (cost=0.29..12.34 rows=100 width=24) (actual time=0.050..0.120 rows=100 loops=1) -> Index Scan Backward using idx_users_last_login_date on users (cost=0.29..12345.67 rows=133333 width=24) (actual time=0.048..0.100 rows=100 loops=1) Filter: (signup_date > '2023-01-01'::date)
What changed?- Index Scan Backward on last_login_date (uses the index for sorting).- Filter is still applied, but now on a smaller dataset (only rows matching the index).- Actual time: 0.12ms (4000x faster!).
last_login_date
Problem: The filter on signup_date is still applied after the index scan. Let’s optimize further:
CREATE INDEX idx_users_signup_last_login ON users(signup_date, last_login_date DESC);
New plan:
Limit (cost=0.29..12.34 rows=100 width=24) (actual time=0.020..0.050 rows=100 loops=1) -> Index Scan using idx_users_signup_last_login on users (cost=0.29..12345.67 rows=133333 width=24) (actual time=0.018..0.040 rows=100 loops=1) Index Cond: (signup_date > '2023-01-01'::date)
Key improvements:- Index Cond (not Filter): The index is used during the scan (no extra filtering step).- Actual time: 0.05ms (another 2x speedup).
user_id
email
SELECT *
pg_stat_statements
SET statement_timeout = '5s'
EXPLAIN (FORMAT JSON) ...
pg_stat_database
CREATE INDEX idx_column ON table(column)
status
CREATE INDEX idx_active_users ON users(id) WHERE status = 'active'
Sort Method: external merge
LIKE '%term%'
LIKE 'term%'
DROP INDEX idx_unused
Trick: It depends! Seq scan is faster for small tables; index scan is faster for large tables with selective filters.
"Why is this query slow?" (Given an execution plan)
Look for:
"How would you optimize this query?"
actual time
"You have a table with 10M rows. A query filtering on status (3 possible values) is slow. What do you do?"- Wrong answer: Add an index on status (low cardinality = bad).- Right answer: - Use a partial index: CREATE INDEX idx_active_users ON users(id) WHERE status = 'active'. - Or partition the table by status.
Challenge:You have a table orders with 5M rows. A query filtering on order_date and sorting by total_amount is slow. Optimize it.
orders
order_date
total_amount
Given:
EXPLAIN ANALYZE SELECT customer_id, total_amount FROM orders WHERE order_date > '2023-01-01' ORDER BY total_amount DESC LIMIT 100;
Current plan shows a seq scan and a sort.
Solution:
-- Add a composite index CREATE INDEX idx_orders_date_amount ON orders(order_date, total_amount DESC); -- Re-run the query EXPLAIN ANALYZE SELECT customer_id, total_amount FROM orders WHERE order_date > '2023-01-01' ORDER BY total_amount DESC LIMIT 100;
Why it works:- The index covers both the WHERE and ORDER BY clauses.- The database can now use an index scan and avoid sorting.
-- Basic execution plan EXPLAIN SELECT * FROM users WHERE id = 1; -- Execution plan with actual runtime stats EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1; -- JSON format (for logging) EXPLAIN (FORMAT JSON) SELECT * FROM users WHERE id = 1; -- Create an index CREATE INDEX idx_name ON table(column); -- Composite index CREATE INDEX idx_name ON table(col1, col2 DESC); -- Partial index CREATE INDEX idx_name ON table(column) WHERE condition;
-- Basic execution plan EXPLAIN SELECT * FROM users WHERE id = 1; -- Extended info EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1; -- Check if a query uses an index EXPLAIN SELECT * FROM users WHERE name LIKE 'A%'; -- Look for "Using index" in the "Extra" column.
shared_buffers
quicksort
external merge
Final Tip:Execution plans are your SQL superpower. The next time a query is slow, don’t guess—run EXPLAIN and let the database tell you why. ?
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.