From 35e97376049616228a638c5dc2308caed8d726fa Mon Sep 17 00:00:00 2001 From: Marcus Gartner Date: Wed, 29 Jun 2022 17:13:24 -0400 Subject: [PATCH] opt: constrain expression indexes with IS NULL expressions The optimizer can generate constrained scans over indexes on computed columns when columns referenced in the computed column expression are held constant. Consider this example: CREATE TABLE t (a INT, v INT AS (a + 1) STORED, INDEX v_idx (v)) SELECT * FROM t WHERE a = 1 A constrained scan can be generated over `v_idx` because `v` depends on `a` and the query filter holds `a` constant. This commit lifts a restriction that prevented this optimization when columns referenced in the computed column expression were held constant to the `NULL` value. As far as I can tell, this restriction is not necessary. In fact, @rytaft had questioned its purpose originally, but the question was never answered: https://github.com/cockroachdb/cockroach/pull/43450#pullrequestreview-336601771 By lifting this restriction, the optimizer can explore constrained scans over both indexed computed columns with `IS NULL` expressions and expression indexes with `IS NULL` expressions. Fixes #83390 Release note (performance improvement): The optimizer now explores more efficient query plans when index computed columns and expressions have `IS NULL` expressions. --- pkg/sql/opt/xform/general_funcs.go | 4 +-- pkg/sql/opt/xform/select_funcs.go | 7 ++-- pkg/sql/opt/xform/testdata/rules/computed | 40 +++++++++++++++++++---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/pkg/sql/opt/xform/general_funcs.go b/pkg/sql/opt/xform/general_funcs.go index da85ffd1959a..81de550e36c6 100644 --- a/pkg/sql/opt/xform/general_funcs.go +++ b/pkg/sql/opt/xform/general_funcs.go @@ -372,9 +372,7 @@ func (c *CustomFuncs) findConstantFilterCols( } datum := span.StartKey().Value(0) - if datum != tree.DNull { - constFilterCols[colID] = c.e.f.ConstructConstVal(datum, colTyp) - } + constFilterCols[colID] = c.e.f.ConstructConstVal(datum, colTyp) } } } diff --git a/pkg/sql/opt/xform/select_funcs.go b/pkg/sql/opt/xform/select_funcs.go index fa7b8d14da0c..9f7feffe7237 100644 --- a/pkg/sql/opt/xform/select_funcs.go +++ b/pkg/sql/opt/xform/select_funcs.go @@ -415,7 +415,7 @@ func (c *CustomFuncs) GenerateConstrainedScans( sb.Init(c, scanPrivate.Table) - // Check constraint and computed column filters + // Build optional filters from check constraint and computed column filters. optionalFilters, filterColumns := c.GetOptionalFiltersAndFilterColumns(explicitFilters, scanPrivate) @@ -424,7 +424,8 @@ func (c *CustomFuncs) GenerateConstrainedScans( iter.Init(c.e.evalCtx, c.e.f, c.e.mem, &c.im, scanPrivate, explicitFilters, rejectInvertedIndexes) iter.ForEach(func(index cat.Index, filters memo.FiltersExpr, indexCols opt.ColSet, isCovering bool, constProj memo.ProjectionsExpr) { - // A structure describing which index partitions are local to the gateway region + // Create a prefix sorter that describes which index partitions are + // local to the gateway region. prefixSorter, _ := tabMeta.IndexPartitionLocality(scanPrivate.Index, index, c.e.evalCtx) // Build Constraints to scan a subset of the table Spans. @@ -491,7 +492,7 @@ func (c *CustomFuncs) GenerateConstrainedScans( // tryFoldComputedCol tries to reduce the computed column with the given column // ID into a constant value, by evaluating it with respect to a set of other // columns that are constant. If the computed column is constant, enter it into -// the constCols map and return false. Otherwise, return false. +// the constCols map and return true. Otherwise, return false. // func (c *CustomFuncs) tryFoldComputedCol( tabMeta *opt.TableMeta, computedColID opt.ColumnID, constCols constColsMap, diff --git a/pkg/sql/opt/xform/testdata/rules/computed b/pkg/sql/opt/xform/testdata/rules/computed index 48d0dd33ac06..9ff88b5d768b 100644 --- a/pkg/sql/opt/xform/testdata/rules/computed +++ b/pkg/sql/opt/xform/testdata/rules/computed @@ -127,17 +127,14 @@ select └── filters └── (k_int:1 = 2) OR (k_int:1 = 3) [outer=(1), constraints=(/1: [/2 - /2] [/3 - /3]; tight)] -# Don't constrain the index for a NULL value. +# Constrain the index for a NULL value. opt SELECT k_int FROM t_int WHERE k_int IS NULL ---- -select +scan t_int@c_int_index ├── columns: k_int:1 - ├── fd: ()-->(1) - ├── scan t_int@c_int_index - │ └── columns: k_int:1 - └── filters - └── k_int:1 IS NULL [outer=(1), constraints=(/1: [/NULL - /NULL]; tight), fd=()-->(1)] + ├── constraint: /2/1/4: [/NULL/NULL - /NULL/NULL] + └── fd: ()-->(1) # Don't constrain the index when the computed column has a volatile function. @@ -267,3 +264,32 @@ project │ └── key: (1) └── filters └── d:4 = 1 [outer=(4), immutable, constraints=(/4: [/1 - /1]; tight), fd=()-->(4)] + +# Regression test for #83390. The optimizer should constrain an expression index +# with an IS NULL expression. +exec-ddl +CREATE TABLE t83390 ( + k INT PRIMARY KEY, + a INT, + INDEX idx ((a IS NULL)) +) +---- + +opt +SELECT * FROM t83390@idx WHERE a IS NULL +---- +select + ├── columns: k:1!null a:2 + ├── key: (1) + ├── fd: ()-->(2) + ├── index-join t83390 + │ ├── columns: k:1!null a:2 + │ ├── key: (1) + │ ├── fd: (1)-->(2) + │ └── scan t83390@idx + │ ├── columns: k:1!null + │ ├── constraint: /5/1: [/true - /true] + │ ├── flags: force-index=idx + │ └── key: (1) + └── filters + └── a:2 IS NULL [outer=(2), constraints=(/2: [/NULL - /NULL]; tight), fd=()-->(2)]