Skip to content

Commit f480dff

Browse files
committed
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.
1 parent 4059c8c commit f480dff

File tree

5 files changed

+117
-13
lines changed

5 files changed

+117
-13
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: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ type Cost struct {
2020
// fullScanCount is the number of full table or index scans in a
2121
// sub-plan, up to 255.
2222
fullScanCount uint8
23+
// unboundedCardinality is true if the operator or any of its
24+
// descendants have no guaranteed upperbound on the number of rows that
25+
// they can produce. It is similar to UnboundedCardinalityPenalty, but
26+
// different in that it is used to propagate the same information up the
27+
// tree without affecting cost comparisons.
28+
unboundedCardinality bool
2329
}
2430
}
2531

@@ -59,6 +65,7 @@ func (c *Cost) Add(other Cost) {
5965
} else {
6066
c.aux.fullScanCount += other.aux.fullScanCount
6167
}
68+
c.aux.unboundedCardinality = c.aux.unboundedCardinality || other.aux.unboundedCardinality
6269
}
6370

6471
// FullScanCount returns the number of full scans in the cost.
@@ -68,11 +75,29 @@ func (c Cost) FullScanCount() uint8 {
6875

6976
// IncrFullScanCount increments that auxiliary full scan count within c.
7077
func (c *Cost) IncrFullScanCount() {
71-
if c.aux.fullScanCount == math.MaxUint8 {
72-
// Avoid overflow.
73-
return
78+
// Avoid overflow.
79+
if c.aux.fullScanCount < math.MaxUint8 {
80+
c.aux.fullScanCount++
7481
}
75-
c.aux.fullScanCount++
82+
}
83+
84+
// HasUnboundedCardinality returns true if any expression in the tree has no
85+
// guaranteed upperbound on the number of rows that it will produce.
86+
//
87+
// NOTE: The returned value is independent of the UnboundedCardinalityPenalty
88+
// and true may be returned when the penalty is not set. It has no effect on
89+
// cost comparisons.
90+
func (c Cost) HasUnboundedCardinality() bool {
91+
return c.aux.unboundedCardinality
92+
}
93+
94+
// SetUnboundedCardinality is called to indicate that an expression has no
95+
// guaranteed upperbound on the number of rows that it will produce.
96+
//
97+
// NOTE: This flag does not affect cost comparisons and is independent of the
98+
// UnboundedCardinalityPenalty.
99+
func (c *Cost) SetUnboundedCardinality() {
100+
c.aux.unboundedCardinality = true
76101
}
77102

78103
// Penalties is an ordered bitmask where each bit indicates a cost penalty. The

pkg/sql/opt/memo/cost_test.go

Lines changed: 6 additions & 4 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) {
@@ -38,7 +39,8 @@ func TestCostLess(t *testing.T) {
3839
{Cost{C: 2.0}, Cost{C: 1.0, Penalties: UnboundedCardinalityPenalty}, true},
3940
{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 {
@@ -58,8 +60,8 @@ func TestCostAdd(t *testing.T) {
5860
{Cost{C: 1.0, Penalties: FullScanPenalty}, Cost{C: 2.0}, Cost{C: 3.0, Penalties: FullScanPenalty}},
5961
{Cost{C: 1.0}, Cost{C: 2.0, Penalties: HugeCostPenalty}, Cost{C: 3.0, Penalties: HugeCostPenalty}},
6062
{Cost{C: 1.0, Penalties: UnboundedCardinalityPenalty}, Cost{C: 2.0, Penalties: HugeCostPenalty}, Cost{C: 3.0, Penalties: HugeCostPenalty | UnboundedCardinalityPenalty}},
61-
{Cost{C: 1.0, aux: testAux{1}}, Cost{C: 1.0, aux: testAux{2}}, Cost{C: 2.0, aux: testAux{3}}},
62-
{Cost{C: 1.0, aux: testAux{200}}, Cost{C: 1.0, aux: testAux{100}}, Cost{C: 2.0, aux: testAux{255}}},
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}}},
6365
}
6466
for _, tc := range testCases {
6567
tc.left.Add(tc.right)

pkg/sql/opt/xform/coster.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,12 +638,14 @@ func (c *coster) ComputeCost(candidate memo.RelExpr, required *physical.Required
638638
}
639639

640640
// Add a one-time cost for any operator with unbounded cardinality. This
641-
// ensures we prefer plans that push limits as far down the tree as possible,
642-
// all else being equal.
641+
// ensures we prefer plans that push limits as far down the tree as
642+
// possible, all else being equal.
643643
//
644-
// Also add a cost flag for unbounded cardinality.
644+
// Also add a cost flag for unbounded cardinality, and a penalty if the
645+
// corresponding session setting is enabled.
645646
if candidate.Relational().Cardinality.IsUnbounded() {
646647
cost.C += cpuCostFactor
648+
cost.SetUnboundedCardinality()
647649
if c.evalCtx.SessionData().OptimizerPreferBoundedCardinality {
648650
cost.Penalties |= memo.UnboundedCardinalityPenalty
649651
}

pkg/sql/prep/statement.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,14 @@ func (p *planCosts) NumCustom() int {
187187
// average cost of the custom plans.
188188
func (p *planCosts) IsGenericOptimal() bool {
189189
// Check cost flags and full scan counts.
190-
if gc := p.generic.FullScanCount(); gc > 0 || p.generic.Penalties != memo.NoPenalties {
190+
if gc := p.generic.FullScanCount(); gc > 0 ||
191+
p.generic.HasUnboundedCardinality() ||
192+
p.generic.Penalties != memo.NoPenalties {
191193
for i := 0; i < p.custom.length; i++ {
192-
if p.custom.costs[i].Penalties < p.generic.Penalties || gc > p.custom.costs[i].FullScanCount() {
194+
custom := &p.custom.costs[i]
195+
if custom.Penalties < p.generic.Penalties ||
196+
(p.generic.HasUnboundedCardinality() && !custom.HasUnboundedCardinality()) ||
197+
gc > custom.FullScanCount() {
193198
return false
194199
}
195200
}

0 commit comments

Comments
 (0)