// 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 planimport ()// patternVarNames is the variable names a pattern binds (nodes + named// relationships), for connectivity and WHERE-dependency tests.func patternVarNames( *ast.Pattern) []string {var []stringif .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] {return0 }if .Var != "" {if := idSeekLiteral(, .Var); != nil && .Kind == ast.LitInt {return1 } }iflen(.Labels) > 0 { := .Labels[0] := int64(-1)for := range .Props { := .Props[].Valif .Kind == ast.LitParam || .Kind == ast.LitNamedParam || .Kind == ast.LitNull {continue } := int64(seekCard(, , .Props[].Key, semantics.LitValue()))if < 0 || < { = } }if >= 0 {returnuint64() }return .LabelCardinality() }returnuint64(.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, , , ) := 0for := 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 {iflen(.Types) == 0 {return1.0 } := 0.0for , := range .Types { += .AvgDegree(, ) }if <= 0 {return1.0 }return } := 1.0for := - 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 {returntrue } := make(map[string]int, len()+8)for := range { [] = 0 }for , := rangepatternVarNames() {if , := []; ! { [] = 0 } } := map[string]bool{}collectAllVars(, )for := range {if [] {if , := []; ! {returnfalse } } }returnsemantics.CheckRefs(, ) == nil}// specBinds adds the variables a spec binds to out (conservative).func specBinds( *stageSpec, map[string]bool) {switch .kind {casespecMatch:for , := rangepatternVarNames(.pattern) { [] = true }if .pathVar != "" { [.pathVar] = true }casespecShortest: [.pathVar] = truefor , := rangepatternVarNames(.pattern) { [] = true }casespecCall:for , := range .yields { := .Aliasif == "" { = .Field } [] = true }casespecUnwind: [.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()) := 0for < len() {if !(&[]) {specBinds(&[], ) = append(, []) ++continue } := for < len() && (&[]) { ++ } := make([]stageSpec, -)copy(, [:])forlen() > 0 {typestruct {uint8uint64float64int } := -1varfor := range { := &[]if !whereSatisfiable(.where, .pattern, , ) {continue } := falsefor , := rangepatternVarNames(.pattern) {if [] { = truebreak } } , := 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. := 0if >= 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) := truefor := 1; < ; ++ {if || [] < { , = , [] = false } }if || > {returnnil, 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 {returnnil, nil, false } := math.Inf(1)for := 1; < ; ++ {if [] == 0 {if := patternFanoutFrom(, , ); < { , = , } } } := math.Inf(1)if [0] == 0 { = patternFanoutFrom(, 0, ) }if [] == 0 { = min(, patternFanoutFrom(, , )) }if >= {returnnil, 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 {returnsplitAnchors(, , , 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 {returnsplitAnchors(, , , 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) >= 2if && ! {for := range .pattern.Hops {if .pattern.Hops[].Rel.Length != nil { = falsebreak } } }if && && .acyclic { = false }if {if , , := trySplitInterior(, .pattern, .where, , , , .scope, .walk); {specBinds(, )specBinds(, ) = append(, *, *)continue } }specBinds(, ) = append(, *) }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.