Skip to content

Commit fcb6e84

Browse files
committed
opt: avoid more types of bad generic query plans
This commit extends #155163. Instead of tracking an auxiliary boolean value to indicate that a plan has one or more expressions with unbounded cardinality, the `memo.Cost` struct now tracks the number of read expressions (i.e., expressions that perform KV reads) that have unbounded cardinality. This helps to avoid picking generic query plans that will perform significantly worse than their related custom query plans. Fixes #156690 Release note: None
1 parent 5078988 commit fcb6e84

File tree

5 files changed

+144
-72
lines changed

5 files changed

+144
-72
lines changed

pkg/sql/logictest/testdata/logic_test/generic

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,9 @@ plan type: custom
474474
statement ok
475475
DEALLOCATE p
476476

477-
# Regression test for #155159. Do not choose a generic query plan with unbounded
478-
# cardinality when the custom plans have bounded cardinality.
477+
# Regression test for #155159 and #156690. Do not choose a generic query plan
478+
# with reads that have unbounded cardinality when the reads in the custom plan
479+
# have bounded cardinality.
479480
statement ok
480481
CREATE TABLE t155159 (
481482
id INT PRIMARY KEY,
@@ -540,3 +541,84 @@ quality of service: regular
540541

541542
statement ok
542543
DEALLOCATE p
544+
545+
statement ok
546+
PREPARE p AS
547+
SELECT * FROM (
548+
SELECT id FROM t155159 WHERE a = $1 AND b >= $2
549+
ORDER BY b, id
550+
LIMIT 250
551+
)
552+
UNION
553+
SELECT * FROM (
554+
SELECT id FROM t155159 WHERE a = $3 AND b >= $4
555+
)
556+
557+
statement ok
558+
EXECUTE p (33, 44, 55, 66)
559+
560+
statement ok
561+
EXECUTE p (33, 44, 55, 66)
562+
563+
statement ok
564+
EXECUTE p (33, 44, 55, 66)
565+
566+
statement ok
567+
EXECUTE p (33, 44, 55, 66)
568+
569+
statement ok
570+
EXECUTE p (33, 44, 55, 66)
571+
572+
query T
573+
EXPLAIN ANALYZE EXECUTE p (33, 44, 55, 66);
574+
----
575+
planning time: 10µs
576+
execution time: 100µs
577+
distribution: <hidden>
578+
vectorized: <hidden>
579+
plan type: custom
580+
maximum memory usage: <hidden>
581+
DistSQL network usage: <hidden>
582+
regions: <hidden>
583+
isolation level: serializable
584+
priority: normal
585+
quality of service: regular
586+
·
587+
• union
588+
│ sql nodes: <hidden>
589+
│ regions: <hidden>
590+
│ actual row count: 0
591+
│ execution time: 0µs
592+
│ estimated max memory allocated: 0 B
593+
594+
├── • scan
595+
│ sql nodes: <hidden>
596+
│ kv nodes: <hidden>
597+
│ regions: <hidden>
598+
│ actual row count: 0
599+
│ KV time: 0µs
600+
│ KV rows decoded: 0
601+
│ KV bytes read: 0 B
602+
│ KV gRPC calls: 0
603+
│ estimated max memory allocated: 0 B
604+
│ missing stats
605+
│ table: t155159@t155159_a_b_idx
606+
│ spans: [/33/44 - /33]
607+
│ limit: 250
608+
609+
└── • scan
610+
sql nodes: <hidden>
611+
kv nodes: <hidden>
612+
regions: <hidden>
613+
actual row count: 0
614+
KV time: 0µs
615+
KV rows decoded: 0
616+
KV bytes read: 0 B
617+
KV gRPC calls: 0
618+
estimated max memory allocated: 0 B
619+
missing stats
620+
table: t155159@t155159_a_b_idx
621+
spans: [/55/66 - /55]
622+
623+
statement ok
624+
DEALLOCATE p

pkg/sql/opt/memo/cost.go

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ type Cost struct {
2222
// cost is compared to other costs with Less.
2323
aux struct {
2424
// fullScanCount is the number of full table or index scans in a
25-
// sub-plan, up to 255.
26-
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
25+
// sub-plan, up to 65535.
26+
fullScanCount uint16
27+
// unboundedReadCount is the number of read expressions (e.g., scans,
28+
// lookup joins, etc.) in a sub-plan that have no upper-bound
29+
// cardinality, up to 65535.
30+
unboundedReadCount uint16
3331
}
3432
}
3533

@@ -63,45 +61,28 @@ func (c Cost) Less(other Cost) bool {
6361
func (c *Cost) Add(other Cost) {
6462
c.C += other.C
6563
c.Penalties |= other.Penalties
66-
if c.aux.fullScanCount > math.MaxUint8-other.aux.fullScanCount {
67-
// Avoid overflow.
68-
c.aux.fullScanCount = math.MaxUint8
69-
} else {
70-
c.aux.fullScanCount += other.aux.fullScanCount
71-
}
72-
c.aux.unboundedCardinality = c.aux.unboundedCardinality || other.aux.unboundedCardinality
64+
c.aux.fullScanCount = addUint16(c.aux.fullScanCount, other.aux.fullScanCount)
65+
c.aux.unboundedReadCount = addUint16(c.aux.unboundedReadCount, other.aux.unboundedReadCount)
7366
}
7467

7568
// FullScanCount returns the number of full scans in the cost.
76-
func (c Cost) FullScanCount() uint8 {
69+
func (c Cost) FullScanCount() uint16 {
7770
return c.aux.fullScanCount
7871
}
7972

8073
// IncrFullScanCount increments that auxiliary full scan count within c.
8174
func (c *Cost) IncrFullScanCount() {
82-
// Avoid overflow.
83-
if c.aux.fullScanCount < math.MaxUint8 {
84-
c.aux.fullScanCount++
85-
}
75+
c.aux.fullScanCount = addUint16(c.aux.fullScanCount, 1)
8676
}
8777

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
78+
// UnboundedReadCount returns the number of full scans in the cost.
79+
func (c Cost) UnboundedReadCount() uint16 {
80+
return c.aux.unboundedReadCount
9681
}
9782

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
83+
// IncrUnboundedReadCount increments that auxiliary full scan count within c.
84+
func (c *Cost) IncrUnboundedReadCount() {
85+
c.aux.unboundedReadCount = addUint16(c.aux.unboundedReadCount, 1)
10586
}
10687

10788
// Penalties is an ordered bitmask where each bit indicates a cost penalty. The
@@ -142,12 +123,11 @@ const (
142123
// <Cost> is the floating point cost value.
143124
// <Penalties> contains "H", "F", or "U" for HugeCostPenalty, FullScanPenalty,
144125
// and UnboundedCardinalityPenalty, respectively.
145-
// <aux> contains a number for full scan count and "u" for
146-
// unboundedCardinality.
126+
// <aux> contains the number of full scans and unbounded reads.
147127
//
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.
128+
// For example, the summary "1.23:HF:5f6u" indicates a cost of 1.23 with the
129+
// HugeCostPenalty and FullScanPenalty penalties, 5 full scans, and 6 unbounded
130+
// reads.
151131
func (c Cost) Summary() string {
152132
var sb strings.Builder
153133
_, _ = fmt.Fprintf(&sb, "%.9g:", c.C)
@@ -161,8 +141,13 @@ func (c Cost) Summary() string {
161141
sb.WriteByte('U')
162142
}
163143
_, _ = fmt.Fprintf(&sb, ":%df", c.aux.fullScanCount)
164-
if c.aux.unboundedCardinality {
165-
sb.WriteByte('u')
166-
}
144+
_, _ = fmt.Fprintf(&sb, "%du", c.aux.unboundedReadCount)
167145
return sb.String()
168146
}
147+
148+
func addUint16(a, b uint16) uint16 {
149+
if a > math.MaxUint16-b {
150+
return math.MaxUint16
151+
}
152+
return a + b
153+
}

pkg/sql/opt/memo/cost_test.go

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

1010
type testAux struct {
11-
fullScanCount uint8
12-
unboundedCardinality bool
11+
fullScanCount uint16
12+
unboundedReadCount uint16
1313
}
1414

1515
func TestCostLess(t *testing.T) {
@@ -39,8 +39,8 @@ func TestCostLess(t *testing.T) {
3939
{Cost{C: 2.0}, Cost{C: 1.0, Penalties: UnboundedCardinalityPenalty}, true},
4040
{Cost{C: 1.0, Penalties: UnboundedCardinalityPenalty}, Cost{C: 2.0}, false},
4141
// Auxiliary information should not affect the comparison.
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},
42+
{Cost{C: 1.0, aux: testAux{0, 0}}, Cost{C: 1.0, aux: testAux{1, 1}}, false},
43+
{Cost{C: 1.0, aux: testAux{1, 1}}, Cost{C: 1.0, aux: testAux{0, 0}}, false},
4444
}
4545
for _, tc := range testCases {
4646
if tc.left.Less(tc.right) != tc.expected {
@@ -60,8 +60,8 @@ func TestCostAdd(t *testing.T) {
6060
{Cost{C: 1.0, Penalties: FullScanPenalty}, Cost{C: 2.0}, Cost{C: 3.0, Penalties: FullScanPenalty}},
6161
{Cost{C: 1.0}, Cost{C: 2.0, Penalties: HugeCostPenalty}, Cost{C: 3.0, Penalties: HugeCostPenalty}},
6262
{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}}},
63+
{Cost{C: 1.0, aux: testAux{1, 4}}, Cost{C: 1.0, aux: testAux{2, 5}}, Cost{C: 2.0, aux: testAux{3, 9}}},
64+
{Cost{C: 1.0, aux: testAux{65530, 65530}}, Cost{C: 1.0, aux: testAux{100, 100}}, Cost{C: 2.0, aux: testAux{65535, 65535}}},
6565
}
6666
for _, tc := range testCases {
6767
tc.left.Add(tc.right)
@@ -76,18 +76,18 @@ func TestCostSummary(t *testing.T) {
7676
c Cost
7777
expected string
7878
}{
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"},
79+
{Cost{C: 1.0}, "1::0f0u"},
80+
{Cost{C: 1.23}, "1.23::0f0u"},
81+
{Cost{C: 1.23456}, "1.23456::0f0u"},
82+
{Cost{C: 1.23, Penalties: HugeCostPenalty}, "1.23:H:0f0u"},
83+
{Cost{C: 1.23, Penalties: FullScanPenalty}, "1.23:F:0f0u"},
84+
{Cost{C: 1.23, Penalties: UnboundedCardinalityPenalty}, "1.23:U:0f0u"},
85+
{Cost{C: 1.23, Penalties: HugeCostPenalty | FullScanPenalty | UnboundedCardinalityPenalty}, "1.23:HFU:0f0u"},
86+
{Cost{C: 1.23, Penalties: HugeCostPenalty | FullScanPenalty | UnboundedCardinalityPenalty}, "1.23:HFU:0f0u"},
87+
{Cost{C: 1.23, aux: testAux{5, 0}}, "1.23::5f0u"},
88+
{Cost{C: 1.23, aux: testAux{0, 6}}, "1.23::0f6u"},
89+
{Cost{C: 1.23, aux: testAux{5, 10}}, "1.23::5f10u"},
90+
{Cost{C: 1.23, Penalties: HugeCostPenalty | FullScanPenalty, aux: testAux{5, 9}}, "1.23:HF:5f9u"},
9191
}
9292
for _, tc := range testCases {
9393
if r := tc.c.Summary(); r != tc.expected {

pkg/sql/opt/xform/coster.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -641,14 +641,19 @@ func (c *coster) ComputeCost(candidate memo.RelExpr, required *physical.Required
641641
// ensures we prefer plans that push limits as far down the tree as
642642
// possible, all else being equal.
643643
//
644-
// Also add a cost flag for unbounded cardinality, and a penalty if the
645-
// corresponding session setting is enabled.
644+
// Also add a penalty if the corresponding session setting is enabled and
645+
// increment the unbounded read count for expressions that read from an
646+
// index.
646647
if candidate.Relational().Cardinality.IsUnbounded() {
647648
cost.C += cpuCostFactor
648-
cost.SetUnboundedCardinality()
649649
if c.evalCtx.SessionData().OptimizerPreferBoundedCardinality {
650650
cost.Penalties |= memo.UnboundedCardinalityPenalty
651651
}
652+
switch candidate.Op() {
653+
case opt.ScanOp, opt.PlaceholderScanOp, opt.LookupJoinOp, opt.IndexJoinOp,
654+
opt.InvertedJoinOp, opt.ZigzagJoinOp, opt.VectorSearchOp:
655+
cost.IncrUnboundedReadCount()
656+
}
652657
}
653658

654659
if !cost.Less(memo.MaxCost) {

pkg/sql/prep/statement.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +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 ||
191-
p.generic.HasUnboundedCardinality() ||
192-
p.generic.Penalties != memo.NoPenalties {
190+
genFullScans := p.generic.FullScanCount()
191+
genUnboundedReads := p.generic.UnboundedReadCount()
192+
if genFullScans > 0 || genUnboundedReads > 0 || p.generic.Penalties != memo.NoPenalties {
193193
for i := 0; i < p.custom.length; i++ {
194194
custom := &p.custom.costs[i]
195195
if custom.Penalties < p.generic.Penalties ||
196-
(p.generic.HasUnboundedCardinality() && !custom.HasUnboundedCardinality()) ||
197-
gc > custom.FullScanCount() {
196+
genFullScans > custom.FullScanCount() ||
197+
genUnboundedReads > custom.UnboundedReadCount() {
198198
return false
199199
}
200200
}
@@ -229,7 +229,7 @@ func (p *planCosts) avgCustom() memo.Cost {
229229
//
230230
// A full example:
231231
//
232-
// custom costs: 1.5 [3]{1.25:U:0u 1.75:U:0u 1.50:U:0u}, generic cost: 4.56:U:0u
232+
// custom costs: 1.5 [3]{1.25:U:0f0u 1.75:U:0f0u 1.50:U:0f0u}, generic cost: 4.56:U:0f0u
233233
func (p *planCosts) Summary() string {
234234
var sb strings.Builder
235235
sb.WriteString("custom costs: ")

0 commit comments

Comments
 (0)