Hook: A slow SQL query is usually a traffic jam, not a mystery — interviewers love this question because it shows whether you diagnose with evidence instead of guessing.
Question: How do I find the root cause of a slow query?
Answer: I start by reproducing the exact query and measuring its real execution plan. Then I look for the biggest bottleneck: too many rows read, a bad join order, a sort or hash that spills to disk, stale statistics, or blocking from locks. Once I know where the time is going, I change one thing at a time and confirm the fix with another measured run.
Interview-Ready Answer: I would first reproduce the query with the same parameters and run an execution plan with actual timings, not just the SQL text. Then I compare estimated rows to actual rows, because a big gap usually points to stale statistics or skewed data, and I check for scans, expensive joins, sort spills, and lock waits. After that I fix the most likely cause — for example by rewriting a non-sargable predicate, adding the right index, or refreshing statistics — and I verify the improvement with another plan and timing check.
Detailed Explanation: A slow query is not diagnosed by guessing the fix; it is diagnosed by finding the bottleneck. In practice, the root cause usually lives in one of five places: too many rows read, too many rows joined, too much work to sort or hash, waiting on locks, or bad estimates from statistics.
EXPLAIN ANALYZE (or the vendor equivalent) so you see real runtime, not just estimates. The key interview clue is the gap between estimated rows and actual rows; rows are the count of records the optimizer thinks it will touch. Big gaps usually mean stale statistics, data skew, or a filter that the optimizer cannot model well.Seq Scan, the database is reading the table row by row. That can be fine for tiny tables or low-selectivity filters, but it is a red flag when only a small fraction should match. Also watch for functions on indexed columns, leading wildcards like %abc, or implicit type casts; these make a predicate non-sargable, meaning the index cannot be used efficiently.work_mem is 4MB, so a moderately wide result set can spill even when the query looks small on paper.ANALYZE, or reduce the amount of data returned. Do one fix at a time so you can prove which change solved the issue.| Plan clue | Usually means | Next check |
|---|---|---|
| Seq Scan | No good index path | Predicate, selectivity |
| Nested Loop | Many repeated lookups | Join keys, row estimate |
| Sort Disk | Memory spill | Result size, work_mem |
| Actual >> Est. | Bad stats or skew | ANALYZE, histograms |
An easy memory trick: if the plan says the query should touch 10 rows but it actually touches 100,000, the planner is blind somewhere. That blindness is often the real root cause.
Think in trade-offs. A table scan is O(n) because the engine may inspect every row. An index lookup is closer to O(log n + k), where k is the number of matched rows. That is why indexes help most when a query is selective, meaning it returns a small slice of the table. But an index is not free: every insert, update, and delete also pays the index maintenance cost.
In other words, the root cause is not just 'slow SQL'; it is 'wrong work for this shape of data and load'. Your job is to find that mismatch and prove it with the plan.
Real-World Story: In an e-commerce checkout service, a customer order lookup query suddenly started taking 3 to 5 seconds instead of 80 milliseconds. The team had already added an index on created_at, so they first blamed the database server, but the real issue was that the query used DATE(created_at) in the filter and then sorted a large result set. The symptoms were clear: p95 latency jumped, logs showed Seq Scan and Sort Method: external merge Disk, and users saw checkout timeouts or abandoned carts. The fix was to rewrite the filter as a range, confirm the plan with EXPLAIN ANALYZE, and keep statistics fresh after data loads. If they had only added more hardware, the problem would have returned during the next traffic spike.
-- Demo: find why a query is slow by comparing a bad predicate to a sargable one.
DROP TABLE IF EXISTS orders;
CREATE TEMP TABLE orders (
order_id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id int NOT NULL,
created_at timestamp NOT NULL,
status text NOT NULL,
amount numeric(10,2) NOT NULL
);
INSERT INTO orders (customer_id, created_at, status, amount)
SELECT
(g % 1000) + 1,
TIMESTAMP '2024-01-01' + (g || ' minutes')::interval,
CASE WHEN g % 10 = 0 THEN 'cancelled' ELSE 'paid' END,
((g % 5000)::numeric / 100)
FROM generate_series(1, 50000) AS g;
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_orders_status ON orders(status);
ANALYZE orders;
-- BAD: wrapping the indexed column in DATE() makes the predicate non-sargable.
-- The planner usually cannot use the b-tree index efficiently here.
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders
WHERE DATE(created_at) = DATE '2024-01-15';
-- GOOD: range predicates keep the column naked, so the index can be used.
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders
WHERE created_at >= TIMESTAMP '2024-01-15'
AND created_at < TIMESTAMP '2024-01-16';
-- Edge case: an index on status helps only if the filter is selective enough.
-- Searching for the common value 'paid' may still touch many rows, so a seq scan can be reasonable.
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders
WHERE status = 'paid';
-- A rarer value is more selective and is more likely to benefit from the index.
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*)
FROM orders
WHERE status = 'cancelled';Follow-up & Tricky Questions:
EXPLAIN enough? Usually no. EXPLAIN shows the plan, but EXPLAIN ANALYZE shows the real runtime, which is what you need for root-cause analysis.Common Mistakes:
Memory Hook: Think road, lights, load: the road is the plan, the lights are locks, and the load is sort/hash pressure. Check them in that order under interview pressure.
Cheat Sheet:
EXPLAIN ANALYZE, not guesswork.Practice Tasks:
DATE(column), then rewrite it as a date range and compare the plan.EXPLAIN ANALYZE to spot the row explosion.