Skip to content

Commit c619ca9

Browse files
craig[bot]Uzair5162
andcommitted
Merge #154892
154892: sql/stats: support partial stats over indexes implied by the filter r=Uzair5162 a=Uzair5162 This commit adds support to collect arbitrary partial statistics when a partial index with a predicate implied by the stat collection's filters exists. Fixes: #154891 Release note (sql change): Added support for collecting partial statistics when the given `WHERE` clause implies the predicate of a partial index with the requested column as the first key column. For example: ``` CREATE TABLE t (a INT, INDEX idx_partial (a) WHERE a > 5); CREATE STATISTICS pstat ON a FROM t WHERE a > 7; ``` Co-authored-by: Uzair Ahmad <[email protected]>
2 parents 3c76919 + 68f14ab commit c619ca9

File tree

2 files changed

+108
-7
lines changed

2 files changed

+108
-7
lines changed

pkg/sql/logictest/testdata/logic_test/constrained_stats

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ CREATE STATISTICS stat_wrong_col ON price FROM products WHERE category_id = 1
9999
statement error pq: filter cannot be a contradiction
100100
CREATE STATISTICS stat_contradiction ON price FROM products WHERE price < 10 AND price > 10
101101

102-
statement error pq: table products does not contain a non-partial forward index with created_at as a prefix column
102+
statement error pq: table products does not contain a suitable index with created_at as a prefix column
103103
CREATE STATISTICS stat_created_at ON created_at FROM products WHERE created_at > '2025-01-05'
104104

105105
statement error column "nonexistent" does not exist
@@ -205,5 +205,91 @@ stats_from_table_id {price} 10 10 0 price
205205
statement ok
206206
CREATE TABLE infer (x INT NOT NULL, y INT NOT NULL AS (x % 5) STORED, PRIMARY KEY (y, x));
207207

208-
statement error pq: table infer does not contain a non-partial forward index with x as a prefix column
208+
statement error pq: table infer does not contain a suitable index with x as a prefix column
209209
CREATE STATISTICS inferred_stat ON x FROM infer WHERE x = 100;
210+
211+
# Test partial statistics with a WHERE clause on tables with partial indexes.
212+
statement ok
213+
CREATE TABLE partial_indexes (
214+
a INT,
215+
b INT,
216+
INDEX idx_partial (a) WHERE a > 5
217+
);
218+
219+
statement ok
220+
INSERT INTO partial_indexes SELECT g, g FROM generate_series(1, 10) AS g;
221+
222+
statement ok
223+
CREATE STATISTICS fullstat FROM partial_indexes;
224+
225+
# Clear the stat cache so that creating partial statistics has access to the
226+
# latest full statistic.
227+
statement ok
228+
SELECT crdb_internal.clear_table_stats_cache();
229+
230+
# Filter implies the partial index predicate, so it can be used.
231+
statement ok
232+
CREATE STATISTICS partial_stat_implied ON a FROM partial_indexes WHERE a > 7;
233+
234+
query TTIIIT colnames
235+
SELECT statistics_name, column_names, row_count, distinct_count, null_count, partial_predicate
236+
FROM [SHOW STATISTICS FOR TABLE partial_indexes]
237+
WHERE statistics_name = 'partial_stat_implied';
238+
----
239+
statistics_name column_names row_count distinct_count null_count partial_predicate
240+
partial_stat_implied {a} 3 3 0 a > 7
241+
242+
# Filter equals partial index predicate, so it can be used.
243+
statement ok
244+
CREATE STATISTICS partial_stat_exact ON a FROM partial_indexes WHERE a > 5;
245+
246+
query TTIIIT colnames
247+
SELECT statistics_name, column_names, row_count, distinct_count, null_count, partial_predicate
248+
FROM [SHOW STATISTICS FOR TABLE partial_indexes]
249+
WHERE statistics_name = 'partial_stat_exact';
250+
----
251+
statistics_name column_names row_count distinct_count null_count partial_predicate
252+
partial_stat_exact {a} 5 5 0 a > 5
253+
254+
# Filter does not imply the partial index predicate, so it can't be used.
255+
statement error pq: table partial_indexes does not contain a suitable index with a as a prefix column
256+
CREATE STATISTICS partial_stat_not_implied ON a FROM partial_indexes WHERE a > 3;
257+
258+
# Ensure that we use suitable full indexes when a partial index is not implied.
259+
statement ok
260+
CREATE INDEX idx_a ON partial_indexes (a);
261+
262+
statement ok
263+
CREATE STATISTICS partial_stat_not_implied ON a FROM partial_indexes WHERE a > 3;
264+
265+
query TTIIIT colnames
266+
SELECT statistics_name, column_names, row_count, distinct_count, null_count, partial_predicate
267+
FROM [SHOW STATISTICS FOR TABLE partial_indexes]
268+
WHERE statistics_name = 'partial_stat_not_implied';
269+
----
270+
statistics_name column_names row_count distinct_count null_count partial_predicate
271+
partial_stat_not_implied {a} 7 7 0 a > 3
272+
273+
# Ensure that we only use partial indexes with the required prefix columns.
274+
statement ok
275+
DROP INDEX partial_indexes@idx_partial;
276+
277+
statement ok
278+
DROP INDEX partial_indexes@idx_a;
279+
280+
statement ok
281+
CREATE INDEX idx_b ON partial_indexes (b) WHERE a > 5;
282+
283+
statement error pq: table partial_indexes does not contain a suitable index with a as a prefix column
284+
CREATE STATISTICS partial_stat_implied ON a FROM partial_indexes WHERE a > 7;
285+
286+
statement ok
287+
DROP INDEX partial_indexes@idx_b;
288+
289+
# Ensure that we can't use an implied partial index when the filter doesn't
290+
# create a single constrained span.
291+
statement ok
292+
CREATE INDEX idx_b ON partial_indexes (b) WHERE b > 1 AND b % 2 = 0;
293+
294+
statement error pq: predicate could not become a constrained scan of an index
295+
CREATE STATISTICS partial_stat_implied ON b FROM partial_indexes WHERE b > 1 AND b % 2 = 0;

pkg/sql/opt/optbuilder/misc_statements.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/cockroachdb/cockroach/pkg/sql/opt/constraint"
1616
"github.com/cockroachdb/cockroach/pkg/sql/opt/idxconstraint"
1717
"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
18+
"github.com/cockroachdb/cockroach/pkg/sql/opt/partialidx"
1819
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
1920
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
2021
"github.com/cockroachdb/cockroach/pkg/sql/privilege"
@@ -265,17 +266,31 @@ func (b *Builder) buildWhereForStatistics(
265266
"WHERE filter must be on the same column as the one specified in the column list"))
266267
}
267268

268-
// Find a non-partial forward index with the filter column as the first key
269-
// column.
269+
// Add the partial index predicate expressions to the table metadata so we
270+
// can check if the filter implies any partial index predicates.
271+
b.addPartialIndexPredicatesForTable(tabMeta, nil /* scan */)
272+
273+
// Find a forward index with the filter column as the first key column.
270274
foundIndex := false
271275
var orderingCol opt.OrderingColumn
272276
var notNullCols opt.ColSet
277+
278+
var im partialidx.Implicator
279+
im.Init(b.ctx, b.factory, b.factory.Metadata(), b.evalCtx)
273280
for i := 0; i < tab.IndexCount(); i++ {
274281
idx := tab.Index(i)
275-
// TODO (uzair): Allow partial indexes that are implied by the filter.
276-
if _, isPartial := idx.Predicate(); isPartial || idx.Type() != idxtype.FORWARD {
282+
if idx.Type() != idxtype.FORWARD {
277283
continue
278284
}
285+
286+
// If this is a partial index, check if the filter implies its predicate.
287+
if pred, isPartialIndex := tabMeta.PartialIndexPredicate(i); isPartialIndex {
288+
predFilters := *pred.(*memo.FiltersExpr)
289+
if _, ok := im.FiltersImplyPredicate(fe, predFilters, tabMeta.ComputedCols); !ok {
290+
continue
291+
}
292+
}
293+
279294
if idx.KeyColumnCount() > 0 {
280295
col := idx.Column(0)
281296
if tabID.IndexColumnID(idx, 0) == filterColID {
@@ -292,7 +307,7 @@ func (b *Builder) buildWhereForStatistics(
292307
if !foundIndex {
293308
panic(
294309
pgerror.Newf(pgcode.InvalidColumnReference,
295-
"table %s does not contain a non-partial forward index with %s as a prefix column",
310+
"table %s does not contain a suitable index with %s as a prefix column",
296311
tab.Name(), tab.Column(tabID.ColumnOrdinal(filterColID)).ColName(),
297312
),
298313
)

0 commit comments

Comments
 (0)