Binder, cost model and the optimiser - #3
Merged
Merged
Conversation
Three layers between a parse tree and something runnable. The binder is the only place that resolves names, so error messages about missing or ambiguous columns all come from one function. An unqualified column matching two tables in a join is an error rather than a first-match win, and a bare column outside GROUP BY is rejected instead of returning an arbitrary row. BETWEEN and the CASE-with-operand form desugar here, so the optimiser and the evaluator only ever see comparisons. The cost model estimates cardinality from the zone maps the storage layer already writes. Its numbers only ever choose between plans, never decide what a query returns, which is why the fallbacks are allowed to be textbook constants. Four rules run over the plan. Constant folding stops short of division by zero and integer overflow so the plan-time answer cannot differ from the run-time one. Predicate pushdown splits conjunctions so each part travels separately, refuses to push into the padded side of an outer join, and turns a comma join with an equality in WHERE into a real inner join. Join reordering puts the smallest relation at the bottom of an inner-join chain, preferring a relation that has a join key over a smaller unrelated one, and restores the original column order above so nothing higher in the tree notices. Projection pushdown narrows every scan to the columns actually read and renumbers everything above it. Every rule has to preserve the output schema, and a test asserts exactly that across a spread of queries.
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.
Parse tree in, optimised logical plan out.
Binder
The single place that answers "does this column exist" and "is this well typed". Once a plan leaves it, every column is a position and every node knows its type, so nothing downstream carries a catalog or produces a name error.
Decisions worth calling out:
SELECT id FROM orders o JOIN customers c ON …is refused. Silently picking one is how a join query returns the wrong column with no complaint.GROUP BYis refused with "must appear in GROUP BY or inside an aggregate" — but only after confirming the column exists, so a typo still reports as a typo.BETWEENandCASE x WHEN …desugar here. Two comparisons are something pushdown can already split, prune with and evaluate; a third node shape would have to be taught to all three.ORDER BYbinds against the pre-projection scope, soSELECT id … ORDER BY amountworks without selectingamount. Ordinals and aliases resolve back to the expression they name.ONclause splits into hash-join keys plus a residual filter. Only an equality with one side reading purely left columns and the other purely right becomes a key; everything else, including an equality within one side, stays a filter, because a hash join cannot build on it.Cost model
Cardinality estimates come from the zone maps the storage layer already writes — no extra pass over data. Equality selectivity is
1/distinctfrom real statistics where a scan provides them, textbook constants above that. The estimates only ever choose between plans; a bad one costs a slower plan, never a wrong answer, and that is the reason the fallbacks are allowed to be crude. Statistics are read through a projected scan, so pruning does not silently point the estimator at the wrong column.Optimiser
Four rules, each of which must preserve both the rows and the output schema. The schema half is what makes them composable, and
optimising_never_changes_the_output_schemaasserts it across a spread of queries.Constant folding evaluates literal arithmetic, applies the boolean identities, cancels double negation and drops dead
CASEbranches — but deliberately stops at division by zero and integer overflow. Folding those would let the plan-time answer differ from the run-time one, which is worse than not folding.Predicate pushdown splits conjunctions so each part travels independently, rewrites predicates through column-only projections, and ends inside the scan — where the predicate becomes a zone-map test and whole row groups stop being read. Two rules matter more than the mechanics:
c.region = 3into the right side of aLEFT JOINdeletes rows that should have come back NULL-padded; there is a test per join type.WHERE— the old-style comma join — becomes a real inner join with a hash-joinable key, rather than staying a full cross product.Join reordering puts the smallest relation at the bottom of an inner-join chain, greedily preferring a relation that has a join key to what is already joined over a smaller unrelated one — an accidental cross product is not something a later choice recovers from. Reordering permutes the output columns, so the rule wraps the result in a projection restoring the original order; the node above cannot tell. Outer-join chains are left alone, since reordering them changes which side gets padded.
Projection pushdown narrows every scan to the columns actually read and renumbers every expression above it. In a columnar format this is the single biggest win available, and the renumbering is the part that has to be exactly right — getting it wrong reads the wrong column and reports a plausible wrong answer. A count-only query still keeps one column so it has rows to count.
Verification. 124 tests in the crate, including one per pushdown rule, one asserting no join key is lost in reordering, and one asserting no cross product is introduced.