SQLite EXPLAIN QUERY PLAN

EXPLAIN QUERY PLAN shows exactly how SQLite plans to execute a query without actually running it. Use it to discover whether SQLite is using an index, performing a full table scan, or sorting in memory. This is the first tool to reach for when a query runs slowly.

Basic Syntax

EXPLAIN QUERY PLAN
SELECT ...;

Reading the Output

EXPLAIN QUERY PLAN
SELECT name FROM employees WHERE email = 'alice@x.com';

id │ parent │ notused │ detail
───┼────────┼─────────┼─────────────────────────────────────────────
2  │ 0      │ 0       │ SEARCH employees USING INDEX idx_email (email=?)

KEY TERMS:
──────────────────────────────────────────────────────────────────────
SCAN employees         → Full table scan (reads every row) — SLOW
SEARCH employees       → Index-based lookup (jumps to rows)  — FAST
USING INDEX idx_email  → Tells you which index is used
USING COVERING INDEX   → Index holds all needed data (fastest)
USE TEMP B-TREE FOR    → SQLite sorts in memory (no ORDER BY index)

Full Table Scan Example

-- No index on "name" column
EXPLAIN QUERY PLAN
SELECT name FROM employees WHERE name = 'Alice';

detail
───────────────────────────────────────────────────────────────
SCAN employees

"SCAN" = reading all rows — bad for large tables.
Fix: CREATE INDEX idx_name ON employees(name);

After adding the index:
EXPLAIN QUERY PLAN
SELECT name FROM employees WHERE name = 'Alice';

detail
───────────────────────────────────────────────────────────────
SEARCH employees USING INDEX idx_name (name=?)

Checking a JOIN Plan

EXPLAIN QUERY PLAN
SELECT e.name, d.name AS dept
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE e.salary > 70000;

detail
───────────────────────────────────────────────────────────────────
SCAN employees
SEARCH departments USING INTEGER PRIMARY KEY (rowid=?)

Interpretation:
  employees: full scan (no index on salary)
  departments: fast PK lookup for each employee row

Fix: CREATE INDEX idx_salary ON employees(salary);

Sorting Without an Index

EXPLAIN QUERY PLAN
SELECT name FROM employees ORDER BY name;

detail
────────────────────────────────────────────
SCAN employees
USE TEMP B-TREE FOR ORDER BY

SQLite scans all rows then sorts them in memory.

After CREATE INDEX idx_name ON employees(name):
detail
────────────────────────────────────────────
SCAN employees USING INDEX idx_name

Now SQLite reads rows in index order — no sort step needed.

Subquery Plan

EXPLAIN QUERY PLAN
SELECT name FROM employees
WHERE dept_id IN (SELECT id FROM departments WHERE name='Engineering');

id │ parent │ detail
───┼────────┼──────────────────────────────────────────────────────
3  │ 0      │ SEARCH departments USING COVERING INDEX ... (name=?)
5  │ 0      │ SEARCH employees USING INDEX idx_dept (dept_id=?)

Covering Index Plan

A covering index contains every column a query needs. SQLite reads only the index and never touches the main table rows. This is the fastest possible plan because it avoids a second disk lookup for each result row.

-- Index on (name, salary) covers a query that only needs those two columns
CREATE INDEX idx_name_salary ON employees(name, salary);

EXPLAIN QUERY PLAN
SELECT name, salary FROM employees WHERE name = 'Alice';

detail
───────────────────────────────────────────────────────────────────────
SEARCH employees USING COVERING INDEX idx_name_salary (name=?)

"COVERING INDEX" means SQLite reads the index only — no table access.
This is faster than USING INDEX because it skips the extra table lookup.

Multi-Table JOIN Plan Reading

-- Three-table join
EXPLAIN QUERY PLAN
SELECT e.name, d.name, l.city
FROM employees e
JOIN departments d ON e.dept_id = d.id
JOIN locations   l ON d.location_id = l.id
WHERE e.salary > 60000;

id │ parent │ detail
───┼────────┼────────────────────────────────────────────────────────────
3  │ 0      │ SCAN employees (no salary index — full scan first)
5  │ 3      │ SEARCH departments USING INTEGER PRIMARY KEY (rowid=?)
7  │ 5      │ SEARCH locations USING INTEGER PRIMARY KEY (rowid=?)

Reading multi-table plans:
  parent=0 → first table in the join order (the driving table)
  parent=3 → processed after row 3's table
  parent=5 → processed after row 5's table

The driving table (parent=0) sets the pace. If it is slow,
the entire query is slow. Index the driving table's filter column.

EXPLAIN QUERY PLAN Workflow

Step 1: Run a slow query — note the execution time

Step 2: Prefix it with EXPLAIN QUERY PLAN
──────────────────────────────────────────
EXPLAIN QUERY PLAN
SELECT ... FROM ... WHERE ... ORDER BY ...;

Step 3: Look for SCAN on large tables → add an index

Step 4: Look for USE TEMP B-TREE → add index on ORDER BY column

Step 5: Add the index
──────────────────────────────────────────
CREATE INDEX idx_col ON table(col);

Step 6: Re-run EXPLAIN QUERY PLAN → confirm SEARCH appears

Step 7: Run the original query → measure new execution time

EXPLAIN vs EXPLAIN QUERY PLAN

EXPLAIN QUERY PLAN SELECT ...;
  → High-level plan: which tables and indexes SQLite will use
  → Human-readable, use this for day-to-day optimization

EXPLAIN SELECT ...;
  → Low-level virtual machine opcodes SQLite will execute
  → Detailed bytecode, useful only for deep SQLite internals study

For most developers, EXPLAIN QUERY PLAN is all you need.

Quick Reference: Plan Signals

OUTPUT CONTAINS               MEANING
────────────────────────────  ────────────────────────────────────────────
SCAN table                    Full table scan — consider adding index
SEARCH table USING INDEX      Index used for row lookup — good
USING COVERING INDEX          No table access needed — fastest
USE TEMP B-TREE FOR ORDER BY  In-memory sort — index on ORDER BY col helps
USE TEMP B-TREE FOR DISTINCT  In-memory dedup — check if needed
SCAN table USING INDEX        Index used for ordering, but all rows read

Key Takeaways

  • EXPLAIN QUERY PLAN SELECT ... shows the execution plan without running the query
  • SCAN means full table scan — often a sign to add an index on the WHERE or ORDER BY column
  • SEARCH USING INDEX confirms an index is being used for lookups
  • USING COVERING INDEX is the best outcome — no table row access needed at all
  • USE TEMP B-TREE FOR ORDER BY means SQLite sorts in memory — create an index on the ORDER BY column to eliminate this step
  • Always re-check the plan after adding an index to confirm it is actually being picked up

Leave a Comment

Your email address will not be published. Required fields are marked *