// Hash-join extraction: when a segment's bind chain re-executes an// independent branch of the pattern per row of a large intermediate (the// branch reads nothing the intermediate bound), nesting multiplies the two// branches' costs. This pass rewrites that shape into a HashJoinStage --// build the independent branch once, keyed by the variable a connecting// expand joins it back through, and probe per outer row -- purely on// estimated cardinality structure, never on query identity. It runs after// markRelUniqueness so every op keeps the uniqueness flags of the original// nested order; the executor's capture/replay protocol (see exec) makes// the reorganized row multiset exactly the nested one.
package planimport ()// tryHashJoin attempts the rewrite at pivot stage k; nil when the shape// does not qualify.func tryHashJoin( []Stage, int, float64, map[int]float64, map[int]int, map[int]graph.NodeID, map[string]int, graph.Graph) []Stage { := / HashJoinExtDivisor := func( int) bool { , := []return && [] < && <= } := func( int) bool { , := []return && [] < }// The collection region: consecutive stages from the pivot; OPTIONAL // stages pass through untouched (their bindings and reads never // interact with the branch -- enforced below), any other stage kind or // a path bind is a hard barrier. := for < len() { , := [].(*MatchStage)if ! || .PathBind != nil {break } ++ }// Op-level dependency-driven collection: an op joins the branch when // everything it reads is external-qualified or branch-bound, with // var-expand shapes whose uniqueness participation cannot be captured // as per-row pairs left post-probe (exact there: the full used-pair // stack is live).typestruct{ , int }var []varmap[]boolvar , map[int]bool// collect runs the op-level dependency-driven collection over stages // [k, lim): an op joins the branch when everything it reads is // external-qualified or branch-bound. Callable twice -- the keyless // (cartesian) fallback re-collects with lim = k+1 so the build is the // pivot stage's own component only, and recursion extracts the rest. := func( int) { = [:0] = make(map[]bool) = make(map[int]bool) = make(map[int]bool) := func( int) bool {if [] {returntrue }if () { [] = truereturntrue }returnfalse }var []intfor := ; < ; ++ { := [].(*MatchStage)if .Optional {continue }for := range .Ops { := &.Ops[] = opReads(, [:0]) := truefor , := range {if !() { = falsebreak } }// A tracked var-expand joins the build only in its // per-trail bounded contributing form: a check-only or // dedup'd (reach-shaped) walk's output depends on the // row's LIVE uniqueness pairs, which a keyed build cannot // see, so those decline. The known mechanism that could // admit the check-only reach class -- capture each // trail's pair set at build with an empty exclusion, then // reconstruct the endpoint set per row by emitting an // endpoint iff some captured trail has no scope-live pair // -- is deliberately not built: the sibling engine // measured ~1.05x on its best case, and no workload here // pays for this decline yet.if && .Kind == OpVarExpand && .Uniq != nil && !(.Max != nil && .Uniq.Contribute && !.DedupEndpoints) { = false }if ! {continue } := {, } = append(, ) [] = trueif .Kind == OpScan {if .Source.Kind != ScanArg { [.Slot] = true } } else {if !.Rebind { [.To] = true }if .RelSlot != NoSlot { [.RelSlot] = true } } } } } ()iflen() == 0 {returnnil }// Key discovery: the connecting expand is the first non-collected // anonymous rebind expand joining a branch-bound endpoint to a // pre-pivot outer one. Its B-side endpoint keys the table; the probe // runs it from the outer endpoint (direction flipped when the pattern // hop ran from the branch side -- uniqPair is reversal-invariant).varBindOp := false := -1 := {-1, -1}for := ; < && < 0; ++ { := [].(*MatchStage)if .Optional {continue }for := range .Ops { := &.Ops[]if [{, }] || .Kind != OpExpand || !.Rebind || .RelSlot != NoSlot {continue } , := [.From], [.To]switch {case && (.To): = * .From, .To = .To, .From .Dir = revDir(.Dir) = true = .Fromcase && (.From): = * = .Todefault:continue } .Rebind = false = {, }break } }// Value-key fallback (the disconnected-components join): no expand // connects the branch, but a region WHERE equality whose sides split // cleanly -- one reading only branch/external slots, the other only // pre-pivot outer ones -- keys the table by VALUE. The conjunct is // consumed (it IS the join), never a stray; nulls never match, exactly // the equality's own semantics.var , ast.Exprvarast.Exprif < 0 {for := ; < && == nil; ++ { := [].(*MatchStage)if .Optional || .Where == nil {continue }var []ast.ExprSplitAnd(.Where, &)for , := range { , := .(*ast.Binary)if ! || .Op != ast.OpEq {continue } := func( ast.Expr) (, bool) { := conjSlotRefs(, )iflen() == 0 {returnfalse, false } , = true, truefor , := range {if ![] && !() { = false }if !() { = false } }return , } , := (.LHS) , := (.RHS)switch {case && : , = .LHS, .RHScase && : , = .RHS, .LHSdefault:continue }// The build side's external reads join the memo tuple like // an op's would.for , := rangeconjSlotRefs(, ) {if ![] { [] = true } } = break } } }// Keyless fallback: nothing relates the branch at all -- a genuine // cartesian product. Build once; the probe emits every build row per // outer row. Output is the same product the nested loop produced; the // win is the branch's scan not re-running per outer row. := falseif < 0 && == nil {// Build-once only pays when there IS an outer to repeat against: // a pivot whose input is the single seed row runs its scan once // either way, so wrapping it in a join is a no-op with overhead.if <= 1 {returnnil } = true// Narrow the build to the pivot stage's own component: collecting // every satisfiable op would swallow sibling disconnected branches // into ONE build whose internal nesting still re-scans -- each // component gets its own build-once via the recursion instead. ( + 1)iflen() == 0 {returnnil } }// The materialized branch must fit the memory bound (the win itself // is guaranteed by the pivot gate's multiply). := make([]BindOp, 0, len())for , := range { = append(, [.].(*MatchStage).Ops[.]) } := &MatchStage{Ops: , Scope: [].(*MatchStage).Scope}if , := matchEstAnchored(, 1, , ); > float64(HashJoinMaxBuildRows) {returnnil }// Split each region stage's WHERE: a conjunct evaluates inside the // build only when it reads nothing beyond the branch and the external // slots the collected OPS already require -- a conjunct must never // admit a new external slot, because every external slot joins the // memo tuple and multiplies rebuilds (ops are structural anchors; // conjuncts are movable filters that evaluate post-probe for free). // The rest ("strays") are re-placed post-probe below.typestruct {ast.Expr []int }var []var []ast.Exprfor := ; < ; ++ { := [].(*MatchStage)if .Optional || .Where == nil {continue }var []ast.ExprSplitAnd(.Where, &)for , := range {if == {continue// the value join's key equality, not a filter } := conjSlotRefs(, ) := truefor , := range {if ![] && ![] { = falsebreak } }if { = append(, ) } else { = append(, {: , : }) } } } .Where = andJoin() := make([]int, 0, len())for := range { = append(, ) }slices.Sort() := make([]int, 0, len())for := range { = append(, ) }slices.Sort() := &HashJoinStage{Build: []*MatchStage{},ExtSlots: ,KeySlot: ,PayloadSlots: ,Probe: ,Reversed: ,KeyBuild: ,KeyProbe: ,Cartesian: , }// Reassemble: pivot-region stages keep their leftover ops in place // (dependencies were never reordered), the consumed connecting expand // and collected ops leave, empty stages vanish. newBinder records // which surviving region stage binds each remaining fresh slot, for // stray placement. := make([]Stage, 0, len()+2) = append(, [:]...) = append(, ) := make(map[int]int) := make(map[int]bool)for := ; < ; ++ { := [].(*MatchStage)if .Optional {for := range .Ops { := &.Ops[]if := freshBind(); >= 0 { [] = len() [len()] = true }if .Kind != OpScan && .RelSlot != NoSlot { [.RelSlot] = len() [len()] = true } } = append(, )continue }var []BindOpfor := range .Ops { := {, }if [] || == {continue } := .Ops[]if := freshBind(&); >= 0 { [] = len() }if .Kind != OpScan && .RelSlot != NoSlot { [.RelSlot] = len() } = append(, ) }iflen() == 0 {continue } = append(, &MatchStage{Ops: , Optional: false, Scope: .Scope}) } = append(, [:]...)// Stray placement: each cross-branch conjunct attaches at its latest // reference's binder -- the join stage when every reference is outer- // bound or payload-bound, else the surviving region stage that binds // the last reference. Attaching to an OPTIONAL stage would change its // left-join condition, and a reference bound past the region cannot // occur (a stage WHERE's references are bound by its own stage), so // either voids the rewrite.for , := range { := -1for , := range . {if [] || () {continue } , := []if ! {returnnil } = max(, ) }switch {case < 0: .Where = andWith(.Where, .)case []:returnnildefault: := [].(*MatchStage) .Where = andWith(.Where, .) } }return}// freshBind is the slot an op newly binds (-1 when it only re-anchors or// joins into an already-bound slot).func freshBind( *BindOp) int {if .Kind == OpScan {if .Source.Kind == ScanArg {return -1 }return .Slot }if .Rebind {return -1 }return .To}// andWith conjoins extra onto base (base may be nil).func andWith(, ast.Expr) ast.Expr {if == nil {return }return &ast.Binary{Op: ast.OpAnd, LHS: , RHS: }}// opReads appends the slots an op reads before binding anything.func opReads( *BindOp, []int) []int {switch .Kind {caseOpScan:switch .Source.Kind {caseScanArg, ScanNodeIDVar: = append(, .Source.Slot)caseScanExistsSeed:for := range .Source.Seeds { = append(, .Source.Seeds[].AnchorSlot) } }default: = append(, .From)if .Rebind { = append(, .To) } }return}// conjSlotRefs is the segment slots a conjunct references (names not in// the slot map -- e.g. an EXISTS pattern's local variables -- are ignored;// a collision with an outer name only delays placement, never advances// it).func conjSlotRefs( ast.Expr, map[string]int) []int { := map[string]bool{}collectAllVars(, ) := make([]int, 0, len())for := range {if , := []; { = append(, ) } }slices.Sort()return}// andJoin folds conjuncts back into one AND tree (nil for none).func andJoin( []ast.Expr) ast.Expr {varast.Exprfor , := range {if == nil { = } else { = &ast.Binary{Op: ast.OpAnd, LHS: , RHS: } } }return}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.