// Join reordering and interior-anchor splitting (split from cost.go for // the file-size norm): the cost pre-passes that run before slot // assignment, plus their name-based selectivity and correlation guards.
package plan import ( ) // patternVarNames is the variable names a pattern binds (nodes + named // relationships), for connectivity and WHERE-dependency tests. func patternVarNames( *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 } // nodeAnchorCostNamed is the name-based analogue of anchorCard for the // reorder phase (which runs before slot assignment). func nodeAnchorCostNamed( *ast.NodePat, ast.Expr, map[string]bool, graph.Graph) uint64 { if .Var != "" && [.Var] { return 0 } if .Var != "" { if := idSeekLiteral(, .Var); != nil && .Kind == ast.LitInt { return 1 } } if len(.Labels) > 0 { := .Labels[0] := int64(-1) for := range .Props { := .Props[].Val if .Kind == ast.LitParam || .Kind == ast.LitNamedParam || .Kind == ast.LitNull { continue } := int64(seekCard(, , .Props[].Key, semantics.LitValue())) if < 0 || < { = } } if >= 0 { return uint64() } return .LabelCardinality() } return uint64(.NodeCount()) } // patternAnchorCostNamed is the cost of the pattern's best anchor given // the bound names: the most selective of ALL its nodes. Interior nodes // count because a bound/selective interior re-roots the pattern (the // bound-aware split pass after reordering), so it is a real start. // Returns the cost and the winning node position (0 = start). func patternAnchorCostNamed( *ast.Pattern, ast.Expr, map[string]bool, graph.Graph) (uint64, int) { := nodeAnchorCostNamed(&.Start, , , ) := 0 for := range .Hops { if := nodeAnchorCostNamed(&.Hops[].Node, , , ); < { , = , +1 } } return , } // patternFanoutFrom estimates the expansion fan-out of matching p rooted // at node position k: the product of each hop's average degree walking // left of k (flipped) and right of k (forward) -- the anchored analogue // of pathCost, used to break anchor-cost ties (two patterns may both // start at a bound node yet differ by orders of magnitude in the rows // they produce). Unknown/any-type hops count as a neutral 1. func patternFanoutFrom( *ast.Pattern, int, graph.Graph) float64 { := func( *ast.RelPat, graph.Direction) float64 { if len(.Types) == 0 { return 1.0 } := 0.0 for , := range .Types { += .AvgDegree(, ) } if <= 0 { return 1.0 } return } := 1.0 for := - 1; >= 0; -- { *= (&.Hops[].Rel, revDir(mapDir(.Hops[].Rel.Dir))) } for := ; < len(.Hops); ++ { *= (&.Hops[].Rel, mapDir(.Hops[].Rel.Dir)) } return } // whereSatisfiable reports whether a WHERE is applicable once this pattern // binds: every reference resolvable, and no correlation on a query // variable not yet in scope (which would silently flip a correlated // subquery to uncorrelated). func whereSatisfiable( ast.Expr, *ast.Pattern, map[string]bool, map[string]bool) bool { if == nil { return true } := make(map[string]int, len()+8) for := range { [] = 0 } for , := range patternVarNames() { if , := []; ! { [] = 0 } } := map[string]bool{} collectAllVars(, ) for := range { if [] { if , := []; ! { return false } } } return semantics.CheckRefs(, ) == nil } // specBinds adds the variables a spec binds to out (conservative). func specBinds( *stageSpec, map[string]bool) { switch .kind { case specMatch: for , := range patternVarNames(.pattern) { [] = true } if .pathVar != "" { [.pathVar] = true } case specShortest: [.pathVar] = true for , := range patternVarNames(.pattern) { [] = true } case specCall: for , := range .yields { := .Alias if == "" { = .Field } [] = true } case specUnwind: [.varName] = true } } // reorderJoins reorders each maximal run of non-OPTIONAL, non-path MATCH // patterns: greedily place the most selective startable pattern first, // then prefer patterns connected to the bound set (original order as the // tie-break), deferring disconnected ones. Everything else is a hard // boundary kept in place. Result-identical: a pattern moves earlier only // when its WHERE stays applicable, and the row set is independent of // binding order. func reorderJoins( []stageSpec, []string, graph.Graph) []stageSpec { := make(map[string]bool, len()+8) := make(map[string]bool, len()+8) for , := range { [] = true [] = true } for := range { specBinds(&[], ) } := func( *stageSpec) bool { return .kind == specMatch && !.optional && .pathVar == "" } := make([]stageSpec, 0, len()) := 0 for < len() { if !(&[]) { specBinds(&[], ) = append(, []) ++ continue } := for < len() && (&[]) { ++ } := make([]stageSpec, -) copy(, [:]) for len() > 0 { type struct { uint8 uint64 float64 int } := -1 var for := range { := &[] if !whereSatisfiable(.where, .pattern, , ) { continue } := false for , := range patternVarNames(.pattern) { if [] { = true break } } , := patternAnchorCostNamed(.pattern, .where, , ) := {: , : patternFanoutFrom(.pattern, , ), : } if ! { . = 1 } if < 0 || less4(., ., ., ., ., ., ., .) { = = } } // Nothing WHERE-satisfiable (shouldn't happen for a valid // query): keep the original order to stay correct. := 0 if >= 0 { = } := [] = append([:], [+1:]...) specBinds(&, ) = append(, ) } = } return } // less4 compares (disconnected, cost, fanout, idx) lexicographically: // startable-and-connected first, then the most selective anchor, then the // smallest expansion fan-out, with the written order as the final // deterministic tie-break. func less4( uint8, uint64, float64, int, uint8, uint64, float64, int) bool { if != { return < } if != { return < } if != { return < } return < } // trySplitInterior splits a linear pattern at its strictly-most-selective // INTERIOR node into two arm-patterns rooted there: the left arm anchor -> // start (relationships flipped) and the right arm anchor -> end (forward). // The shared anchor is scanned in the first arm and bound-referenced in // the second; the WHERE rides the second arm so it applies once everything // binds. Result-identical: identical nodes/rels, re-rooted. func trySplitInterior( int, *ast.Pattern, ast.Expr, map[string]bool, graph.Graph, string, uint32, bool) (*stageSpec, *stageSpec, bool) { := len(.Hops) := func( int) *ast.NodePat { if == 0 { return &.Start } return &.Hops[-1].Node } := make([]uint64, +1) for := 0; <= ; ++ { [] = nodeAnchorCostNamed((), , , ) } := min([0], []) , := 0, uint64(0) := true for := 1; < ; ++ { if || [] < { , = , [] = false } } if || > { return nil, nil, false } if == { // A BOUND interior tying a bound endpoint (both cost 0) is still // worth re-rooting at when its expansion fan-out is strictly // smaller -- anchoring a fully-anchored check pattern at its // high-degree end enumerates that degree per row where the // low-degree root touches a handful (the estimator's own fan-out // metric decides, never query identity). Nonzero-cost ties keep // the endpoint anchor as before. if != 0 { return nil, nil, false } := math.Inf(1) for := 1; < ; ++ { if [] == 0 { if := patternFanoutFrom(, , ); < { , = , } } } := math.Inf(1) if [0] == 0 { = patternFanoutFrom(, 0, ) } if [] == 0 { = min(, patternFanoutFrom(, , )) } if >= { return nil, nil, false } } // The anchor must carry a variable so the second arm can reference it; // synthesize one for an anonymous interior node (safe: the caller // skips this pass under star projections). The prefix keeps the two // split passes' synthetic names from colliding. := *() if .Var == "" { .Var = fmt.Sprintf("%s_%d_%d", , , ) } := ast.Pattern{Start: } for := - 1; >= 0; -- { := .Hops[].Rel .Dir = .Dir.Flipped() .Hops = append(.Hops, ast.PatternHop{Rel: , Node: *()}) } := ast.Pattern{Start: } .Hops = append(.Hops, .Hops[:]...) return &stageSpec{kind: specMatch, pattern: &, scope: , walk: }, &stageSpec{kind: specMatch, pattern: &, where: , scope: , walk: }, true } // splitInteriorAnchors applies trySplitInterior to each eligible MATCH // spec (non-OPTIONAL, non-path, all-simple-hop, >=2 hops), threading the // bound-name set. Composes with reorderJoins (split first, then reorder). func splitInteriorAnchors( []stageSpec, []string, graph.Graph) []stageSpec { return splitAnchors(, , , false, "__ia") } // splitBoundAnchors is the bound-aware split pass AFTER join reordering: // by then earlier patterns have bound variables that can make a pattern's // INTERIOR its most selective node, which endpoint-only anchoring cannot // exploit -- the stage would expand from a huge endpoint and semijoin // back instead of growing outward from the bound interior. Var-length // hops are allowed (flipping a quantified hop's direction preserves its // reachable pairs); ACYCLIC patterns are excluded (the per-segment // node-uniqueness scope must not be re-rooted). func splitBoundAnchors( []stageSpec, []string, graph.Graph) []stageSpec { return splitAnchors(, , , true, "__ba") } // splitAnchors is the shared body of the two split passes. func splitAnchors( []stageSpec, []string, graph.Graph, bool, string) []stageSpec { := make(map[string]bool, len()+8) for , := range { [] = true } := make([]stageSpec, 0, len()) for := range { := &[] := .kind == specMatch && !.optional && .pathVar == "" && len(.pattern.Hops) >= 2 if && ! { for := range .pattern.Hops { if .pattern.Hops[].Rel.Length != nil { = false break } } } if && && .acyclic { = false } if { if , , := trySplitInterior(, .pattern, .where, , , , .scope, .walk); { specBinds(, ) specBinds(, ) = append(, *, *) continue } } specBinds(, ) = append(, *) } return }