optimizer: add prefix-unique join order scoring behind a flag#37523
Draft
frankmcsherry wants to merge 3 commits into
Draft
optimizer: add prefix-unique join order scoring behind a flag#37523frankmcsherry wants to merge 3 commits into
frankmcsherry wants to merge 3 commits into
Conversation
FK chain, star, and snowflake shapes, unindexed and indexed, with selective-filter variants. The captured plans document how join ordering currently scores equivalence edges traversed backwards (unique side placed, foreign-key side incoming): bounded by the incoming relation's size, but scored without any uniqueness credit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Join ordering scores a candidate only by target-side uniqueness: an edge into a relation whose bound columns cover one of its unique keys cannot grow the result. The dual signal was missing. When the placed prefix is unique on the equated expressions, each incoming row matches at most one prefix row, so the result is bounded by the incoming relation's size. Foreign-key equivalences traversed backwards (unique side placed, foreign-key side incoming) are exactly this shape, and delta join paths starting from the "many" end of a chain cannot avoid them. The Orderer now tracks unique keys of the placed prefix, folding each placement in with the standard join key rule (forward-unique keeps the old keys, reverse-unique adopts the incoming relation's keys, otherwise pairwise unions, which is what lets the bound compose along chains). Candidates gain a `prefix_unique` characteristic, ranked just below `unique_key`, in a new `JoinInputCharacteristicsV3` selected by the default-off, cluster-scoped flag `enable_join_reverse_edge_scoring`. EXPLAIN renders the new field as `P`, and accepts the flag as a WITH option for per-statement replanning. With the flag off, plans are unchanged: rewriting the new slt corpus reproduces every baseline byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The differential and delta planners each called optimize_orders with identical inputs, computing the same N greedy orders twice per join. Compute them once in action and pass them in, which retires the TODO(mgree) noting exactly this. Error handling is preserved: the first-run path panics on order-construction failure as the differential planner did, and the second-run upgrade path remains Ok-gated. Also replace the order tie-break's max_by_key(|o| o.clone()) with max_by(cmp), same selection without cloning every candidate order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Motivation
Join ordering scores each candidate order extension only by target-side uniqueness:
unique_keysays the incoming relation is unique on its bound columns, so the join cannot grow the carried prefix. The dual signal is missing. When the placed prefix is unique on the equated expressions, each incoming row matches at most one prefix row, so the extended result is bounded by the incoming relation's size. Foreign-key equivalences traversed backwards (unique side placed, fk side incoming) are exactly this shape, the bound composes down FK chains, and delta join paths starting from the "many" end of a chain cannot avoid such edges. Today they score as unbounded mysteries: in a chaina.b_fk = b.b_pk AND b.c_fk = c.c_pkwith everything indexed, the path fromcreads%2:c » %1:b[c_fk]KA » %0:a[b_fk]KA, indistinguishable from a genuine many-to-many hazard.Change
The
Orderernow tracks unique keys of the placed prefix, folding in each placement with the standard join key rule: forward-unique keeps the old keys, reverse-unique adopts the incoming relation's keys, and otherwise pairwise unions (which is what lets the bound compose along chains). Candidates gain aprefix_uniquecharacteristic ranked immediately belowunique_key, in a newJoinInputCharacteristicsV3following the V1/V2 versioning precedent. V3 is selected by a new default-off, cluster-scoped flagenable_join_reverse_edge_scoring, and builds on V2 (enable_join_prioritize_arranged) ordering. EXPLAIN prints the new field asPand acceptsenable join reverse edge scoringas a WITH option.A cute corollary: a cross join against a prefix with the empty unique key (at most one row, e.g. a scalar aggregate) counts as bounded, so singleton cross joins stop being feared.
Known conservatism, documented in the code: characteristics are computed at push time, so a queued candidate does not benefit from prefix keys established after its last push. Sound (never overclaims), just conservative.
Verification
transform/,explain/,joins.slt,chbench.slt, and both TPCH files, and rewriting the new corpus reproduces every baseline byte-identical.test/sqllogictest/transform/join_reverse_edges.slt, FK chain / star / snowflake shapes with captured plans flag-off and flag-on.mz-expr,mz-sql-parser, andmz-sql(including the optimizer-features vars invariant) pass.Evidence from existing corpora (flag on)
Three-way attribution over 110 EXPLAIN queries in chbench, TPCH (index + MV), joins, and ldbc_bi, comparing V1 (production default), V2, and V3, with the
Pmarker stripped before comparison so cosmetic changes don't count: 22 identical, 36 marker-only, 4 changes inherited from V2's arranged-first ranking, 44 reranked by the new signal (plus 4 mixed). Sampled judgments: binary sub-joins now start from the bounded side (no cost, both sides of a differential join are arranged anyway). The tradeoff deserving review: on delta paths,PaboveAmeans a bounded-unarranged key can beat an arranged-unbounded key, buying a cardinality bound at the price of an arrangement, the same philosophy asunique_key > arrangedin V2 extended one level.Open questions
enable_join_prioritize_arranged?prefix_unique > arrangedthe right rank, given the arrangement-vs-bound tradeoff above?CREATE CLUSTER ... FEATURESis deliberately not wired for the new flag (EXPLAIN WITH and the system var are). Should it matchenable_join_prioritize_arranged?🤖 Generated with Claude Code