SQLite / Intermediate: query structure and transactional design / Lesson 10
Query plans, statistics, and measured evidence
Interpret SQLite EXPLAIN QUERY PLAN without treating its text as a stable API, and distinguish useful access paths from unsupported performance claims.
On this page
Lesson goals
- Read SEARCH, SCAN, and temporary sorting in context.
- Separate a plan from runtime measurement.
- Review statistics and version assumptions.
Ask which work the chosen strategy performs
EXPLAIN QUERY PLAN describes a high-level access strategy, including table/index access, join nesting, and some temporary structures. It is not an actual execution profile with universal elapsed-time or row-count metrics. Full EXPLAIN exposes virtual-machine instructions, another lower-level diagnostic representation. Do not copy another database's EXPLAIN ANALYZE assumptions into SQLite without checking support.
The official documentation warns that EQP output format can change across releases. The exact rows shown here were observed on3.45.1 and are learning evidence, not an application protocol to parse forever. Node identifiers and description wording are not business data. A tool can display them for a human, while a product should depend on query results and documented APIs.
Read a selective primary-key search
The first script looks up one INTEGER PRIMARY KEY. Its tested plan reports a search through that rowid identity and then returns alpha. The plan explains an access path, while the SELECT confirms semantic correctness. A search can still be expensive if its range is broad or many follow-up operations are required; the label alone is not a stopwatch.
Inspect the predicate, expected cardinality, projected columns, and remaining sort or filter work. A covering index can avoid table lookups, but adding many columns carries write and size costs. For nested joins, identify which source is accessed for each outer row. Repeated small lookups can dominate a workload even when every individual access appears indexed.
CREATE TABLE note(id INTEGER PRIMARY KEY,body TEXT);
INSERT INTO note VALUES(1,'alpha'),(2,'beta');
EXPLAIN QUERY PLAN SELECT body FROM note WHERE id=1;
SELECT body FROM note WHERE id=1;
id|parent|notused|detail
2|0|0|SEARCH note USING INTEGER PRIMARY KEY (rowid=?)
body
alpha
The observed plan searches the integer primary key; the result is alpha. Exact EQP wording is version-specific.
Notice when ordering needs temporary work
The second script first requests body ordering without a matching index. The observed3.45.1 plan scans the table and uses a temporary B-tree for ordering. After a body index is created, the observed plan can scan that covering structure instead. The final SELECT still declares its own order. An index never removes the need to specify the output contract.
This is a structural comparison over two rows, not a measured production speedup. A temporary sort is not inherently a defect; it may be appropriate for a small result or an uncommon report. Evaluate memory, I/O, result size, and write cost together. The useful question is whether the complete workload benefits without changing results or imposing an unacceptable mutation cost.
CREATE TABLE note(id INTEGER PRIMARY KEY,body TEXT);
INSERT INTO note VALUES(1,'alpha'),(2,'beta');
EXPLAIN QUERY PLAN SELECT id,body FROM note ORDER BY body,id;
CREATE INDEX note_body ON note(body);
EXPLAIN QUERY PLAN SELECT id,body FROM note ORDER BY body,id;
SELECT id,body FROM note ORDER BY body,id;
id|parent|notused|detail
3|0|0|SCAN note
10|0|0|USE TEMP B-TREE FOR ORDER BY
id|parent|notused|detail
3|0|0|SCAN note USING COVERING INDEX note_body
id|body
1|alpha
2|beta
On the tested runtime the temporary sort is replaced by covering-index traversal. The result remains the same ordered two rows.
Treat statistics and parameters as plan context
The planner uses schema information, constraints, available indexes, and statistics where present. ANALYZE gathers planner statistics; PRAGMA optimize and recommended maintenance behavior are release-dependent. Read the documentation for the deployed version instead of assuming every current recommendation describes the3.45.1 validation environment. Statistics maintenance itself is work that should have an owner and a purpose.
Compare representative parameter classes and distributions. A plan suitable for one rare customer can be inappropriate for a common value, and a tiny uniform training table cannot reproduce that skew. Reprepare and plan behavior can depend on the API and statement form. Keep the actual SQL, bound-value types, schema, and runtime recorded alongside any claimed improvement.
Measure comparable work without resetting shared systems
Benchmark only isolated synthetic or approved representative data, with identical results and controlled conditions. Record warm/cold state, query count, rows returned, host overhead, and resource limits. SQLite's C status interfaces and shell diagnostics can expose useful counters, but a Python trace callback merely showing SQL text is not an execution profile. Diagnostic text can contain sensitive values and needs appropriate access control.
Make one evidence-backed change and verify both the target query and affected writes. Avoid universal prescriptions such as forcing every index, vacuuming after every change, or clearing a shared cache for a test. A useful tuning note explains the hypothesis, observed strategy, measured workload effect, correctness checks, and limits of the conclusion, including any dependence on a specific SQLite release.
Common errors and fixes
A parser broke after a SQLite upgrade.
- Why it happens
- EQP diagnostic text was treated as a stable application API.
- How to fix it
- Use documented result/API contracts and keep plan text for version-aware diagnostics.
- Prevent it next time
- Label observed plans by runtime version.
A SEARCH was assumed faster than every SCAN.
- Why it happens
- Operator labels replaced workload measurement.
- How to fix it
- Inspect qualifying work and measure the complete query.
- Prevent it next time
- Compare results, cardinality, and resource cost.
A tiny benchmark justified a global index change.
- Why it happens
- Unrepresentative data hid distribution and write costs.
- How to fix it
- Test representative classes and affected mutations in isolation.
- Prevent it next time
- Record workload and rollback criteria.
Check your understanding
1. What can a temporary B-tree for ORDER BY tell you, and what can it not prove?
Show worked solution
It shows the selected strategy needs temporary ordering work for that query. It does not prove the query is slow, the sort is avoidable under all conditions, or an added index benefits the whole workload. Measure size, frequency, and mutation tradeoffs before acting.
2. Why retain the actual SQLite version with a saved EQP example?
Show worked solution
The diagnostic format and planner decisions can change. The saved observation is evidence for a specific environment, not a timeless string protocol. Compare semantic results independently and reassess an optimization when schema, data, or runtime changes.
Key takeaways
- A plan describes a strategy rather than a universal timing result.
- EQP output is version-sensitive diagnostic material.
- Statistics, data distribution, and write costs complete the tuning context.