Expert: design, speed and engines · Indexes, query plans and views · topic 49 of 62 · 35 min
Reading query plans
Explanation
Why look at the plan?
SQL says what you want, and the query planner decides how. Usually it chooses well, but when a query is slow you need to see its choice: is it reading the whole table? Ignoring your index? Sorting a million rows? The plan answers these questions without guessing.
SQLite: EXPLAIN QUERY PLAN
Put EXPLAIN QUERY PLAN in front of any query. The query is not run; SQLite describes its plan. In the sqlite3 shell it prints a small tree; on this site it returns rows whose detail column holds the same text.
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1;
QUERY PLAN
`--SCAN orders
CREATE INDEX idx_orders_customer ON orders(customer_id);
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1;
QUERY PLAN
`--SEARCH orders USING INDEX idx_orders_customer (customer_id=?)| Words in the plan | Meaning |
|---|---|
| SCAN orders | Full table scan: every row is read |
| SEARCH orders USING INDEX idx (customer_id=?) | Index lookup on customer_id, then the table is read for the other columns |
| USING COVERING INDEX | The index has every needed column; the table is not touched |
| USING INTEGER PRIMARY KEY (rowid=?) | Direct lookup by the table's own key, the fastest case |
| SCAN t USING COVERING INDEX idx | Reads the whole index from start to end: still a full scan, just of a smaller structure |
| USE TEMP B-TREE FOR ORDER BY | An extra sort step, because no index delivers rows in the needed order |
| AUTOMATIC ... INDEX | SQLite builds a temporary index for this one query: a hint that a permanent index may be missing |
For a join, each line is one table, in the order SQLite visits them. On SHOP with an index on orders(customer_id), a join from Pune customers to their orders gives "SCAN c" then "SEARCH o USING COVERING INDEX idx_orders_customer (customer_id=?)": read the small customers table once, and for each customer jump to its orders.
PostgreSQL: EXPLAIN and EXPLAIN ANALYZE
EXPLAIN shows the plan with estimated costs. EXPLAIN ANALYZE runs the query for real and adds actual times and row counts, which is what you want when tuning. Read the plan from the most indented line outward: inner steps feed the outer ones.
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
Seq Scan on orders (cost=0.00..1887.00 rows=10 width=40) (actual time=0.031..11.402 rows=12 loops=1)
Filter: (customer_id = 42)
Rows Removed by Filter: 99988
Planning Time: 0.095 ms
Execution Time: 11.431 ms
-- after CREATE INDEX idx_orders_customer ON orders (customer_id);
Index Scan using idx_orders_customer on orders (cost=0.29..44.40 rows=10 width=40) (actual time=0.022..0.041 rows=12 loops=1)
Index Cond: (customer_id = 42)
Planning Time: 0.120 ms
Execution Time: 0.062 ms- Seq Scan: a full table scan. Rows Removed by Filter: 99988 is the waste: almost everything read was thrown away.
- Index Scan: searched the index, then fetched rows. Index Only Scan: a covering index. Bitmap Heap Scan: collects many matches from an index, then reads them in page order.
- cost=start..total is the planner's estimate in its own units, not milliseconds. rows= is the estimated row count.
- actual time is in milliseconds, per loop. When estimated rows and actual rows differ hugely (10 versus 50,000), statistics are stale: run ANALYZE.
MySQL: EXPLAIN
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
type: ALL key: NULL rows: 99823 Extra: Using where -- before the index
type: ref key: idx_orders_customer rows: 12 Extra: NULL -- after the indexThe type column is the access method, from worst to best roughly: ALL (full scan), index (full index scan), range (an index range), ref (index lookup on a non-unique value), eq_ref (one row per join through a unique key) and const (at most one row). key is the index chosen, and rows is the estimated number of rows examined. "Using filesort" in Extra means an extra sort step, and "Using index" means a covering index.
Common mistakes
- Tuning by feel. Fix: read the plan before and after every change.
- Running EXPLAIN ANALYZE on a DELETE or UPDATE in production. Fix: wrap it in a transaction and roll back, or test on a copy.
- Panicking at a SCAN on a tiny table. Fix: on a few hundred rows, a scan is often the best plan; judge with realistic data.
- Reading cost numbers as milliseconds. Fix: compare costs only between plans of the same query; use actual time for real speed.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Reading query plans · 1/7
SQL says what you want. The query plan shows how the database will do it. Reading plans is how you find slow spots.
Prefer a video by a teacher? Find free videos on “explain query plan sqlite postgresql explain analyze seq scan index scan tutorial” on YouTube ↗
In real life: A slow "My orders" page
After a big sale, the "My orders" page of a shopping app starts taking two seconds. A developer copies the page's query into psql and runs EXPLAIN ANALYZE. The plan shows a Seq Scan on orders with "Rows Removed by Filter" in the crores, followed by a sort. She adds an index on (customer_id, created_at), runs EXPLAIN ANALYZE again, and sees an Index Scan with no separate sort and an execution time of under a millisecond. The plan is her proof in the pull request.
-- SQLite version of the same check
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);
EXPLAIN QUERY PLAN
SELECT id, order_date FROM orders WHERE customer_id = 1 ORDER BY order_date;
-- SEARCH orders USING COVERING INDEX idx_orders_customer_date (customer_id=?)Practice (0/4 done)
SQLite prints this plan: QUERY PLAN `--SCAN products What does it mean?
SQLite prints: |--SEARCH orders USING INDEX idx_orders_customer (customer_id=?) `--USE TEMP B-TREE FOR ORDER BY The query is: ... WHERE customer_id = ? ORDER BY order_date. What would remove the second line?
PostgreSQL shows: Seq Scan on payments (cost=0.00..45210.00 rows=3 width=52) (actual time=0.02..310.55 rows=2 loops=1) Filter: (gateway_ref = 'UPI123') Rows Removed by Filter: 2000000 What is the best next step?
Use the SQL runner on this site with the SHOP tables (any SHOP task works as a playground). Run EXPLAIN QUERY PLAN on SELECT id, order_date FROM orders WHERE customer_id = 1 ORDER BY order_date, and write down the detail lines. Then create an index on orders(customer_id) and run it again, then drop it and create one on orders(customer_id, order_date) and run it a third time. Explain each change in one sentence.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Writing fast queriesIndexes, query plans and views · same module
- Indexes: making queries fastBackend Development · SQL in depth
- MySQL vs PostgreSQL vs SQLite vs SQL ServerProcedures, triggers, SQL engines and NoSQL
- Joins: combining tablesBackend Development · SQL in depth
- Joining three or more tablesJOINs
- Upserts: insert or updateChanging data, transactions and ACID
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which SQLite plan line means the index alone answered the query?
Answer: SEARCH orders USING COVERING INDEX idx_x (customer_id=?)
COVERING means every needed column was in the index.
What is the key difference between EXPLAIN and EXPLAIN ANALYZE in PostgreSQL?
Answer: EXPLAIN ANALYZE actually runs the query and reports real times and rows
ANALYZE executes the statement, so be careful with data changes.
In MySQL EXPLAIN, what does type = ALL tell you?
Answer: A full table scan
ALL is the full scan access type; ref, eq_ref and const are index lookups.
A PostgreSQL plan estimates rows=10 but the actual rows are 48,000. What is the likely cause?
Answer: Out-of-date statistics; run ANALYZE
The planner relies on statistics. Big estimate errors usually mean stale stats.
How do you study the plan of an UPDATE with EXPLAIN ANALYZE without changing data?
Answer: Wrap it in BEGIN ... ROLLBACK
The update runs inside the transaction, and ROLLBACK undoes it.
In SQLite, EXPLAIN QUERY PLAN shows "SCAN users". What does that mean?Related
Answer: The whole table is read row by row
SCAN means a full table scan. "SEARCH users USING INDEX ..." would mean the index is used.
Which column gets an index automatically in SQLite and PostgreSQL?Related
Answer: A column with a UNIQUE constraint
UNIQUE and PRIMARY KEY are enforced with an index. Foreign keys are not indexed automatically.
From How indexes work
A unique index on email exists. How many rows with a NULL email can SQLite or PostgreSQL store?Related
Answer: As many as you like
NULLs are not equal to each other, so they do not clash. SQL Server differs and allows one.
A page shows 50 orders and then runs one query per order to fetch its customer's name. What is this called?Related
Answer: The N+1 query problem
One query for the list plus N follow-up queries. Use a JOIN instead.
From Writing fast queries
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Indexes, query plans and views” before its exam: Writing fast queries, Views and materialized views.
- Module 10 exam: Indexes, query plans and views · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Procedures, triggers, SQL engines and NoSQL. 62 topics left in the course before the final exam.