Step 1 of 3 · Reading · ~3 min
Understanding Full Table Scans
In-Memory Storage
Table Scan — SELECT * Full Scan
Behind every SELECT you've run so far is the simplest possible query strategy: look at every row and check if it matches. This lesson makes that strategy explicit and visible, via .explain, and puts a name and a cost model on it before you spend the next chapter building something faster.
What a full table scan does
For a query like SELECT * FROM users WHERE age > 25, a full scan:
- Starts at the first row.
- Evaluates the
WHEREpredicate against it. - If it passes, includes it in the result (after projection).
- Advances to the next row.
- Repeats until every row in the table has been visited.
There is no shortcut based on the values involved — even if age happens to be sorted, or even if only one row could possibly match, the scan still visits all of them. That's what makes it O(n) in the number of rows: cost is proportional to table size, not to the number of matching rows or any property of the predicate.
Implementing .explain
.explain <query> doesn't execute the query — it reports the plan the engine would use to execute it. For every query at this stage of the project, that plan is the same: a scan of the target table.
.explain SELECT * FROM users WHERE age > 25
→ SCAN TABLE users
Implementing this is mostly a matter of parsing the query enough to identify the target table (you already have this logic from SELECT), and then printing the fixed-format line rather than running the query. Keep .explain's parsing lightweight — it should reuse your existing SELECT parser, just short-circuit before actually scanning rows.
Why introduce this now
This lesson exists as a checkpoint, not just an exercise: it's the moment where you name the cost of what you've built so you have a baseline to compare against. In the next chapter, once you build a B-tree index, .explain on an indexed lookup should print something like SEARCH TABLE users USING INDEX instead of SCAN TABLE users — and you'll be able to point at a concrete, structural difference in the plan, not just "it feels faster." Real query planners (PostgreSQL's EXPLAIN, SQLite's EXPLAIN QUERY PLAN) exist for exactly this reason: to let you see how a query will be executed before running it, so you can reason about performance without guessing.
.count
.count <table> is a simpler, related primitive: report the number of rows currently stored, as a plain number:
.count users
→ 2
This is also, not coincidentally, itself a full scan if implemented naively (count while iterating) — though since you already track row counts implicitly via your rows list, len(table.rows) gives you the answer without a real per-row loop. It's worth noting the difference: a count only needs cardinality, while a WHERE-filtered query needs the actual values, which is why some real databases can answer unconditional COUNT(*) faster than a full predicate scan (by maintaining row-count metadata) — something you're implicitly doing here already.
Edge cases
.explainon an unknown table should still surfaceERR unknown table: <name>, matching the error behavior ofSELECTitself..counton an empty table should print0, not an error.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…