Skip to content

Commit 1d82da6

Browse files
author
Mark Sirek
committed
opt: do not push LIMIT into the scan of a virtual table
Fixes #78578 Previously, a LIMIT operation could be pushed into the scan of a virtual table with an ORDER BY clause. This was inadequate because in-order scans of virtual indexes aren't supported. When an index that should provide the order requested by a query is used, a sort is actually produced under the covers: ``` EXPLAIN(vec) SELECT oid, typname FROM pg_type ORDER BY OID; info ---------------------------------- │ └ Node 1 └ *colexec.sortOp └ *sql.planNodeToRowSource ``` Functions `CanLimitFilteredScan` and `GenerateLimitedScans` are modified to avoid pushing LIMIT operations into ordered scans of virtual indexes. Release justification: Low risk fix for incorrect results in queries involving virtual system tables. Release note (bug fix): LIMIT queries with an ORDER BY clause which scan the index of a virtual system tables, such as `pg_type`, could previously return incorrect results. This is corrected by teaching the optimizer that LIMIT operations cannot be pushed into ordered scans of virtual indexes.
1 parent b7b37f4 commit 1d82da6

4 files changed

Lines changed: 68 additions & 1 deletion

File tree

pkg/sql/logictest/testdata/logic_test/limit

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,10 @@ SELECT * FROM t ORDER BY v, w LIMIT 3;
351351
6 -36 216
352352
4 -16 94
353353
2 -4 8
354+
355+
query IT
356+
SELECT oid::INT, typname FROM pg_type ORDER BY oid LIMIT 3
357+
----
358+
16 bool
359+
17 bytea
360+
18 char

pkg/sql/opt/exec/execbuilder/testdata/limit

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,42 @@ vectorized: true
527527
estimated row count: 100 - 1,001 (100% of the table; stats collected <hidden> ago)
528528
table: a@a_i_j_idx
529529
spans: FULL SCAN
530+
531+
# A limit cannot be pushed into the scan of a virtual table with ORDER BY.
532+
query T
533+
EXPLAIN SELECT oid, typname FROM pg_type ORDER BY oid LIMIT 10
534+
----
535+
distribution: local
536+
vectorized: true
537+
·
538+
• limit
539+
│ count: 10
540+
541+
└── • virtual table
542+
table: pg_type@pg_type_oid_idx
543+
544+
# A limit can be pushed into the scan of a virtual table without ORDER BY.
545+
query T
546+
EXPLAIN SELECT oid, typname FROM pg_type LIMIT 10
547+
----
548+
distribution: local
549+
vectorized: true
550+
·
551+
• virtual table
552+
table: pg_type@primary
553+
limit: 10
554+
555+
# A limit cannot be pushed into the constrained scan of a virtual table with
556+
# ORDER BY.
557+
query T
558+
EXPLAIN SELECT oid, typname FROM pg_type WHERE OID BETWEEN 1 AND 1000 ORDER BY oid LIMIT 10
559+
----
560+
distribution: local
561+
vectorized: true
562+
·
563+
• limit
564+
│ count: 10
565+
566+
└── • virtual table
567+
table: pg_type@pg_type_oid_idx
568+
spans: [/1 - /1000]

pkg/sql/opt/memo/expr.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,12 @@ func (s *ScanPrivate) IsFullIndexScan(md *opt.Metadata) bool {
668668
s.HardLimit == 0
669669
}
670670

671+
// IsVirtualTable returns true if the table being scanned is a virtual table.
672+
func (s *ScanPrivate) IsVirtualTable(md *opt.Metadata) bool {
673+
tab := md.Table(s.Table)
674+
return tab.IsVirtualTable()
675+
}
676+
671677
// IsLocking returns true if the ScanPrivate is configured to use a row-level
672678
// locking mode. This can be the case either because the Scan is in the scope of
673679
// a SELECT .. FOR [KEY] UPDATE/SHARE clause or because the Scan was configured

pkg/sql/opt/xform/limit_funcs.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,14 @@ func (c *CustomFuncs) CanLimitFilteredScan(
6060
// unconstrained scans on non-partial indexes.
6161
return false
6262
}
63-
63+
// Virtual indexes are not sorted, but are presented to the optimizer as
64+
// sorted, and have a sort automatically applied to them on the output of the
65+
// scan. Since this implicit sort happens after the scan, we can't place a
66+
// hard limit on the scan if the query semantics require a sort order on the
67+
// entire relation.
68+
if scanPrivate.IsVirtualTable(md) && !required.Any() {
69+
return false
70+
}
6471
ok, _ := ordering.ScanPrivateCanProvide(c.e.mem.Metadata(), scanPrivate, &required)
6572
return ok
6673
}
@@ -87,6 +94,14 @@ func (c *CustomFuncs) CanLimitFilteredScan(
8794
func (c *CustomFuncs) GenerateLimitedScans(
8895
grp memo.RelExpr, scanPrivate *memo.ScanPrivate, limit tree.Datum, required props.OrderingChoice,
8996
) {
97+
// Virtual indexes are not sorted, but are presented to the optimizer as
98+
// sorted, and have a sort automatically applied to them on the output of the
99+
// scan. Since this implicit sort happens after the scan, we can't place a
100+
// hard limit on the scan if the query semantics require a sort order on the
101+
// entire relation.
102+
if scanPrivate.IsVirtualTable(c.e.mem.Metadata()) && !required.Any() {
103+
return
104+
}
90105
limitVal := int64(*limit.(*tree.DInt))
91106

92107
var pkCols opt.ColSet

0 commit comments

Comments
 (0)