Query Optimization & EXPLAIN

Creating an index doesn't guarantee SQLite actually uses it — and the only way to know for sure is to ask the query planner directly. EXPLAIN QUERY PLAN is SQLite's primary diagnostic tool for this: it prints a short, human-readable summary of how a query will be executed, so you can confirm it's hitting an index rather than scanning the whole table.

EXPLAIN QUERY PLAN

Put EXPLAIN QUERY PLAN in front of any query to see its plan instead of running it. This is different from plain EXPLAIN, which dumps SQLite's raw low-level VDBE bytecode — nearly unreadable unless you're debugging the engine itself. EXPLAIN QUERY PLAN (the exact two-word phrase) is the one you want for everyday optimization work.

EXPLAIN QUERY PLAN
SELECT rating, comment
FROM reviews
WHERE recipe_id = 42;

Reading the Output: SEARCH vs. SCAN

The output boils down to one of two words per table touched:

Output wordMeaning
SCANFull table scan — SQLite reads every row in the table to find matches. Slow on large tables.
SEARCHSQLite is using an index (or the primary key) to jump directly to matching rows, shown as USING INDEX ... or USING PRIMARY KEY

Before the index from the previous lesson exists, RecipeBox's review lookup does a full scan:

-- Before: no index on reviews.recipe_id
EXPLAIN QUERY PLAN
SELECT rating, comment FROM reviews WHERE recipe_id = 42;

-- Output:
-- QUERY PLAN
-- `--SCAN reviews
-- After: CREATE INDEX idx_reviews_recipe_id ON reviews(recipe_id);
EXPLAIN QUERY PLAN
SELECT rating, comment FROM reviews WHERE recipe_id = 42;

-- Output:
-- QUERY PLAN
-- `--SEARCH reviews USING INDEX idx_reviews_recipe_id (recipe_id=?)
The difference: SCAN reviews means every row in reviews was examined. SEARCH reviews USING INDEX idx_reviews_recipe_id means SQLite jumped straight to the matching rows — exactly the improvement the index was built for.

Practical Optimization Tips

TipWhy it helps
Index columns used in WHERE, JOIN, and ORDER BYThese are exactly the operations EXPLAIN QUERY PLAN checks for a SEARCH instead of a SCAN
Avoid SELECT * when you only need a few columnsLess data read and returned per row, and some queries can be satisfied entirely from the index without touching the table at all
Avoid wrapping an indexed column in a function inside WHEREAn expression like WHERE lower(title) = 'pie' prevents SQLite from using a plain index on title, since it would have to evaluate the function on every row first
-- This prevents index use on title, forcing a SCAN:
SELECT * FROM recipes WHERE lower(title) = 'apple pie';

-- Better: compare directly, or store/index a normalized column,
-- and only select the columns you actually need
SELECT id, title FROM recipes WHERE title = 'Apple Pie';
Next up: Head to Transactions & ACID to see how grouping writes together also affects performance, not just correctness.