Vectorised execution engine - #4
Merged
Merged
Conversation
Operators pull batches from each other, which is what lets a LIMIT stop a scan early instead of running it to completion and discarding the rest. Expression evaluation is vectorised: the operator is resolved once and the loop runs over a contiguous buffer. Comparing a column against a literal has its own path so no constant column is materialised, and same-type column pairs skip boxing through Value. Three-valued logic is implemented properly — `false AND NULL` is false, `true OR NULL` is true — because getting it wrong changes which rows a query returns. The scan is where predicate pushdown stops being a plan rewrite and starts saving I/O: before reading a row group it asks each predicate whether the group's zone map could satisfy it, and skips the column chunks entirely if not. A conjunction prunes when either half rules the group out; a disjunction only when both do. Sorting has three modes. Feeding a LIMIT it keeps only the best k rows, so memory is bounded by the limit rather than the input. Fitting in memory it sorts once. Otherwise it writes sorted runs to spill files and merges them, which is the only reason a database sorts differently from Vec::sort. Spill files are removed even when a query is abandoned partway through. The hash join builds one side and streams the other. For an inner join the smaller estimate is built; for an outer join the choice is forced, because the side that may be NULL-padded has to be the one that can be scanned for non-matches afterwards. Output column order is left-then-right either way. Fixed along the way: a projection producing no columns dropped its row count, which would have made `SELECT count(*)` return zero after column pruning.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The half that actually runs the query. 43 end-to-end tests go SQL text in, rows out, through the same
Sessionthe CLI uses — nothing reaches past it into a convenient shortcut.Pull-based operators
Each operator asks its input for a batch and returns one of its own. Pulling is not a style preference: it is what lets
LIMIT 10stop a scan after the first row group instead of running it to completion and throwing the rest away, and there is a test asserting exactly that (1 readout of ten row groups). Blocking operators — sort, hash join build, aggregate — buffer internally and stream their output, so nothing above them has to know the difference.Vectorised evaluation
Every kernel takes whole columns. The operator is resolved once, outside the loop, and the loop runs over a contiguous buffer — the row-at-a-time alternative would walk the expression tree per row and spend most of its time on dispatch. Two cases get their own paths because they dominate real queries: column-against-literal (no constant column is materialised, and a literal on the left mirrors the comparison rather than copying the data) and same-type column pairs (no boxing through
Value).Three-valued logic is implemented properly, because getting it wrong silently changes which rows come back:
false AND NULLisfalse, not NULL — the row cannot match whatever the unknown turns out to be.true OR NULListrue. Both asymmetric cases have their own test.5 IN (1, NULL)is unknown, not false: the NULL might have been 5.x / 0yields NULL rather than aborting — one bad row should not kill a scan of a million.LIKEis a hand-written backtracking matcher rather than a regex, iterative so a pattern of many%s cannot blow the stack.Scan: where pushdown becomes I/O
Before reading a row group the scan asks each pushed predicate whether the group's zone map could possibly satisfy it. If not, its column chunks are never read. A conjunction prunes when either half rules the group out; a disjunction only when both do; anything the zone maps cannot decide reads the group. Tests assert the exact read/pruned split, and one asserts the answer is identical whether the table is in memory or on disk — pruning that changes an answer is a bug, not an optimisation.
Sort: three modes
LIMIT— memory bounded byk, not by the input. A test asserts it agrees with a full sort then truncate.Ties keep input order (stable sort), so results are reproducible run to run.
Hash join
One side is built, the other streams past it. For inner and cross joins the smaller estimate is built — the build side is what has to fit in memory. For outer joins the choice is forced: the side that may be NULL-padded has to be the one that can be scanned for non-matches at the end, so the operator overrides whatever the planner suggested. Output column order is left-then-right either way, and a test asserts an inner join gives identical results with either build side.
All five join types work: inner, left, right, full and cross.
Fixed along the way
A projection producing no columns dropped its row count, which would have made
SELECT count(*)return zero after column pruning removed every column. Caught by a test written for the case, not in passing.Verification. 105 unit tests plus 43 end-to-end tests. Workspace line coverage is 96%.