Skip to content

Expert: design, speed and engines · Indexes, query plans and views · topic 49 of 62 · 35 min

Reading query plans

Explanation

A query plan is the database's description of how it will run a query: which tables it scans, which indexes it searches, and where it sorts. SQLite shows it with EXPLAIN QUERY PLAN (SCAN or SEARCH ... USING INDEX), PostgreSQL with EXPLAIN and EXPLAIN ANALYZE (Seq Scan, Index Scan, actual times), and MySQL with EXPLAIN (type, key, rows). Reading plans is how you prove an index helps.

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.

output
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=?)
Real output on SHOP. SCAN reads every row; SEARCH jumps in through the index.
Words in the planMeaning
SCAN ordersFull 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 INDEXThe 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 idxReads the whole index from start to end: still a full scan, just of a smaller structure
USE TEMP B-TREE FOR ORDER BYAn extra sort step, because no index delivers rows in the needed order
AUTOMATIC ... INDEXSQLite 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.

output
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
An illustrative plan on a 1-lakh-row table, trimmed. Exact numbers and extra lines (such as Buffers) vary by version and data.
  • 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.
Careful: EXPLAIN ANALYZE really executes the statement. EXPLAIN ANALYZE DELETE FROM orders ... deletes the rows. To study a data change safely, wrap it: BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;

MySQL: EXPLAIN

output
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 index
Key columns of MySQL's EXPLAIN table (illustrative numbers). MySQL 8.0.18+ also has EXPLAIN ANALYZE.

The 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

Reading query plans

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.

sql
-- 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=?)
No TEMP B-TREE line: the index returns the rows already sorted by date.

Practice (0/4 done)

Task 1 · Question

SQLite prints this plan: QUERY PLAN `--SCAN products What does it mean?

Task 2 · Question

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?

Task 3 · Question

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?

Task 4 · Practice

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

Question 1

1.Which SQLite plan line means the index alone answered the query?

Question 2

2.What is the key difference between EXPLAIN and EXPLAIN ANALYZE in PostgreSQL?

Question 3

3.In MySQL EXPLAIN, what does type = ALL tell you?

Question 4

4.A PostgreSQL plan estimates rows=10 but the actual rows are 48,000. What is the likely cause?

Question 5

5.How do you study the plan of an UPDATE with EXPLAIN ANALYZE without changing data?

Read more (free)

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.

From Indexes: making queries fast

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.

From Composite, covering, unique and partial indexes

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

  1. 2 more topics in “Indexes, query plans and views” before its exam: Writing fast queries, Views and materialized views.
  2. Module 10 exam: Indexes, query plans and views · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Procedures, triggers, SQL engines and NoSQL. 62 topics left in the course before the final exam.

← Previous: Composite, covering, unique and partial indexes