// OPTIONAL-MATCH group-join decorrelation (port of the Rust sibling's // technique): when an OPTIONAL MATCH's bindings are consumed ONLY through // bare null-skipping duplicate-decomposable aggregates grouped by outer // variables, the clause is semantically a grouped left join whose inner // pattern can be planned as a standalone query -- free to take its own // selective anchor, where the nested walk is chained to the outer rows' // bindings. The inner executes once into a key -> aggregate-values table, // each outer row binds synthetic columns by correlation-key lookup with // the aggregates' empty-group identities as the fill, and the segment // projection re-aggregates those columns (count -> sum), which reproduces // the nested left-join answer exactly, including over duplicate outer // rows. // // The rewrite's dual: a SELECTIVE outer's nested walk is cheap while the // standalone inner enumerates globally, so it is gated on the estimated // outer breadth -- below the floor the nested execution stays. An // inner-side estimate gate was tried and rejected on the Rust side: the // estimator prices the inner at raw average fan, blind to the selective // anchor that actually runs.
package plan import ( ) // GroupJoinMinOuterRows is the estimated-outer-breadth floor below which // the nested OPTIONAL execution stays (a variable so tests can force the // rewrite onto fixture-scale graphs). var GroupJoinMinOuterRows = 1024.0 // GroupJoinMinCoverage is the minimum fraction of the correlation // variable's population the outer must reach. The standalone inner // enumerates its pattern for EVERY correlation node while the nested walk // only expands the outer's slice, so their cost ratio IS the coverage: a // broad-but-narrow outer (thousands of rows over millions of candidates) // keeps the cheap nested walk (measured: a tag-seek outer of ~4k messages // against 3M messages regressed 65x under a breadth-only gate). var GroupJoinMinCoverage = 0.5 // gjAgg is one consumed aggregate: the projection item it rewrites, the // aggregate function, and the output column name it must keep. type gjAgg struct { item int fn string name string } // gjCandidate is a detected group-join shape: the optional spec's index, // the correlation variables (outer vars the pattern reads, in first- // appearance order), their labels as written in the outer patterns // (first labeled occurrence; "" when never labeled -- the coverage gate // then prices the population at the whole node count), and the consumed // aggregates. type gjCandidate struct { specIdx int corr []string corrLabels []string aggs []gjAgg // repetition is the gate's estimated outer rows per correlation-key // TUPLE: breadth over the product of the correlation labels' // populations. ~1 or below means each key tuple appears at most // once (no re-walk for the table build to amortize), >1 means the // nested drive would re-walk shared keys. repetition float64 } // exprVars collects every variable an expression reads (bare references // and property bases, subquery interiors included via the shared walker). func exprVars( ast.Expr) []string { var []string ast.Walk(, func( ast.Expr) bool { switch n := .(type) { case *ast.Var: = append(, .Name) case *ast.Prop: = append(, .Var) } return true }) return } // patternVars lists a pattern's variable names in appearance order // (duplicates kept; callers dedup). func patternVars( *ast.Pattern) []string { var []string if .Start.Var != "" { = append(, .Start.Var) } for := range .Hops { if := .Hops[].Rel.Var; != "" { = append(, ) } if := .Hops[].Node.Var; != "" { = append(, ) } } return } // detectGroupJoin recognizes the decorrelatable shape over the final spec // list and the projection: the LAST spec is a single-pattern OPTIONAL // MATCH whose fresh bindings feed only qualifying aggregates. Everything // else declines -- count(*) (it counts the null fill row), DISTINCT or // unsupported aggregates, aggregates over outer-only arguments (they // count join multiplicity the standalone inner cannot see), nested // aggregates, inner variables escaping through group keys or ORDER BY, // an uncorrelated optional, and WHERE reads beyond the pattern's own // variables. func detectGroupJoin( []stageSpec, *ast.Projection, []string) *gjCandidate { := len() - 1 if < 1 { return nil } := &[] if .kind != specMatch || !.optional || .pathVar != "" || .walk || .pattern == nil { return nil } := map[string]bool{} for , := range { [] = true } for := range { := &[] // A sibling sharing the optional's relationship-uniqueness scope // (comma patterns in one MATCH) excludes its used rels from the // nested walk -- an exclusion the standalone inner cannot see. if .kind == specMatch && .scope == .scope { return nil } switch .kind { case specMatch, specShortest: for , := range patternVars(.pattern) { [] = true } if .pathVar != "" { [.pathVar] = true } case specUnwind: [.varName] = true default: // CALL yields / subquery outputs would need their own var // accounting; decline rather than guess. return nil } } := map[string]bool{} := map[string]bool{} var []string for , := range patternVars(.pattern) { if [] { continue } [] = true if [] { = append(, ) } else { [] = true } } if len() == 0 || len() == 0 { return nil } // The optional's WHERE moves into the standalone inner, so it may read // only the pattern's own variables. if .where != nil { for , := range exprVars(.where) { if ![] { return nil } } } if .Star || .Distinct { return nil } := func( ast.Expr) bool { for , := range exprVars() { if [] { return true } } return false } var []gjAgg for := range .Items { := &.Items[] if , := .Expr.(*ast.Func); && semantics.IsAggName(.Name) { switch .Name { case "count", "sum", "min", "max": default: return nil } if .Distinct || .Star || len(.Args) != 1 || semantics.ExprHasAgg(.Args[0]) { return nil } := false for , := range exprVars(.Args[0]) { if ![] { return nil } if [] { = true } } if ! { return nil } := .Alias if == "" { = semantics.DerivedName(.Expr) } = append(, gjAgg{item: , fn: .Name, name: }) continue } if (.Expr) { return nil } } if len() == 0 { return nil } for , := range .OrderBy { if (.Expr) { return nil } } := make([]string, len()) for , := range { [] = varLabelIn([:+1], ) } return &gjCandidate{specIdx: , corr: , corrLabels: , aggs: } } // gjAnchorDeclines counts inner plans refused as unselective with no // outer repetition to amortize the table, so tests can assert the // discriminator engaged. DisableGJAnchorDecline pins differential tests // to the always-admit behavior the discriminator replaced. var ( gjAnchorDeclines int DisableGJAnchorDecline bool ) // gjInnerAnchorSelective reports whether the standalone inner plan // drives from a value seek. A seek is the rewrite's winning case: the // per-outer-row nested drive cannot use it, so the standalone inner // genuinely walks less. A label or all-nodes anchor walks the same // relationships the nested drive would (merely from the other end), so // selectivity is the benefit's existence test. An unrecognized shape // reports selective (the rewrite stays admitted, the pre-discriminator // behavior). func gjInnerAnchorSelective( *Plan) bool { if len(.Branches) != 1 || len(.Branches[0]) == 0 { return true } := .Branches[0][0] if len(.Stages) == 0 { return true } , := .Stages[0].(*MatchStage) if ! || len(.Ops) == 0 || .Ops[0].Kind != OpScan { return true } switch .Ops[0].Source.Kind { case ScanLabel, ScanAll: return false } return true } // varLabelIn finds v's first written label across the specs' patterns // ("" when v is never labeled). func varLabelIn( []stageSpec, string) string { := func( *ast.NodePat) string { if .Var == && len(.Labels) > 0 { return .Labels[0] } return "" } for := range { := [].pattern if == nil { continue } if := (&.Start); != "" { return } for := range .Hops { if := (&.Hops[].Node); != "" { return } } } return "" } // gjGate is the rewrite's economics check over the built outer prefix: // the estimated breadth must clear the absolute floor AND cover enough of // every correlation variable's population that running the standalone // inner over ALL of it beats re-walking the nested pattern per outer row. func gjGate( *gjCandidate, []Stage, graph.Graph) bool { := gjOuterBreadth(, ) if < GroupJoinMinOuterRows { return false } // The repetition estimate divides breadth by the key DOMAIN -- the // PRODUCT of the correlation labels' populations -- not any single // label's. A multi-key outer (a cross of two labels, say) has one // row per key TUPLE even though breadth is many multiples of each // label alone; dividing per-label reads that as amortizable // repetition and false-admits at every size (the Rust sibling's // 80e59cc, confirmed against this arm). := 1.0 for , := range .corrLabels { := float64(.NodeCount()) if != "" { = float64(.LabelCardinality()) } if < GroupJoinMinCoverage* { return false } *= } .repetition = / return true } // gjOuterBreadth is the estimated row count leaving the already-built // stage prefix -- the group-join gate's outer breadth. Hop fan-outs use // the LABEL-CONDITIONAL average degree whenever the hop's source label is // known, falling back to the global average: the global statistic // inflates for every label merely touching a hub-heavy type, and an // inflated breadth fires the rewrite over a selective outer whose nested // walk was cheap (measured: two tag/country-seek chains estimated past // the floor globally regressed 65x and 210x before this refinement while // their conditional estimates sit two decades under it). func gjOuterBreadth( []Stage, graph.Graph) float64 { := 1.0 := map[int]string{} for , := range { , := .(*MatchStage) if ! { continue } , := anchorFirstHopDegree(, ) for := range .Ops { := &.Ops[] if .Kind == OpScan { if len(.Labels) > 0 { [.Slot] = .Labels[0] } if .Source.Kind != ScanArg { *= float64(scanCard(&.Source, .Props, )) } continue } // The anchor's resolved degree beats any average; a // label-conditional average beats the global one. var *float64 if == 1 && { = & } else if , := [.From]; { if , := condFanout(, .Types, .Dir, ); { = & } } = opEst(, , , ) if len(.Labels) > 0 { [.To] = .Labels[0] } } if .Where != nil { *= whereSel } } return } // condFanout sums the label-conditional average degree over types; // ok=false when no type has a conditional statistic (the caller falls // back to the global fan-out). func condFanout( string, []string, graph.Direction, graph.Graph) (float64, bool) { , := 0.0, false for , := range { if , := .AvgDegreeByLabel(, , ); { += = true } } return , } // buildGroupJoinStage synthesizes the standalone inner query (the optional // pattern and WHERE, projecting the correlation keys then the aggregates), // plans it, binds the synthetic output slots, and rewrites the caller's // projection items to re-aggregate them. All caller-visible mutation // happens only after the inner plan built successfully, so a decline falls // back to the nested execution untouched. The caller must have unshared // projAST.Items before calling (the ast is shared across plannings). func buildGroupJoinStage( *gjCandidate, *stageSpec, *ast.Projection, map[string]int, map[int]bool, *int, graph.Graph) (*GroupJoinStage, error) { := make([]ast.ReturnItem, 0, len(.corr)+len(.aggs)) for , := range .corr { = append(, ast.ReturnItem{Expr: &ast.Var{Name: }, Alias: }) } for , := range .aggs { = append(, ast.ReturnItem{Expr: .Items[.item].Expr, Alias: fmt.Sprintf("$gj%d", )}) } := &ast.Query{Parts: []ast.QueryPart{{ Clauses: []ast.Clause{&ast.Match{Patterns: []ast.Pattern{*.pattern}, Where: .where, Acyclic: .acyclic}}, Ret: ast.Projection{Items: }, }}} , := Build(, ) if != nil { return nil, } // The rewrite's benefits are a SELECTIVE inner anchor the nested // per-outer-row drive cannot use, and amortizing repeated // correlation keys into one walk; its cost is the hash table over // every inner row. An inner driving from an unselective label scan // walks the same relationships the nested drive would (merely from // the other end), so when the outer also has no key repetition to // amortize (~one row per key), the table build is pure loss -- // measured 1.27x slower here and 7.6x in the Rust sibling on the // unfiltered whole-label shape. Decline BEFORE the projection // rewrite below, which the nested fallback must see unmutated. if !DisableGJAnchorDecline && .repetition <= 1.05 && !gjInnerAnchorSelective() { gjAnchorDeclines++ return nil, fmt.Errorf("group-join inner is unselective with no outer key repetition") } := &GroupJoinStage{Sub: } for , := range .corr { .KeySlots = append(.KeySlots, []) } for , := range .aggs { := fmt.Sprintf("$gj%d", ) := * *++ [] = [] = true .OutSlots = append(.OutSlots, ) , := FillNull, .fn if .fn == "count" { , = FillZero, "sum" } .Fills = append(.Fills, ) .Items[.item] = ast.ReturnItem{ Expr: &ast.Func{Name: , Args: []ast.Expr{&ast.Var{Name: }}}, Alias: .name, } } return , nil }