Skip to content

Commit d0b9c08

Browse files
craig[bot]mgartner
andcommitted
Merge #155163
155163: opt: never pick unbounded generic plans over bounded custom plans r=mgartner a=mgartner #### opt: make CostFlags an ordered bitmask `memo.CostFlags` is now an ordered bitmask where each bit indicates a specific cost penalty and the penalties are ordered by precedence. The highest precedence penalty uses the highest-order bit. This allows CostFlags to be easily compared with built-in comparison operators (>, <, =). Release note: None #### opt: rename "CostFlags" to "Penalties" `memo.CostFlags` has been renamed to `memo.Penalities` which better reflects its meaning. Release note: None #### opt: use a consistent name for the unbounded cardinality penalty `memo.UnboundedCardinality` has been renamed to `memo.UnboundedCardinalityPenalty` to be consistent with the names of the other cost penalties. Release note: None #### opt: never pick unbounded generic plans over bounded custom plans The optimizer will no longer choose a generic query plan with unbounded cardinality over a custom query plan with bounded cardinality, regardless of `optimizer_prefer_bounded_cardinality`. In order to implement this behavior, a flag had to be added to `memo.Cost` that is separate from `UnboundedCardinalityPenalty`. The penalty is only added to the cost when `optimizer_prefer_bounded_cardinality` is enabled. The new flag is unconditionally set, allowing the decision between a generic and custom plan to work as expected regardless of the session setting. This is a tad confusing, so I did my best to document the differences clearly. Fixes #155159 Release note (performance improvement): The optimizer chooses suboptimal generic query plans in fewer cases. #### opt: add cost penalties and aux flags to log line All cost penalties and auxiliary flags are now printed in the tracing log for custom and generic query plan costs. This will aid in debugging. Release note: None Co-authored-by: Marcus Gartner <marcus@cockroachlabs.com>
2 parents 85658a4 + cce2892 commit d0b9c08

File tree

6 files changed

+228
-121
lines changed

6 files changed

+228
-121
lines changed

pkg/sql/logictest/testdata/logic_test/generic

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,3 +470,73 @@ query T match(plan\stype)
470470
EXPLAIN ANALYZE EXECUTE p(1, 2);
471471
----
472472
plan type: custom
473+
474+
statement ok
475+
DEALLOCATE p
476+
477+
# Regression test for #155159. Do not choose a generic query plan with unbounded
478+
# cardinality when the custom plans have bounded cardinality.
479+
statement ok
480+
CREATE TABLE t155159 (
481+
id INT PRIMARY KEY,
482+
a INT,
483+
b INT,
484+
INDEX (a, b)
485+
)
486+
487+
statement ok
488+
SET plan_cache_mode = auto
489+
490+
statement ok
491+
SET optimizer_prefer_bounded_cardinality = false
492+
493+
statement ok
494+
PREPARE p AS SELECT id FROM t155159 WHERE a = $1 AND b >= $2 ORDER BY b, id LIMIT 250
495+
496+
statement ok
497+
EXECUTE p (33, 44)
498+
499+
statement ok
500+
EXECUTE p (33, 44)
501+
502+
statement ok
503+
EXECUTE p (33, 44)
504+
505+
statement ok
506+
EXECUTE p (33, 44)
507+
508+
statement ok
509+
EXECUTE p (33, 44)
510+
511+
query T
512+
EXPLAIN ANALYZE EXECUTE p (33, 44)
513+
----
514+
planning time: 10µs
515+
execution time: 100µs
516+
distribution: <hidden>
517+
vectorized: <hidden>
518+
plan type: custom
519+
maximum memory usage: <hidden>
520+
DistSQL network usage: <hidden>
521+
regions: <hidden>
522+
isolation level: serializable
523+
priority: normal
524+
quality of service: regular
525+
·
526+
• scan
527+
sql nodes: <hidden>
528+
kv nodes: <hidden>
529+
regions: <hidden>
530+
actual row count: 0
531+
KV time: 0µs
532+
KV rows decoded: 0
533+
KV bytes read: 0 B
534+
KV gRPC calls: 0
535+
estimated max memory allocated: 0 B
536+
missing stats
537+
table: t155159@t155159_a_b_idx
538+
spans: [/33/44 - /33]
539+
limit: 250
540+
541+
statement ok
542+
DEALLOCATE p

pkg/sql/opt/memo/cost.go

Lines changed: 97 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,46 @@
55

66
package memo
77

8-
import "math"
8+
import (
9+
"fmt"
10+
"math"
11+
"strings"
12+
)
913

1014
// Cost is the best-effort approximation of the actual cost of executing a
1115
// particular operator tree.
1216
// TODO: Need more details about what one "unit" of cost means.
1317
type Cost struct {
14-
C float64
15-
Flags CostFlags
18+
C float64
19+
Penalties
1620

1721
// aux is auxiliary information within a cost that does not affect how the
1822
// cost is compared to other costs with Less.
1923
aux struct {
2024
// fullScanCount is the number of full table or index scans in a
2125
// sub-plan, up to 255.
2226
fullScanCount uint8
27+
// unboundedCardinality is true if the operator or any of its
28+
// descendants have no guaranteed upperbound on the number of rows that
29+
// they can produce. It is similar to UnboundedCardinalityPenalty, but
30+
// different in that it is used to propagate the same information up the
31+
// tree without affecting cost comparisons.
32+
unboundedCardinality bool
2333
}
2434
}
2535

2636
// MaxCost is the maximum possible estimated cost. It's used to suppress memo
2737
// group members during testing, by setting their cost so high that any other
2838
// member will have a lower cost.
2939
var MaxCost = Cost{
30-
C: math.Inf(+1),
31-
Flags: CostFlags{
32-
FullScanPenalty: true,
33-
HugeCostPenalty: true,
34-
UnboundedCardinality: true,
35-
},
40+
C: math.Inf(+1),
41+
Penalties: HugeCostPenalty | FullScanPenalty | UnboundedCardinalityPenalty,
3642
}
3743

3844
// Less returns true if this cost is lower than the given cost.
3945
func (c Cost) Less(other Cost) bool {
40-
if c.Flags != other.Flags {
41-
return c.Flags.Less(other.Flags)
46+
if c.Penalties != other.Penalties {
47+
return c.Penalties < other.Penalties
4248
}
4349
// Two plans with the same cost can have slightly different floating point
4450
// results (e.g. same subcosts being added up in a different order). So we
@@ -56,13 +62,14 @@ func (c Cost) Less(other Cost) bool {
5662
// Add adds the other cost to this cost.
5763
func (c *Cost) Add(other Cost) {
5864
c.C += other.C
59-
c.Flags.Add(other.Flags)
65+
c.Penalties |= other.Penalties
6066
if c.aux.fullScanCount > math.MaxUint8-other.aux.fullScanCount {
6167
// Avoid overflow.
6268
c.aux.fullScanCount = math.MaxUint8
6369
} else {
6470
c.aux.fullScanCount += other.aux.fullScanCount
6571
}
72+
c.aux.unboundedCardinality = c.aux.unboundedCardinality || other.aux.unboundedCardinality
6673
}
6774

6875
// FullScanCount returns the number of full scans in the cost.
@@ -72,55 +79,90 @@ func (c Cost) FullScanCount() uint8 {
7279

7380
// IncrFullScanCount increments that auxiliary full scan count within c.
7481
func (c *Cost) IncrFullScanCount() {
75-
if c.aux.fullScanCount == math.MaxUint8 {
76-
// Avoid overflow.
77-
return
82+
// Avoid overflow.
83+
if c.aux.fullScanCount < math.MaxUint8 {
84+
c.aux.fullScanCount++
7885
}
79-
c.aux.fullScanCount++
8086
}
8187

82-
// CostFlags contains flags that penalize the cost of an operator.
83-
type CostFlags struct {
84-
// FullScanPenalty is true if the cost of a full table or index scan is
85-
// penalized, indicating that a full scan should only be used if no other plan
86-
// is possible.
87-
FullScanPenalty bool
88-
// HugeCostPenalty is true if a plan should be avoided at all costs. This is
89-
// used when the optimizer is forced to use a particular plan, and will error
90-
// if it cannot be used.
91-
HugeCostPenalty bool
92-
// UnboundedCardinality is true if the operator or any of its descendants
93-
// have no guaranteed upperbound on the number of rows that they can
94-
// produce. See props.AnyCardinality.
95-
UnboundedCardinality bool
88+
// HasUnboundedCardinality returns true if any expression in the tree has no
89+
// guaranteed upperbound on the number of rows that it will produce.
90+
//
91+
// NOTE: The returned value is independent of the UnboundedCardinalityPenalty
92+
// and true may be returned when the penalty is not set. It has no effect on
93+
// cost comparisons.
94+
func (c Cost) HasUnboundedCardinality() bool {
95+
return c.aux.unboundedCardinality
9696
}
9797

98-
// Less returns true if these flags indicate a lower penalty than the other
99-
// CostFlags.
100-
func (c CostFlags) Less(other CostFlags) bool {
101-
// HugeCostPenalty takes precedence over other penalties, since it indicates
102-
// that a plan is being forced with a hint, and will error if we cannot comply
103-
// with the hint.
104-
if c.HugeCostPenalty != other.HugeCostPenalty {
105-
return !c.HugeCostPenalty
106-
}
107-
if c.FullScanPenalty != other.FullScanPenalty {
108-
return !c.FullScanPenalty
109-
}
110-
if c.UnboundedCardinality != other.UnboundedCardinality {
111-
return !c.UnboundedCardinality
112-
}
113-
return false
98+
// SetUnboundedCardinality is called to indicate that an expression has no
99+
// guaranteed upperbound on the number of rows that it will produce.
100+
//
101+
// NOTE: This flag does not affect cost comparisons and is independent of the
102+
// UnboundedCardinalityPenalty.
103+
func (c *Cost) SetUnboundedCardinality() {
104+
c.aux.unboundedCardinality = true
114105
}
115106

116-
// Add adds the other flags to these flags.
117-
func (c *CostFlags) Add(other CostFlags) {
118-
c.FullScanPenalty = c.FullScanPenalty || other.FullScanPenalty
119-
c.HugeCostPenalty = c.HugeCostPenalty || other.HugeCostPenalty
120-
c.UnboundedCardinality = c.UnboundedCardinality || other.UnboundedCardinality
121-
}
107+
// Penalties is an ordered bitmask where each bit indicates a cost penalty. The
108+
// penalties are ordered by precedence, with the highest precedence penalty
109+
// using the highest-order bit. This allows Penalties to be easily compared with
110+
// built-in comparison operators (>, <, =, etc.). For example, Penalties with
111+
// HugeCostPenalty will always be greater than Penalties without.
112+
type Penalties uint8
122113

123-
// Empty returns true if these flags are empty.
124-
func (c CostFlags) Empty() bool {
125-
return !c.FullScanPenalty && !c.HugeCostPenalty && !c.UnboundedCardinality
114+
const (
115+
// HugeCostPenalty is true if a plan should be avoided at all costs. This is
116+
// used when the optimizer is forced to use a particular plan, and will
117+
// error if it cannot be used. It takes precedence over other penalties,
118+
// since it indicates that a plan is being forced with a hint, and will
119+
// error if we cannot comply with the hint.
120+
HugeCostPenalty Penalties = 1 << (7 - iota)
121+
122+
// FullScanPenalty is true if the cost of a full table or index scan is
123+
// penalized, indicating that a full scan should only be used if no other
124+
// plan is possible.
125+
FullScanPenalty
126+
127+
// UnboundedCardinalityPenalty is true if the operator or any of its
128+
// descendants have no guaranteed upperbound on the number of rows that they
129+
// can produce. See props.AnyCardinality.
130+
UnboundedCardinalityPenalty
131+
132+
// NoPenalties represents no penalties.
133+
NoPenalties Penalties = 0
134+
)
135+
136+
// Summary returns a short string describing the cost. The format is:
137+
//
138+
// <Cost>:<Penalties>:<aux>
139+
//
140+
// Where:
141+
//
142+
// <Cost> is the floating point cost value.
143+
// <Penalties> contains "H", "F", or "U" for HugeCostPenalty, FullScanPenalty,
144+
// and UnboundedCardinalityPenalty, respectively.
145+
// <aux> contains a number for full scan count and "u" for
146+
// unboundedCardinality.
147+
//
148+
// For example, the summary "1.23:HF:5fu" indicates a cost of 1.23 with the
149+
// HugeCostPenalty and FullScanPenalty penalties, 5 full scans, and the
150+
// unboundedCardinality flag set.
151+
func (c Cost) Summary() string {
152+
var sb strings.Builder
153+
_, _ = fmt.Fprintf(&sb, "%.9g:", c.C)
154+
if c.Penalties&HugeCostPenalty != 0 {
155+
sb.WriteByte('H')
156+
}
157+
if c.Penalties&FullScanPenalty != 0 {
158+
sb.WriteByte('F')
159+
}
160+
if c.Penalties&UnboundedCardinalityPenalty != 0 {
161+
sb.WriteByte('U')
162+
}
163+
_, _ = fmt.Fprintf(&sb, ":%df", c.aux.fullScanCount)
164+
if c.aux.unboundedCardinality {
165+
sb.WriteByte('u')
166+
}
167+
return sb.String()
126168
}

pkg/sql/opt/memo/cost_test.go

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ package memo
88
import "testing"
99

1010
type testAux struct {
11-
fullScanCount uint8
11+
fullScanCount uint8
12+
unboundedCardinality bool
1213
}
1314

1415
func TestCostLess(t *testing.T) {
@@ -26,19 +27,20 @@ func TestCostLess(t *testing.T) {
2627
{Cost{C: 1}, Cost{C: 1.00000001}, true},
2728
{Cost{C: 1000}, Cost{C: 1000.00000000001}, false},
2829
{Cost{C: 1000}, Cost{C: 1000.00001}, true},
29-
{Cost{C: 1.0, Flags: CostFlags{FullScanPenalty: true}}, Cost{C: 1.0}, false},
30-
{Cost{C: 1.0}, Cost{C: 1.0, Flags: CostFlags{HugeCostPenalty: true}}, true},
31-
{Cost{C: 1.0, Flags: CostFlags{FullScanPenalty: true, HugeCostPenalty: true}}, Cost{C: 1.0}, false},
32-
{Cost{C: 1.0, Flags: CostFlags{FullScanPenalty: true}}, Cost{C: 1.0, Flags: CostFlags{HugeCostPenalty: true}}, true},
30+
{Cost{C: 1.0, Penalties: FullScanPenalty}, Cost{C: 1.0}, false},
31+
{Cost{C: 1.0}, Cost{C: 1.0, Penalties: HugeCostPenalty}, true},
32+
{Cost{C: 1.0, Penalties: FullScanPenalty | HugeCostPenalty}, Cost{C: 1.0}, false},
33+
{Cost{C: 1.0, Penalties: FullScanPenalty}, Cost{C: 1.0, Penalties: HugeCostPenalty}, true},
3334
{MaxCost, Cost{C: 1.0}, false},
3435
{Cost{C: 0.0}, MaxCost, true},
3536
{MaxCost, MaxCost, false},
36-
{MaxCost, Cost{C: 1.0, Flags: CostFlags{FullScanPenalty: true}}, false},
37-
{Cost{C: 1.0, Flags: CostFlags{HugeCostPenalty: true}}, MaxCost, true},
38-
{Cost{C: 2.0, Flags: CostFlags{}}, Cost{C: 1.0, Flags: CostFlags{UnboundedCardinality: true}}, true},
39-
{Cost{C: 1.0, Flags: CostFlags{UnboundedCardinality: true}}, Cost{C: 2.0, Flags: CostFlags{}}, false},
37+
{MaxCost, Cost{C: 1.0, Penalties: FullScanPenalty}, false},
38+
{Cost{C: 1.0, Penalties: HugeCostPenalty}, MaxCost, true},
39+
{Cost{C: 2.0}, Cost{C: 1.0, Penalties: UnboundedCardinalityPenalty}, true},
40+
{Cost{C: 1.0, Penalties: UnboundedCardinalityPenalty}, Cost{C: 2.0}, false},
4041
// Auxiliary information should not affect the comparison.
41-
{Cost{C: 1.0, aux: testAux{0}}, Cost{C: 1.0, aux: testAux{1}}, false},
42+
{Cost{C: 1.0, aux: testAux{0, false}}, Cost{C: 1.0, aux: testAux{1, true}}, false},
43+
{Cost{C: 1.0, aux: testAux{1, true}}, Cost{C: 1.0, aux: testAux{0, false}}, false},
4244
}
4345
for _, tc := range testCases {
4446
if tc.left.Less(tc.right) != tc.expected {
@@ -55,10 +57,11 @@ func TestCostAdd(t *testing.T) {
5557
{Cost{C: 0.0}, Cost{C: 0.0}, Cost{C: 0.0}},
5658
{Cost{C: -1.0}, Cost{C: 1.0}, Cost{C: 0.0}},
5759
{Cost{C: 1.5}, Cost{C: 2.5}, Cost{C: 4.0}},
58-
{Cost{C: 1.0, Flags: CostFlags{FullScanPenalty: true}}, Cost{C: 2.0}, Cost{C: 3.0, Flags: CostFlags{FullScanPenalty: true}}},
59-
{Cost{C: 1.0}, Cost{C: 2.0, Flags: CostFlags{HugeCostPenalty: true}}, Cost{C: 3.0, Flags: CostFlags{HugeCostPenalty: true}}},
60-
{Cost{C: 1.0, aux: testAux{1}}, Cost{C: 1.0, aux: testAux{2}}, Cost{C: 2.0, aux: testAux{3}}},
61-
{Cost{C: 1.0, aux: testAux{200}}, Cost{C: 1.0, aux: testAux{100}}, Cost{C: 2.0, aux: testAux{255}}},
60+
{Cost{C: 1.0, Penalties: FullScanPenalty}, Cost{C: 2.0}, Cost{C: 3.0, Penalties: FullScanPenalty}},
61+
{Cost{C: 1.0}, Cost{C: 2.0, Penalties: HugeCostPenalty}, Cost{C: 3.0, Penalties: HugeCostPenalty}},
62+
{Cost{C: 1.0, Penalties: UnboundedCardinalityPenalty}, Cost{C: 2.0, Penalties: HugeCostPenalty}, Cost{C: 3.0, Penalties: HugeCostPenalty | UnboundedCardinalityPenalty}},
63+
{Cost{C: 1.0, aux: testAux{1, false}}, Cost{C: 1.0, aux: testAux{2, true}}, Cost{C: 2.0, aux: testAux{3, true}}},
64+
{Cost{C: 1.0, aux: testAux{200, true}}, Cost{C: 1.0, aux: testAux{100, false}}, Cost{C: 2.0, aux: testAux{255, true}}},
6265
}
6366
for _, tc := range testCases {
6467
tc.left.Add(tc.right)
@@ -68,40 +71,27 @@ func TestCostAdd(t *testing.T) {
6871
}
6972
}
7073

71-
func TestCostFlagsLess(t *testing.T) {
74+
func TestCostSummary(t *testing.T) {
7275
testCases := []struct {
73-
left, right CostFlags
74-
expected bool
75-
}{
76-
{CostFlags{FullScanPenalty: false, HugeCostPenalty: false}, CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, true},
77-
{CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, CostFlags{FullScanPenalty: false, HugeCostPenalty: false}, false},
78-
{CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, false},
79-
{CostFlags{FullScanPenalty: false}, CostFlags{FullScanPenalty: true}, true},
80-
{CostFlags{HugeCostPenalty: false}, CostFlags{HugeCostPenalty: true}, true},
81-
{CostFlags{UnboundedCardinality: false}, CostFlags{UnboundedCardinality: true}, true},
82-
{CostFlags{UnboundedCardinality: true}, CostFlags{UnboundedCardinality: false}, false},
83-
}
84-
for _, tc := range testCases {
85-
if tc.left.Less(tc.right) != tc.expected {
86-
t.Errorf("expected %v.Less(%v) to be %v", tc.left, tc.right, tc.expected)
87-
}
88-
}
89-
}
90-
91-
func TestCostFlagsAdd(t *testing.T) {
92-
testCases := []struct {
93-
left, right, expected CostFlags
76+
c Cost
77+
expected string
9478
}{
95-
{CostFlags{FullScanPenalty: false, HugeCostPenalty: false}, CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, CostFlags{FullScanPenalty: true, HugeCostPenalty: true}},
96-
{CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, CostFlags{FullScanPenalty: false, HugeCostPenalty: false}, CostFlags{FullScanPenalty: true, HugeCostPenalty: true}},
97-
{CostFlags{FullScanPenalty: false}, CostFlags{FullScanPenalty: true}, CostFlags{FullScanPenalty: true}},
98-
{CostFlags{HugeCostPenalty: false}, CostFlags{HugeCostPenalty: true}, CostFlags{HugeCostPenalty: true}},
99-
{CostFlags{FullScanPenalty: true, HugeCostPenalty: false}, CostFlags{FullScanPenalty: false, HugeCostPenalty: true}, CostFlags{FullScanPenalty: true, HugeCostPenalty: true}},
79+
{Cost{C: 1.0}, "1::0f"},
80+
{Cost{C: 1.23}, "1.23::0f"},
81+
{Cost{C: 1.23456}, "1.23456::0f"},
82+
{Cost{C: 1.23, Penalties: HugeCostPenalty}, "1.23:H:0f"},
83+
{Cost{C: 1.23, Penalties: FullScanPenalty}, "1.23:F:0f"},
84+
{Cost{C: 1.23, Penalties: UnboundedCardinalityPenalty}, "1.23:U:0f"},
85+
{Cost{C: 1.23, Penalties: HugeCostPenalty | FullScanPenalty | UnboundedCardinalityPenalty}, "1.23:HFU:0f"},
86+
{Cost{C: 1.23, Penalties: HugeCostPenalty | FullScanPenalty | UnboundedCardinalityPenalty}, "1.23:HFU:0f"},
87+
{Cost{C: 1.23, aux: testAux{5, false}}, "1.23::5f"},
88+
{Cost{C: 1.23, aux: testAux{0, true}}, "1.23::0fu"},
89+
{Cost{C: 1.23, aux: testAux{5, true}}, "1.23::5fu"},
90+
{Cost{C: 1.23, Penalties: HugeCostPenalty | FullScanPenalty, aux: testAux{5, true}}, "1.23:HF:5fu"},
10091
}
10192
for _, tc := range testCases {
102-
tc.left.Add(tc.right)
103-
if tc.left != tc.expected {
104-
t.Errorf("expected %v.Add(%v) to be %v, got %v", tc.left, tc.right, tc.expected, tc.left)
93+
if r := tc.c.Summary(); r != tc.expected {
94+
t.Errorf("expected %q, got %q", tc.expected, r)
10595
}
10696
}
10797
}

0 commit comments

Comments
 (0)