Hook: Interviewers ask this because it shows whether you can read the database's map before blaming the SQL.
Question: What is EXPLAIN PLAN?
Answer: EXPLAIN PLAN is a way to ask the database how it expects to run a query. It shows the planner's chosen steps, such as table scans, index usage, joins, and sorts, so you can see why a query may be slow. In many databases the exact command name differs, but the idea is the same: inspect the plan before guessing.
Interview-Ready Answer: "EXPLAIN PLAN is how I ask the database to show me the execution strategy for a query. I use it to see whether the optimizer will do a sequential scan, use an index, or choose an expensive join order. The key detail is that the plan is usually an estimate unless I use ANALYZE, so it helps me prove where the bottleneck is before I change indexes or rewrite the SQL."
Detailed Explanation: An execution plan is the database's step-by-step route for answering your query. The planner (also called the optimizer, meaning the part of the database that picks the cheapest way to run a query) estimates how many rows each step will touch, what access path it will use, and how much work that will cost. A good plan is not the one with the smallest SQL text; it is the one with the least real work.
EXPLAIN PLAN is the fastest way to stop guessing. It tells you whether the database is doing a full table scan, whether your index is ignored, whether a join order is bad, or whether the planner thinks a filter is much more selective than it really is. This is why performance work often starts with the plan, not with random index creation.
| Access path | Good when | Trade-off |
|---|---|---|
| Sequential scan | Small tables or many rows needed | Reads the whole table |
| Index scan | Few rows match, predicate is selective | Random reads can be costly |
| Index-only scan | Needed columns are in the index and visibility checks allow it | Not always possible |
Use EXPLAIN when a query is slower than expected, when an index exists but is not used, when a join became expensive after data growth, or before and after an index change. A very common surprise is that the database picks a sequential scan for a small table even though an index exists; that is often correct because scanning 500 rows can be cheaper than bouncing through an index and then fetching table rows.
On a B-tree index, a lookup is often roughly O(log N + K), where N is table size and K is the number of matching rows. A full scan is O(N). In practical terms, a query that reads 10 rows from a 10 million-row table can be milliseconds with a good index, while a scan that reads millions of rows can take seconds and burn CPU and I/O. But if 30-50% of the table matches, the full scan can actually win because one clean pass is cheaper than many random page visits.
Planning itself is usually fast for simple queries, but it can get expensive for large join graphs because the optimizer may consider many join orders. Databases use heuristics and pruning to keep planning time manageable. That is why very complex queries may show a planning time that is small but non-zero, while execution time dominates.
Different databases spell the idea differently. Oracle commonly uses EXPLAIN PLAN FOR ... and stores output in PLAN_TABLE. PostgreSQL uses EXPLAIN and EXPLAIN ANALYZE. MySQL uses EXPLAIN, and newer versions also support EXPLAIN ANALYZE. The mental model stays the same even when the command changes.
First, a function wrapped around a column can block a plain index, because the database cannot directly search the raw column values anymore. Second, stale statistics can make the optimizer choose a bad plan; running ANALYZE or the database's stats refresh job often fixes this. Third, EXPLAIN alone does not run the query, but EXPLAIN ANALYZE does run it, and on INSERT, UPDATE, or DELETE that means real data changes unless you wrap it in a transaction and roll it back.
Memory-friendly rule: read the plan like a route map: fewer stops, fewer detours, less traffic.
Real-World Example: Imagine an e-commerce checkout service that needs to fetch today's paid orders for a customer. The team sees p95 latency jump from 40 ms to 2.8 s after the order table grows. EXPLAIN shows a sequential scan because the query uses DATE(created_at) = CURRENT_DATE, which hides the indexed timestamp column behind a function. Users experience slow checkout, retries, and eventually timeout errors in the logs. The fix is to rewrite the predicate into a date range and, if needed, add the right index. After that, the plan changes from scanning most of the table to touching only the matching rows, and latency drops back to normal.
What goes wrong: the team keeps adding hardware, but CPU stays high because the real problem is access path, not raw capacity. In logs you may see statements like statement timeout, the database CPU pinned near 90-100%, and a flood of repeated requests from impatient clients. The user-facing symptom is simple: checkout feels broken even though the app server is healthy.
-- PostgreSQL example: show how EXPLAIN reveals plan choices.
-- This script is runnable as-is in PostgreSQL.
-- It demonstrates:
-- 1) a query that can use a normal index,
-- 2) a query that often cannot use that index because the column is wrapped in a function,
-- 3) an edge case fix using an expression index.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id INT NOT NULL,
created_at TIMESTAMP NOT NULL,
status TEXT NOT NULL
);
-- Fill the table with enough rows to make the planner's choice interesting.
-- We intentionally create predictable data so the example is stable.
INSERT INTO orders (customer_id, created_at, status)
SELECT
(g % 1000) + 1,
TIMESTAMP '2024-01-01 00:00:00' + (g || ' minutes')::interval,
CASE WHEN g % 10 = 0 THEN 'cancelled' ELSE 'paid' END
FROM generate_series(1, 10000) AS g;
-- Basic indexes that the optimizer can consider.
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_orders_created_at ON orders (created_at);
-- Update statistics so EXPLAIN has realistic estimates.
ANALYZE orders;
-- Good: this predicate is sargable (the column is used directly),
-- so the planner can use the created_at index efficiently.
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders
WHERE created_at >= TIMESTAMP '2024-01-05 00:00:00'
AND created_at < TIMESTAMP '2024-01-06 00:00:00';
-- Bad for a plain b-tree index: wrapping the column in DATE()
-- often prevents direct index lookup, so the planner may choose a scan.
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders
WHERE DATE(created_at) = DATE '2024-01-05';
-- Edge case fix: if you must query by DATE(created_at), a matching
-- expression index can make that expression searchable.
CREATE INDEX idx_orders_created_date ON orders ((DATE(created_at)));
ANALYZE orders;
-- After the expression index exists, the plan has a better option.
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders
WHERE DATE(created_at) = DATE '2024-01-05';
-- Another small sanity check: equality on customer_id is a classic index-friendly predicate.
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders
WHERE customer_id = 42;
Common Mistakes:
Memory Hook: Think of EXPLAIN PLAN as the database's GPS: it shows the route, not the trip, and a bad route often means the wrong road or too many detours.
Cheat Sheet:
Practice Tasks: