package plan
import (
"fmt"
"math"
"github.com/freeeve/gochickpeas/gql/internal/ast"
"github.com/freeeve/gochickpeas/gql/internal/graph"
"github.com/freeeve/gochickpeas/gql/internal/semantics"
)
const whereSel = 0.5
const untypedFanout = 3.0
const unwindFanout = 4.0
type Estimates struct {
Segs []SegEst
}
type SegEst struct {
Stages []StageEst
ProjRows *uint64
PostWhereRows *uint64
AnchorNotes []string
}
type StageEst struct {
Match []uint64
Single *uint64
}
func rnd(x float64 ) uint64 {
if math .IsNaN (x ) || x < 0 {
return 0
}
if math .IsInf (x , 1 ) || x >= float64 (math .MaxUint64 ) {
return math .MaxUint64
}
return uint64 (math .Round (x ))
}
func Estimate (p *Plan , g graph .Graph ) *Estimates {
out := &Estimates {}
for _ , segments := range p .Branches {
for _ , seg := range segments {
out .Segs = append (out .Segs , segEst (seg , g ))
}
}
return out
}
func segEst(seg *Segment , g graph .Graph ) SegEst {
names := stageNames (seg )
rows := 1.0
stages := make ([]StageEst , 0 , len (seg .Stages ))
notes := make ([]string , 0 , len (seg .Stages ))
for _ , stage := range seg .Stages {
switch s := stage .(type ) {
case *MatchStage :
ests , outRows := matchEst (s , rows , g )
rows = outRows
stages = append (stages , StageEst {Match : ests })
notes = append (notes , anchorNote (s , names , g ))
case *UnwindStage :
rows *= unwindFanout
n := rnd (rows )
stages = append (stages , StageEst {Single : &n })
notes = append (notes , "" )
case *HashJoinStage :
bRows := 1.0
for _ , bs := range s .Build {
_, bRows = matchEst (bs , bRows , g )
}
rows *= math .Min (fanout (s .Probe .Types , s .Probe .Dir , g ), math .Max (bRows , 1 ))
if s .Where != nil {
rows *= whereSel
}
n := rnd (rows )
stages = append (stages , StageEst {Single : &n })
notes = append (notes , "" )
default :
n := rnd (rows )
stages = append (stages , StageEst {Single : &n })
notes = append (notes , "" )
}
}
se := SegEst {Stages : stages , AnchorNotes : notes }
se .ProjRows = projEst (&seg .Proj , rows )
if seg .PostWhere != nil && se .ProjRows != nil {
n := rnd (float64 (*se .ProjRows ) * whereSel )
se .PostWhereRows = &n
}
return se
}
func projEst(proj *ProjPlan , rows float64 ) *uint64 {
if proj .Aggregated {
return nil
}
n := rows
if proj .Limit != nil {
n = math .Min (n , float64 (*proj .Limit ))
}
r := rnd (n )
return &r
}
func matchEst(ms *MatchStage , inRows float64 , g graph .Graph ) ([]uint64 , float64 ) {
return matchEstAnchored (ms , inRows , g , nil )
}
func matchEstAnchored(ms *MatchStage , inRows float64 , g graph .Graph , resolved map [int ]graph .NodeID ) ([]uint64 , float64 ) {
rows := inRows
ests := make ([]uint64 , 0 , len (ms .Ops )+1 )
anchorDeg , anchorDegOK := anchorFirstHopDegree (ms , g )
for i := range ms .Ops {
var firstHop *float64
if i == 1 && anchorDegOK {
firstHop = &anchorDeg
} else if resolved != nil {
op := &ms .Ops [i ]
if op .Kind == OpExpand || op .Kind == OpVarExpand {
if n , ok := resolved [op .From ]; ok {
d := resolvedDegree ([]graph .NodeID {n }, true , op .Types , op .Dir , g )
dv := float64 (d .val )
firstHop = &dv
}
}
}
rows = opEst (&ms .Ops [i ], rows , firstHop , g )
ests = append (ests , rnd (rows ))
}
if ms .Where != nil {
rows *= whereSel
ests = append (ests , rnd (rows ))
}
return ests , rows
}
func anchorFirstHopDegree(ms *MatchStage , g graph .Graph ) (float64 , bool ) {
if len (ms .Ops ) < 2 || ms .Ops [0 ].Kind != OpScan {
return 0 , false
}
scan := &ms .Ops [0 ]
hop := &ms .Ops [1 ]
if hop .Kind != OpExpand && hop .Kind != OpVarExpand {
return 0 , false
}
nodes , ok := resolveScanNodes (&scan .Source , scan .Labels , scan .Props , g )
if !ok || len (nodes ) == 0 {
return 0 , false
}
d := resolvedDegree (nodes , true , hop .Types , hop .Dir , g )
return float64 (d .val ), true
}
func opEst(op *BindOp , rows float64 , firstHopDeg *float64 , g graph .Graph ) float64 {
switch op .Kind {
case OpScan :
return rows * float64 (scanCard (&op .Source , op .Props , g ))
case OpExpand :
pop := targetPop (op .Labels , g )
deg := fanout (op .Types , op .Dir , g )
if firstHopDeg != nil {
deg = *firstHopDeg
}
if op .Rebind {
return rows * math .Min (deg /math .Max (pop , 1 ), 1 )
}
return rows * deg
default :
d := fanout (op .Types , op .Dir , g )
if firstHopDeg != nil {
d = *firstHopDeg
}
pop := targetPop (op .Labels , g )
var out float64
if op .Max != nil {
mult := 0.0
for h := op .Min ; h <= *op .Max ; h ++ {
mult += math .Pow (d , float64 (h ))
}
out = rows * mult
} else {
out = pop
}
if op .DedupEndpoints || op .Max == nil {
return math .Min (out , math .Max (pop , 1 ))
}
return out
}
}
func scanCard(source *ScanSource , props []ast .PropEntry , g graph .Graph ) uint64 {
switch source .Kind {
case ScanProperty :
if isConcrete (source .Value ) {
return seekCard (g , source .Label , source .Key , semantics .LitValue (source .Value ))
}
return g .LabelCardinality (source .Label )
case ScanLabel :
base := float64 (g .LabelCardinality (source .Label ))
return rnd (base * propSel (props ))
case ScanTextMatch :
return g .LabelCardinality (source .Label )
case ScanExistsSeed :
if source .Label != "" {
return g .LabelCardinality (source .Label )
}
return uint64 (g .NodeCount ())
case ScanNodeID , ScanNodeIDVar , ScanArg :
return 1
default :
return uint64 (g .NodeCount ())
}
}
func fanout(types []string , dir graph .Direction , g graph .Graph ) float64 {
if len (types ) == 0 {
return untypedFanout
}
total := 0.0
for _ , t := range types {
total += g .AvgDegree (t , dir )
}
return total
}
func targetPop(labels []string , g graph .Graph ) float64 {
if len (labels ) > 0 {
return float64 (g .LabelCardinality (labels [0 ]))
}
return float64 (g .NodeCount ())
}
func propSel(props []ast .PropEntry ) float64 {
concrete := 0
for i := range props {
if isConcrete (props [i ].Val ) {
concrete ++
}
}
return math .Max (math .Pow (0.1 , float64 (concrete )), math .SmallestNonzeroFloat64 )
}
func isConcrete(l ast .Literal ) bool {
return l .Kind != ast .LitParam && l .Kind != ast .LitNamedParam && l .Kind != ast .LitNull
}
func stageNames(seg *Segment ) []string {
names := make ([]string , seg .RowWidth )
for name , slot := range seg .Slots {
if slot < len (names ) {
names [slot ] = name
}
}
return names
}
func anchorNote(ms *MatchStage , names []string , g graph .Graph ) string {
if len (ms .Ops ) < 2 || ms .Ops [0 ].Kind != OpScan {
return ""
}
scan := &ms .Ops [0 ]
if scan .Source .Kind == ScanArg {
return ""
}
hop := &ms .Ops [1 ]
if hop .Kind != OpExpand && hop .Kind != OpVarExpand {
return ""
}
last := &ms .Ops [len (ms .Ops )-1 ]
if last .Kind != OpExpand && last .Kind != OpVarExpand {
return ""
}
c1 := scanCard (&scan .Source , scan .Props , g )
n1 , ok1 := resolveScanNodes (&scan .Source , scan .Labels , scan .Props , g )
d1 := resolvedDegree (n1 , ok1 , hop .Types , hop .Dir , g )
c2 := nodeCard (last .Labels , last .Props , g )
n2 , ok2 := resolveByProps (last .Labels , last .Props , g )
d2 := resolvedDegree (n2 , ok2 , last .Types , revDir (last .Dir ), g )
chosen := endpointDesc (scan .Slot , names , scan .Labels , scan .Props )
other := endpointDesc (last .To , names , last .Labels , last .Props )
var reason string
switch {
case c1 < c2 :
reason = "smaller leaf cardinality"
case c1 == c2 && d1 .val < d2 .val :
reason = "smaller first-hop degree (hub-aware)"
case c1 == c2 && d1 .val == d2 .val :
reason = "avg-degree tie-break"
default :
reason = "selectivity tier"
}
return fmt .Sprintf ("[anchor: %s card=%d, fan-out %s · vs %s card=%d, fan-out %s → %s]" ,
chosen , c1 , d1 .render (), other , c2 , d2 .render (), reason )
}
type degree struct {
val uint64
exact bool
}
func (d degree ) render () string {
if d .exact {
return GroupDigits (d .val )
}
return "~" + GroupDigits (d .val )
}
func resolvedDegree(nodes []graph .NodeID , ok bool , types []string , dir graph .Direction , g graph .Graph ) degree {
if !ok {
return degree {val : rnd (fanout (types , dir , g ))}
}
var total uint64
for _ , n := range nodes {
total += countNeighbors (g , n , dir , types )
}
return degree {val : total , exact : true }
}
func resolveScanNodes(source *ScanSource , labels []string , props []ast .PropEntry , g graph .Graph ) ([]graph .NodeID , bool ) {
switch source .Kind {
case ScanProperty :
if isConcrete (source .Value ) {
return seekNodes (g , source .Label , source .Key , semantics .LitValue (source .Value )), true
}
return nil , false
case ScanLabel :
return resolveByProps (labels , props , g )
}
return nil , false
}
func resolveByProps(labels []string , props []ast .PropEntry , g graph .Graph ) ([]graph .NodeID , bool ) {
if len (labels ) == 0 {
return nil , false
}
for i := range props {
if isConcrete (props [i ].Val ) {
return seekNodes (g , labels [0 ], props [i ].Key , semantics .LitValue (props [i ].Val )), true
}
}
return nil , false
}
func nodeCard(labels []string , props []ast .PropEntry , g graph .Graph ) uint64 {
if len (labels ) == 0 {
return uint64 (g .NodeCount ())
}
for i := range props {
if isConcrete (props [i ].Val ) {
return seekCard (g , labels [0 ], props [i ].Key , semantics .LitValue (props [i ].Val ))
}
}
return g .LabelCardinality (labels [0 ])
}
func endpointDesc(slot int , names []string , labels []string , props []ast .PropEntry ) string {
v := ""
if slot >= 0 && slot < len (names ) {
v = names [slot ]
}
label := ""
if len (labels ) > 0 {
label = ":" + labels [0 ]
}
key := ""
for i := range props {
if isConcrete (props [i ].Val ) {
key = " {" + props [i ].Key + "}"
break
}
}
return v + label + key
}
func revDir(d graph .Direction ) graph .Direction {
switch d {
case graph .Outgoing :
return graph .Incoming
case graph .Incoming :
return graph .Outgoing
}
return graph .Both
}
func GroupDigits (n uint64 ) string {
s := fmt .Sprintf ("%d" , n )
out := make ([]byte , 0 , len (s )+len (s )/3 )
for i := range len (s ) {
if i > 0 && (len (s )-i )%3 == 0 {
out = append (out , ',' )
}
out = append (out , s [i ])
}
return string (out )
}
The pages are generated with Golds v0.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 .