Skip to content

Commit c2c3d55

Browse files
Merge e318d31 into blathers/backport-release-25.4-156879
2 parents ec93efb + e318d31 commit c2c3d55

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)