// 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 planimport ()// 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).varGroupJoinMinOuterRows = 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).varGroupJoinMinCoverage = 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 []stringast.Walk(, func( ast.Expr) bool {switch n := .(type) {case *ast.Var: = append(, .Name)case *ast.Prop: = append(, .Var) }returntrue })return}// patternVars lists a pattern's variable names in appearance order// (duplicates kept; callers dedup).func patternVars( *ast.Pattern) []string {var []stringif .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() - 1if < 1 {returnnil } := &[]if .kind != specMatch || !.optional || .pathVar != "" || .walk || .pattern == nil {returnnil } := 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 {returnnil }switch .kind {casespecMatch, specShortest:for , := rangepatternVars(.pattern) { [] = true }if .pathVar != "" { [.pathVar] = true }casespecUnwind: [.varName] = truedefault:// CALL yields / subquery outputs would need their own var // accounting; decline rather than guess.returnnil } } := map[string]bool{} := map[string]bool{}var []stringfor , := rangepatternVars(.pattern) {if [] {continue } [] = trueif [] { = append(, ) } else { [] = true } }iflen() == 0 || len() == 0 {returnnil }// The optional's WHERE moves into the standalone inner, so it may read // only the pattern's own variables.if .where != nil {for , := rangeexprVars(.where) {if ![] {returnnil } } }if .Star || .Distinct {returnnil } := func( ast.Expr) bool {for , := rangeexprVars() {if [] {returntrue } }returnfalse }var []gjAggfor := range .Items { := &.Items[]if , := .Expr.(*ast.Func); && semantics.IsAggName(.Name) {switch .Name {case"count", "sum", "min", "max":default:returnnil }if .Distinct || .Star || len(.Args) != 1 || semantics.ExprHasAgg(.Args[0]) {returnnil } := falsefor , := rangeexprVars(.Args[0]) {if ![] {returnnil }if [] { = true } }if ! {returnnil } := .Aliasif == "" { = semantics.DerivedName(.Expr) } = append(, gjAgg{item: , fn: .Name, name: })continue }if (.Expr) {returnnil } }iflen() == 0 {returnnil }for , := range .OrderBy {if (.Expr) {returnnil } } := 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 intDisableGJAnchorDeclinebool)// 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 {iflen(.Branches) != 1 || len(.Branches[0]) == 0 {returntrue } := .Branches[0][0]iflen(.Stages) == 0 {returntrue } , := .Stages[0].(*MatchStage)if ! || len(.Ops) == 0 || .Ops[0].Kind != OpScan {returntrue }switch .Ops[0].Source.Kind {caseScanLabel, ScanAll:returnfalse }returntrue}// 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 { := [].patternif == 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 {returnfalse }// 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.0for , := range .corrLabels { := float64(.NodeCount())if != "" { = float64(.LabelCardinality()) }if < GroupJoinMinCoverage* {returnfalse } *= } .repetition = / returntrue}// 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 {iflen(.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 *float64if == 1 && { = & } elseif , := [.From]; {if , := condFanout(, .Types, .Dir, ); { = & } } = opEst(, , , )iflen(.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, falsefor , := 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 {returnnil, }// 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++returnnil, 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, .fnif .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}
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.