Add explain analyze for SELECT: run the plan to the end without printing its rows, count for every node the rows it handed up and the different pages it read, and print them after the estimates, with timings on one line that tests ignore.
explain shows what the planner intends. When a query is slow, the question is what happened: did the index scan find one row, as estimated, or a thousand? Did the filter throw away most of what the scan read? EXPLAIN ANALYZE answers it by running the plan and counting, node by node, and printing the counts beside the estimates.
It is also the best way to watch the previous stage at work. The stale-statistics trap is invisible in explain, which still shows a confident rows=1. Under explain analyze it is one line:
[PLAN] └── IndexScan (table=t, index=by_v, key='x') (rows=1 cost=6) (actual rows=1000 pages=137)
Rows are what the node handed up to its parent. They tell you where rows are made and where they are thrown away:
Pages are the different pages the node itself read from the buffer pool while it ran, counted once each, whether they were already in the pool or not. Only scans read pages; a Filter, a Project, a Sort or a Limit reads 0. An IndexOnlyScan reads only index pages, and an IndexScan the index pages plus the table pages of every row it looked up, which is why the two can differ by orders of magnitude for the same WHERE.
Early stops show. Under limit 5, the scan hands up 5 rows, not the table: the Limit stopped asking. That is the same pull model you built in the Core Engine, now visible.
Real EXPLAIN ANALYZE prints times per node, and that is often what you want. Times are never the same twice, so no test can read them, and droid's output rule says nothing a test reads may depend on timing. So the times go on one line, [TIME] ..., after the plan. Print anything you find useful there; the tests skip it.
explain analyze runs the statement. For an INSERT, UPDATE or DELETE that would mean changing the data in order to explain it, which PostgreSQL does (and many people have learned the hard way). droid refuses: 01608, and nothing runs.
Every node gets two counters, filled while it runs.
The explain lines, with what happened appended.
These are the exact lines the tests check. The full definition is on the output contract page, section 61.
| Input | Output |
|---|---|
explain analyze select * from t where v = 'c7'; (analysed) | [PLAN] select * from t where v = 'c7', then [PLAN] └── IndexScan (table=t, index=by_v, key='c7') (rows=1 cost=<C>) (actual rows=1 pages=<P>), then [TIME] <anything> |
| a node on a table without statistics | the node, then (actual rows=<A> pages=<P>) |
a Filter, Project, Sort, Limit or aggregate | pages=0 |
explain analyze insert ..., update ..., delete ... | [ERROR:01608] <message>, and nothing runs |
No result rows, no [EXEC] line.
$droid --db plans.dbWelcome to droid!>create table t (id int, v varchar(10));Create table statement executed successfully>insert into t (id, v) values (1, 'a');Insert statement executed successfully (1 rows)>insert into t (id, v) values (2, 'b');Insert statement executed successfully (1 rows)>insert into t (id, v) values (3, 'a');Insert statement executed successfully (1 rows)>explain analyze select id from t where v = 'a';[PLAN] select id from t where v = 'a'[PLAN] └── Project (cols=[id]) (actual rows=2 pages=0)[PLAN] └── Filter (v = 'a') (actual rows=2 pages=0)[PLAN] └── SeqScan (table=t) (actual rows=3 pages=1)[TIME] 0.041 ms>explain analyze delete from t;[ERROR:01608] explain analyze takes a SELECT>.exitexiting.. good bye!
[TIME] line only.The page above is the same for every language. These are the only language-specific hints.
uint32_t page numbers per node, or a bitmap of 1000 bits, since a file has at most 1000 pages.std::unordered_set<uint32_t> per node, or std::bitset<1000>.HashSet<u32> per node, or a [u64; 16] bitmap.std.StaticBitSet(1000) per node.