Skip to content

Commit 21a2c80

Browse files
craig[bot]RaduBerinde
andcommitted
Merge #27111
27111: opt: refactor lower level of optsteps r=RaduBerinde a=RaduBerinde Extracting the lower level logic around setting up the optimizer for `optsteps` into a new struct. The same low-level functionality will be used to implement a new command focused around exploration transforms. We also fix an edge case in `suppressExprs` (now `restrictToGroup`) - when an expression has multiple children from which the target group is reachable, we were only recursing on the first one. Release note: None Co-authored-by: Radu Berinde <radu@cockroachlabs.com>
2 parents 4052f30 + 83ef602 commit 21a2c80

File tree

2 files changed

+212
-150
lines changed

2 files changed

+212
-150
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright 2018 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
package testutils
16+
17+
import (
18+
"github.com/cockroachdb/cockroach/pkg/sql/opt"
19+
"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
20+
"github.com/cockroachdb/cockroach/pkg/sql/opt/props"
21+
"github.com/cockroachdb/cockroach/pkg/sql/opt/xform"
22+
"github.com/cockroachdb/cockroach/pkg/util"
23+
)
24+
25+
// forcingOptimizer is a wrapper around an Optimizer which adds low-level
26+
// control, like restricting rule application or the expressions that can be
27+
// part of the final expression.
28+
type forcingOptimizer struct {
29+
o *xform.Optimizer
30+
31+
root memo.GroupID
32+
required *props.Physical
33+
34+
// remaining is the number of "unused" steps remaining.
35+
remaining int
36+
37+
// lastMatched records the name of the rule that was most recently matched
38+
// by the optimizer.
39+
lastMatched opt.RuleName
40+
41+
// lastApplied records the id of the expression that marks the portion of the
42+
// tree affected by the most recent rule application. All expressions in the
43+
// same memo group that are < lastApplied.Expr will assigned an infinite cost
44+
// by the forcingCoster. Therefore, only expressions >= lastApplied.Expr can
45+
// be in the output expression tree.
46+
lastApplied memo.ExprID
47+
}
48+
49+
// newForcingOptimizer creates a forcing optimizer that stops applying any rules
50+
// after <steps> rules are matched.
51+
func newForcingOptimizer(tester *OptTester, steps int) (*forcingOptimizer, error) {
52+
fo := &forcingOptimizer{
53+
o: xform.NewOptimizer(&tester.evalCtx),
54+
remaining: steps,
55+
lastMatched: opt.InvalidRuleName,
56+
lastApplied: memo.InvalidExprID,
57+
}
58+
59+
fo.o.NotifyOnMatchedRule(func(ruleName opt.RuleName) bool {
60+
if fo.remaining == 0 {
61+
return false
62+
}
63+
fo.remaining--
64+
fo.lastMatched = ruleName
65+
return true
66+
})
67+
68+
// Hook the AppliedRule notification in order to track the portion of the
69+
// expression tree affected by each transformation rule.
70+
fo.lastApplied = memo.InvalidExprID
71+
fo.o.NotifyOnAppliedRule(func(ruleName opt.RuleName, group memo.GroupID, added int) {
72+
if added > 0 {
73+
// This was an exploration rule that added one or more expressions to
74+
// an existing group. Record the id of the first of those expressions.
75+
// Previous expressions will be suppressed.
76+
ord := memo.ExprOrdinal(fo.o.Memo().ExprCount(group) - added)
77+
fo.lastApplied = memo.ExprID{Group: group, Expr: ord}
78+
} else {
79+
// This was a normalization that created a new memo group, or it was
80+
// an exploration rule that didn't add any expressions to the group.
81+
// Either way, none of the expressions in the group need to be
82+
// suppressed.
83+
fo.lastApplied = memo.MakeNormExprID(group)
84+
}
85+
})
86+
87+
var err error
88+
fo.root, fo.required, err = tester.buildExpr(fo.o.Factory())
89+
if err != nil {
90+
return nil, err
91+
}
92+
return fo, nil
93+
}
94+
95+
func (fo *forcingOptimizer) optimize() memo.ExprView {
96+
return fo.o.Optimize(fo.root, fo.required)
97+
}
98+
99+
// restrictToExprs sets up the optimizer to restrict the result to only those
100+
// containing one of the given expressions (all in the same group).
101+
//
102+
// mem is the resulting Memo obtained from another instance of forcingOptimizer, with
103+
// the same configuration.
104+
//
105+
// exprs is a set of ExprOrdinals (in the given group).
106+
func (fo *forcingOptimizer) restrictToExprs(
107+
mem *memo.Memo, group memo.GroupID, exprs util.FastIntSet,
108+
) {
109+
coster := newForcingCoster(fo.o.Coster())
110+
111+
restrictToGroup(coster, mem, fo.root, group)
112+
113+
for e := 0; e < mem.ExprCount(group); e++ {
114+
if !exprs.Contains(e) {
115+
coster.SuppressExpr(memo.ExprID{Group: group, Expr: memo.ExprOrdinal(e)})
116+
}
117+
}
118+
119+
fo.o.SetCoster(coster)
120+
}
121+
122+
// restrictToGroup walks the memo and adds expressions which need to be
123+
// suppressed to the forcingCoster so that the optimization result must contain
124+
// an expression in the given target group.
125+
//
126+
// restrictToGroup does this by recursively traversing the memo, starting at the
127+
// root group. If a group expression is not an ancestor of the target group,
128+
// then it is suppressed. If it is an ancestor, then restrictExprs recurses on
129+
// any child group that is an ancestor.
130+
//
131+
// Must be called before optimize().
132+
func restrictToGroup(coster *forcingCoster, mem *memo.Memo, group, target memo.GroupID) {
133+
if group == target {
134+
return
135+
}
136+
137+
for e := 0; e < mem.ExprCount(group); e++ {
138+
eid := memo.ExprID{Group: group, Expr: memo.ExprOrdinal(e)}
139+
found := false
140+
expr := mem.Expr(eid)
141+
for g := 0; g < expr.ChildCount(); g++ {
142+
child := expr.ChildGroup(mem, g)
143+
if isGroupReachable(mem, child, target) {
144+
restrictToGroup(coster, mem, child, target)
145+
found = true
146+
}
147+
}
148+
149+
if !found {
150+
coster.SuppressExpr(eid)
151+
}
152+
}
153+
}
154+
155+
// isGroupReachable returns true if the target group can be "reached" from the
156+
// given group; in other words, if the given group is the target group or one of
157+
// its ancestor groups.
158+
func isGroupReachable(mem *memo.Memo, group, target memo.GroupID) bool {
159+
if group == target {
160+
return true
161+
}
162+
163+
for e := 0; e < mem.ExprCount(group); e++ {
164+
eid := memo.ExprID{Group: group, Expr: memo.ExprOrdinal(e)}
165+
expr := mem.Expr(eid)
166+
for g := 0; g < expr.ChildCount(); g++ {
167+
if isGroupReachable(mem, expr.ChildGroup(mem, g), target) {
168+
return true
169+
}
170+
}
171+
}
172+
173+
return false
174+
}
175+
176+
// forcingCoster implements the xform.Coster interface so that it can suppress
177+
// expressions in the memo that can't be part of the output tree.
178+
type forcingCoster struct {
179+
inner xform.Coster
180+
suppressed map[memo.ExprID]bool
181+
}
182+
183+
func newForcingCoster(inner xform.Coster) *forcingCoster {
184+
return &forcingCoster{inner: inner, suppressed: make(map[memo.ExprID]bool)}
185+
}
186+
187+
func (fc *forcingCoster) SuppressExpr(eid memo.ExprID) {
188+
fc.suppressed[eid] = true
189+
}
190+
191+
// ComputeCost is part of the xform.Coster interface.
192+
func (fc *forcingCoster) ComputeCost(candidate *memo.BestExpr, logical *props.Logical) memo.Cost {
193+
if fc.suppressed[candidate.Expr()] {
194+
// Suppressed expressions get assigned MaxCost so that they never have
195+
// the lowest cost.
196+
return memo.MaxCost
197+
}
198+
return fc.inner.ComputeCost(candidate, logical)
199+
}

0 commit comments

Comments
 (0)