diff --git a/pkg/sql/opt/constraint/span.go b/pkg/sql/opt/constraint/span.go index dd66881fe2f7..1da02122ef96 100644 --- a/pkg/sql/opt/constraint/span.go +++ b/pkg/sql/opt/constraint/span.go @@ -13,6 +13,7 @@ package constraint import ( "bytes" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/errors" ) @@ -69,6 +70,24 @@ func (sp *Span) IsUnconstrained() bool { return startUnconstrained && endUnconstrained } +// HasSingleKey is true if the span contains exactly one key. This is true when +// the start key is the same as the end key, and both boundaries are inclusive. +func (sp *Span) HasSingleKey(evalCtx *tree.EvalContext) bool { + l := sp.start.Length() + if l == 0 || l != sp.end.Length() { + return false + } + if sp.startBoundary != IncludeBoundary || sp.endBoundary != IncludeBoundary { + return false + } + for i, n := 0, l; i < n; i++ { + if sp.start.Value(i).Compare(evalCtx, sp.end.Value(i)) != 0 { + return false + } + } + return true +} + // StartKey returns the start key. func (sp *Span) StartKey() Key { return sp.start diff --git a/pkg/sql/opt/constraint/span_test.go b/pkg/sql/opt/constraint/span_test.go index 234ba7d4a4e1..3fbcd6f71453 100644 --- a/pkg/sql/opt/constraint/span_test.go +++ b/pkg/sql/opt/constraint/span_test.go @@ -17,6 +17,7 @@ import ( "math" "testing" + "github.com/cockroachdb/cockroach/pkg/settings/cluster" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" ) @@ -114,6 +115,90 @@ func TestSpanUnconstrained(t *testing.T) { } } +func TestSpanSingleKey(t *testing.T) { + testCases := []struct { + start Key + startBoundary SpanBoundary + end Key + endBoundary SpanBoundary + expected bool + }{ + { // 0 + MakeKey(tree.NewDInt(1)), IncludeBoundary, + MakeKey(tree.NewDInt(1)), IncludeBoundary, + true, + }, + { // 1 + MakeKey(tree.NewDInt(1)), IncludeBoundary, + MakeKey(tree.NewDInt(2)), IncludeBoundary, + false, + }, + { // 2 + MakeKey(tree.NewDInt(1)), IncludeBoundary, + MakeKey(tree.NewDInt(1)), ExcludeBoundary, + false, + }, + { // 3 + MakeKey(tree.NewDInt(1)), ExcludeBoundary, + MakeKey(tree.NewDInt(1)), IncludeBoundary, + false, + }, + { // 4 + EmptyKey, IncludeBoundary, + MakeKey(tree.NewDInt(1)), IncludeBoundary, + false, + }, + { // 5 + MakeKey(tree.NewDInt(1)), IncludeBoundary, + EmptyKey, IncludeBoundary, + false, + }, + { // 6 + MakeKey(tree.NewDInt(1)), IncludeBoundary, + MakeKey(tree.DNull), IncludeBoundary, + false, + }, + { // 7 + MakeKey(tree.NewDString("a")), IncludeBoundary, + MakeKey(tree.NewDString("ab")), IncludeBoundary, + false, + }, + { // 8 + MakeCompositeKey(tree.NewDString("cherry"), tree.NewDInt(1)), IncludeBoundary, + MakeCompositeKey(tree.NewDString("cherry"), tree.NewDInt(1)), IncludeBoundary, + true, + }, + { // 9 + MakeCompositeKey(tree.NewDString("cherry"), tree.NewDInt(1)), IncludeBoundary, + MakeCompositeKey(tree.NewDString("mango"), tree.NewDInt(1)), IncludeBoundary, + false, + }, + { // 10 + MakeCompositeKey(tree.NewDString("cherry")), IncludeBoundary, + MakeCompositeKey(tree.NewDString("cherry"), tree.NewDInt(1)), IncludeBoundary, + false, + }, + { // 11 + MakeCompositeKey(tree.NewDString("cherry"), tree.NewDInt(1), tree.DNull), IncludeBoundary, + MakeCompositeKey(tree.NewDString("cherry"), tree.NewDInt(1), tree.DNull), IncludeBoundary, + true, + }, + } + + for i, tc := range testCases { + st := cluster.MakeTestingClusterSettings() + evalCtx := tree.MakeTestingEvalContext(st) + + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var sp Span + sp.Init(tc.start, tc.startBoundary, tc.end, tc.endBoundary) + if sp.HasSingleKey(&evalCtx) != tc.expected { + t.Errorf("expected: %v, actual: %v", tc.expected, !tc.expected) + } + }) + } +} + func TestSpanCompare(t *testing.T) { keyCtx := testKeyContext(1, 2) diff --git a/pkg/sql/opt/optbuilder/mutation_builder.go b/pkg/sql/opt/optbuilder/mutation_builder.go index 05805bc8a6d1..db279775fead 100644 --- a/pkg/sql/opt/optbuilder/mutation_builder.go +++ b/pkg/sql/opt/optbuilder/mutation_builder.go @@ -653,7 +653,7 @@ func findRoundingFunction(typ *types.T, precision int) (*tree.FunctionProperties // constraint defined on the target table. The mutation operator will report // a constraint violation error if the value of the column is false. func (mb *mutationBuilder) addCheckConstraintCols() { - if mb.tab.CheckCount() > 0 { + if mb.tab.CheckCount() != 0 { // Disambiguate names so that references in the constraint expression refer // to the correct columns. mb.disambiguateColumns() @@ -810,17 +810,7 @@ func (mb *mutationBuilder) buildReturning(returning tree.ReturningExprs) { // inScope := mb.outScope.replace() inScope.expr = mb.outScope.expr - inScope.cols = make([]scopeColumn, 0, mb.tab.ColumnCount()) - for i, n := 0, mb.tab.ColumnCount(); i < n; i++ { - tabCol := mb.tab.Column(i) - inScope.cols = append(inScope.cols, scopeColumn{ - name: tabCol.ColName(), - table: mb.alias, - typ: tabCol.DatumType(), - id: mb.tabID.ColumnID(i), - hidden: tabCol.IsHidden(), - }) - } + inScope.appendColumnsFromTable(mb.md.TableMeta(mb.tabID), &mb.alias) // extraAccessibleCols contains all the columns that the RETURNING // clause can refer to in addition to the table columns. This is useful for diff --git a/pkg/sql/opt/optbuilder/scope.go b/pkg/sql/opt/optbuilder/scope.go index 60c4893b96c2..8b4808c7b662 100644 --- a/pkg/sql/opt/optbuilder/scope.go +++ b/pkg/sql/opt/optbuilder/scope.go @@ -166,6 +166,25 @@ func (s *scope) appendColumnsFromScope(src *scope) { } } +// appendColumnsFromTable adds all columns from the given table metadata to this +// scope. +func (s *scope) appendColumnsFromTable(tabMeta *opt.TableMeta, alias *tree.TableName) { + tab := tabMeta.Table + if s.cols == nil { + s.cols = make([]scopeColumn, 0, tab.ColumnCount()) + } + for i, n := 0, tab.ColumnCount(); i < n; i++ { + tabCol := tab.Column(i) + s.cols = append(s.cols, scopeColumn{ + name: tabCol.ColName(), + table: *alias, + typ: tabCol.DatumType(), + id: tabMeta.MetaID.ColumnID(i), + hidden: tabCol.IsHidden(), + }) + } +} + // appendColumns adds newly bound variables to this scope. // The expressions in the new columns are reset to nil. func (s *scope) appendColumns(cols []scopeColumn) { diff --git a/pkg/sql/opt/optbuilder/select.go b/pkg/sql/opt/optbuilder/select.go index d45bb1fc2bbf..fad236b9de14 100644 --- a/pkg/sql/opt/optbuilder/select.go +++ b/pkg/sql/opt/optbuilder/select.go @@ -469,7 +469,8 @@ func (b *Builder) buildScan( } outScope.expr = b.factory.ConstructScan(&private) - b.addCheckConstraintsForTable(outScope, tabMeta, ordinals != nil /* allowMissingColumns */) + b.addCheckConstraintsForTable(tabMeta) + b.addComputedColsForTable(tabMeta) if b.trackViewDeps { dep := opt.ViewDep{DataSource: tab} @@ -489,17 +490,12 @@ func (b *Builder) buildScan( // addCheckConstraintsForTable finds all the check constraints that apply to the // table and adds them to the table metadata. To do this, the scalar expression // of the check constraints are built here. -// -// If allowMissingColumns is true, we ignore check constraints that involve -// columns not in the current scope (useful when we build a scan that doesn't -// contain all table columns). -func (b *Builder) addCheckConstraintsForTable( - scope *scope, tabMeta *opt.TableMeta, allowMissingColumns bool, -) { - tab := tabMeta.Table +func (b *Builder) addCheckConstraintsForTable(tabMeta *opt.TableMeta) { // Find all the check constraints that apply to the table and add them // to the table meta data. To do this, we must build them into scalar // expressions. + tableScope := scope{builder: b} + tab := tabMeta.Table for i, n := 0, tab.CheckCount(); i < n; i++ { checkConstraint := tab.Check(i) @@ -512,25 +508,40 @@ func (b *Builder) addCheckConstraintsForTable( panic(err) } - var texpr tree.TypedExpr - func() { - if allowMissingColumns { - // Swallow any undefined column errors. - defer func() { - if r := recover(); r != nil { - if err, ok := r.(error); ok { - if code := pgerror.GetPGCode(err); code == pgcode.UndefinedColumn { - return - } - } - panic(r) - } - }() - } - texpr = scope.resolveAndRequireType(expr, types.Bool) - }() - if texpr != nil { - tabMeta.AddConstraint(b.buildScalar(texpr, scope, nil, nil, nil)) + if len(tableScope.cols) == 0 { + tableScope.appendColumnsFromTable(tabMeta, &tabMeta.Alias) + } + + if texpr := tableScope.resolveAndRequireType(expr, types.Bool); texpr != nil { + scalar := b.buildScalar(texpr, &tableScope, nil, nil, nil) + tabMeta.AddConstraint(scalar) + } + } +} + +// addComputedColsForTable finds all computed columns in the given table and +// caches them in the table metadata as scalar expressions. +func (b *Builder) addComputedColsForTable(tabMeta *opt.TableMeta) { + tableScope := scope{builder: b} + tab := tabMeta.Table + for i, n := 0, tab.ColumnCount(); i < n; i++ { + tabCol := tab.Column(i) + if !tabCol.IsComputed() { + continue + } + expr, err := parser.ParseExpr(tabCol.ComputedExprStr()) + if err != nil { + continue + } + + if len(tableScope.cols) == 0 { + tableScope.appendColumnsFromTable(tabMeta, &tabMeta.Alias) + } + + if texpr := tableScope.resolveAndRequireType(expr, types.Any); texpr != nil { + colID := tabMeta.MetaID.ColumnID(i) + scalar := b.buildScalar(texpr, &tableScope, nil, nil, nil) + tabMeta.AddComputedCol(colID, scalar) } } } diff --git a/pkg/sql/opt/optgen/cmd/optgen/factory_gen.go b/pkg/sql/opt/optgen/cmd/optgen/factory_gen.go index 70666b2ae19c..7514f2d6d34e 100644 --- a/pkg/sql/opt/optgen/cmd/optgen/factory_gen.go +++ b/pkg/sql/opt/optgen/cmd/optgen/factory_gen.go @@ -158,19 +158,19 @@ func (g *factoryGen) genReplace() { g.w.writeIndent("// ancestors via a calls to the corresponding factory Construct methods. Here\n") g.w.writeIndent("// is example usage:\n") g.w.writeIndent("//\n") - g.w.writeIndent("// var replace func(e opt.Expr, replace ReplaceFunc) opt.Expr\n") - g.w.writeIndent("// replace = func(e opt.Expr, replace ReplaceFunc) opt.Expr {\n") + g.w.writeIndent("// var replace func(e opt.Expr) opt.Expr\n") + g.w.writeIndent("// replace = func(e opt.Expr) opt.Expr {\n") g.w.writeIndent("// if e.Op() == opt.VariableOp {\n") g.w.writeIndent("// return getReplaceVar(e)\n") g.w.writeIndent("// }\n") - g.w.writeIndent("// return e.Replace(e, replace)\n") + g.w.writeIndent("// return factory.Replace(e, replace)\n") g.w.writeIndent("// }\n") g.w.writeIndent("// replace(root, replace)\n") g.w.writeIndent("//\n") g.w.writeIndent("// Here, all variables in the tree are being replaced by some other expression\n") g.w.writeIndent("// in a pre-order traversal of the tree. Post-order traversal is trivially\n") - g.w.writeIndent("// achieved by moving the e.Replace call to the top of the replace function\n") - g.w.writeIndent("// rather than bottom.\n") + g.w.writeIndent("// achieved by moving the factory.Replace call to the top of the replace\n") + g.w.writeIndent("// function rather than bottom.\n") g.w.nestIndent("func (f *Factory) Replace(e opt.Expr, replace ReplaceFunc) opt.Expr {\n") g.w.writeIndent("switch t := e.(type) {\n") diff --git a/pkg/sql/opt/optgen/cmd/optgen/testdata/factory b/pkg/sql/opt/optgen/cmd/optgen/testdata/factory index da8b1800cde8..505028cb8a53 100644 --- a/pkg/sql/opt/optgen/cmd/optgen/testdata/factory +++ b/pkg/sql/opt/optgen/cmd/optgen/testdata/factory @@ -166,19 +166,19 @@ func (_f *Factory) ConstructKVOptionsItem( // ancestors via a calls to the corresponding factory Construct methods. Here // is example usage: // -// var replace func(e opt.Expr, replace ReplaceFunc) opt.Expr -// replace = func(e opt.Expr, replace ReplaceFunc) opt.Expr { +// var replace func(e opt.Expr) opt.Expr +// replace = func(e opt.Expr) opt.Expr { // if e.Op() == opt.VariableOp { // return getReplaceVar(e) // } -// return e.Replace(e, replace) +// return factory.Replace(e, replace) // } // replace(root, replace) // // Here, all variables in the tree are being replaced by some other expression // in a pre-order traversal of the tree. Post-order traversal is trivially -// achieved by moving the e.Replace call to the top of the replace function -// rather than bottom. +// achieved by moving the factory.Replace call to the top of the replace +// function rather than bottom. func (f *Factory) Replace(e opt.Expr, replace ReplaceFunc) opt.Expr { switch t := e.(type) { case *memo.SelectExpr: diff --git a/pkg/sql/opt/table_meta.go b/pkg/sql/opt/table_meta.go index 4cc1ca17c86c..213bfcf012b5 100644 --- a/pkg/sql/opt/table_meta.go +++ b/pkg/sql/opt/table_meta.go @@ -138,6 +138,12 @@ type TableMeta struct { // detail. Constraints []ScalarExpr + // ComputedCols stores ScalarExprs for each computed column on the table, + // indexed by ColumnID. These will be used when building mutation statements + // and constraining indexes. See comment above GenerateConstrainedScans for + // more detail. + ComputedCols map[ColumnID]ScalarExpr + // anns annotates the table metadata with arbitrary data. anns [maxTableAnnIDCount]interface{} } @@ -182,6 +188,14 @@ func (tm *TableMeta) AddConstraint(constraint ScalarExpr) { tm.Constraints = append(tm.Constraints, constraint) } +// AddComputedCol adds a computed column expression to the table's metadata. +func (tm *TableMeta) AddComputedCol(colID ColumnID, computedCol ScalarExpr) { + if tm.ComputedCols == nil { + tm.ComputedCols = make(map[ColumnID]ScalarExpr) + } + tm.ComputedCols[colID] = computedCol +} + // TableAnnotation returns the given annotation that is associated with the // given table. If the table has no such annotation, TableAnnotation returns // nil. diff --git a/pkg/sql/opt/xform/custom_funcs.go b/pkg/sql/opt/xform/custom_funcs.go index 87deba1ace39..dca8c0079668 100644 --- a/pkg/sql/opt/xform/custom_funcs.go +++ b/pkg/sql/opt/xform/custom_funcs.go @@ -24,6 +24,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt/props" "github.com/cockroachdb/cockroach/pkg/sql/opt/props/physical" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util" "github.com/cockroachdb/errors" @@ -120,6 +121,243 @@ func (c *CustomFuncs) GenerateIndexScans(grp memo.RelExpr, scanPrivate *memo.Sca // // ---------------------------------------------------------------------- +// GenerateConstrainedScans enumerates all secondary indexes on the Scan +// operator's table and tries to push the given Select filter into new +// constrained Scan operators using those indexes. Since this only needs to be +// done once per table, GenerateConstrainedScans should only be called on the +// original unaltered primary index Scan operator (i.e. not constrained or +// limited). +// +// For each secondary index that "covers" the columns needed by the scan, there +// are three cases: +// +// - a filter that can be completely converted to a constraint over that index +// generates a single constrained Scan operator (to be added to the same +// group as the original Select operator): +// +// (Scan $scanDef) +// +// - a filter that can be partially converted to a constraint over that index +// generates a constrained Scan operator in a new memo group, wrapped in a +// Select operator having the remaining filter (to be added to the same group +// as the original Select operator): +// +// (Select (Scan $scanDef) $filter) +// +// - a filter that cannot be converted to a constraint generates nothing +// +// And for a secondary index that does not cover the needed columns: +// +// - a filter that can be completely converted to a constraint over that index +// generates a single constrained Scan operator in a new memo group, wrapped +// in an IndexJoin operator that looks up the remaining needed columns (and +// is added to the same group as the original Select operator) +// +// (IndexJoin (Scan $scanDef) $indexJoinDef) +// +// - a filter that can be partially converted to a constraint over that index +// generates a constrained Scan operator in a new memo group, wrapped in an +// IndexJoin operator that looks up the remaining needed columns; the +// remaining filter is distributed above and/or below the IndexJoin, +// depending on which columns it references: +// +// (IndexJoin +// (Select (Scan $scanDef) $filter) +// $indexJoinDef +// ) +// +// (Select +// (IndexJoin (Scan $scanDef) $indexJoinDef) +// $filter +// ) +// +// (Select +// (IndexJoin +// (Select (Scan $scanDef) $innerFilter) +// $indexJoinDef +// ) +// $outerFilter +// ) +// +// GenerateConstrainedScans will further constrain the enumerated index scans +// by trying to use the check constraints and computed columns that apply to the +// table being scanned, as well as the partitioning defined for the index. See +// comments above checkColumnFilters, computedColFilters, and +// partitionValuesFilters for more detail. +func (c *CustomFuncs) GenerateConstrainedScans( + grp memo.RelExpr, scanPrivate *memo.ScanPrivate, explicitFilters memo.FiltersExpr, +) { + var sb indexScanBuilder + sb.init(c, scanPrivate.Table) + + // Generate implicit filters from constraints and computed columns and add + // them to the list of explicit filters provided in the query. + checkFilters := c.checkConstraintFilters(scanPrivate.Table) + expandedFilters := append(explicitFilters, checkFilters...) + + computedColFilters := c.computedColFilters(scanPrivate.Table, expandedFilters) + expandedFilters = append(expandedFilters, computedColFilters...) + + // Iterate over all indexes. + var iter scanIndexIter + md := c.e.mem.Metadata() + tabMeta := md.TableMeta(scanPrivate.Table) + iter.init(c.e.mem, scanPrivate) + for iter.next() { + // We may append to this slice below; avoid any potential aliasing by + // limiting its capacity (forcing append to reallocate). + allFilters := expandedFilters[:len(expandedFilters):len(expandedFilters)] + indexColumns := tabMeta.IndexKeyColumns(iter.indexOrdinal) + filterColumns := c.FilterOuterCols(allFilters) + firstIndexCol := scanPrivate.Table.ColumnID(iter.index.Column(0).Ordinal) + + // We only consider the partition values when a particular index can otherwise + // not be constrained. For indexes that are constrained, the partitioned values + // add no benefit as they don't really constrain anything. + // Furthermore, if the filters don't take advantage of the index (use any of the + // index columns), using the partition values add no benefit. + var constrainedInBetweenFilters memo.FiltersExpr + var isIndexPartitioned bool + if !filterColumns.Contains(firstIndexCol) && indexColumns.Intersects(filterColumns) { + // Add any partition filters if appropriate. + partitionFilters, inBetweenFilters := c.partitionValuesFilters(scanPrivate.Table, iter.index) + + if len(partitionFilters) > 0 { + // We must add the filters so when we generate the inBetween spans, they are + // also constrained. This is also needed so the remaining filters are generated + // correctly using the in between spans (some remaining filters may be blown + // by the partition constraints). + constrainedInBetweenFilters = append(inBetweenFilters, allFilters...) + allFilters = append(allFilters, partitionFilters...) + isIndexPartitioned = true + } + } + + // Check whether the filter can constrain the index. + constraint, remainingFilters, ok := c.tryConstrainIndex( + allFilters, scanPrivate.Table, iter.indexOrdinal, false /* isInverted */) + if !ok { + continue + } + + // If the index is partitioned (by list), then the constraints above only + // contain spans within the partition ranges. For correctness, we must + // also add the spans for the in between ranges. Consider the following index + // and its partition: + // + // CREATE INDEX orders_by_created_at + // ON orders (region, created_at, id) + // STORING (total) + // PARTITION BY LIST (region) + // ( + // PARTITION us_east1 VALUES IN ('us-east1'), + // PARTITION us_west1 VALUES IN ('us-west1'), + // PARTITION europe_west2 VALUES IN ('europe-west2') + // ) + // + // The constraint generated for the query: + // SELECT sum(total) FROM orders WHERE created_at >= '2019-05-04' AND created_at < '2019-05-05' + // is: + // + // [/'europe-west2'/'2019-05-04 00:00:00+00:00' - /'europe-west2'/'2019-05-04 23:59:59.999999+00:00'] [/'us-east1'/'2019-05-04 00:00:00+00:00' - /'us-east1'/'2019-05-04 23:59:59.999999+00:00'] [/'us-west1'/'2019-05-04 00:00:00+00:00' - /'us-west1'/'2019-05-04 23:59:59.999999+00:00'] + // + // You'll notice that the spans before europe-west2, after us-west1 and in between + // the defined partitions are missing. We must add these spans now, appropriately + // constrained using the filters. + // + // It is important that we add these spans after the partition spans are generated + // because otherwise these spans would merge with the partition spans and would + // disallow the partition spans (and the in between ones) to be constrained further. + // Using the partitioning example and the query above, if we added the in between + // spans at the same time as the partitioned ones, we would end up with a span that + // looked like: + // + // [ - /'europe-west2'/'2019-05-04 23:59:59.999999+00:00'] ... + // + // However, allowing the partition spans to be constrained further and then adding the + // spans give us a more constrained index scan as shown below: + // + // [ - /'europe-west2') [/'europe-west2'/'2019-05-04 00:00:00+00:00' - /'europe-west2'/'2019-05-04 23:59:59.999999+00:00'] ... + // + // Notice how we 'skip' all the europe-west2 values that satisfy (created_at < '2019-05-04') + if isIndexPartitioned { + inBetweenConstraint, inBetweenRemainingFilters, ok := c.tryConstrainIndex( + constrainedInBetweenFilters, scanPrivate.Table, iter.indexOrdinal, false /* isInverted */) + if !ok { + panic(errors.AssertionFailedf("constraining index should not failed with the in between filters")) + } + + constraint.UnionWith(c.e.evalCtx, inBetweenConstraint) + + // Even though the partitioned constrains and the inBetween constraints + // were consolidated, we must make sure their Union is as well. + constraint.ConsolidateSpans(c.e.evalCtx) + + // Add all remaining filters that need to be present in the + // inBetween spans. Some of the remaining filters are common + // between them, so we must deduplicate them. + remainingFilters = c.ConcatFilters(remainingFilters, inBetweenRemainingFilters) + remainingFilters.Sort() + remainingFilters.Deduplicate() + } + + // If a check constraint filter or a partition filter wasn't able to + // constrain the index, it should not be used anymore for this group + // expression. + // TODO(ridwanmsharif): Does it ever make sense for us to continue + // using any constraint filter that wasn't able to constrain a scan? + // Maybe once we have more information about data distribution, we may + // use it to further constrain an index scan. We should revisit this + // once we have index skip scans. A constraint that may not constrain + // an index scan may still allow the index to be used more effectively + // if an index skip scan is possible. + if len(explicitFilters) != len(allFilters) { + remainingFilters.RetainCommonFilters(explicitFilters) + } + + // Construct new constrained ScanPrivate. + newScanPrivate := *scanPrivate + newScanPrivate.Index = iter.indexOrdinal + newScanPrivate.Constraint = constraint + // Record whether we were able to use partitions to constrain the scan. + newScanPrivate.PartitionConstrainedScan = isIndexPartitioned + + // If the alternate index includes the set of needed columns, then construct + // a new Scan operator using that index. + if iter.isCovering() { + sb.setScan(&newScanPrivate) + + // If there are remaining filters, then the constrained Scan operator + // will be created in a new group, and a Select operator will be added + // to the same group as the original operator. + sb.addSelect(remainingFilters) + + sb.build(grp) + continue + } + + // Otherwise, construct an IndexJoin operator that provides the columns + // missing from the index. + if scanPrivate.Flags.NoIndexJoin { + continue + } + + // Scan whatever columns we need which are available from the index, plus + // the PK columns. + newScanPrivate.Cols = iter.indexCols().Intersection(scanPrivate.Cols) + newScanPrivate.Cols.UnionWith(sb.primaryKeyCols()) + sb.setScan(&newScanPrivate) + + // If remaining filter exists, split it into one part that can be pushed + // below the IndexJoin, and one part that needs to stay above. + remainingFilters = sb.addSelectAfterSplit(remainingFilters, newScanPrivate.Cols) + sb.addIndexJoin(scanPrivate.Cols) + sb.addSelect(remainingFilters) + + sb.build(grp) + } +} + // checkConstraintFilters generates all filters that we can derive from the // check constraints. These are constraints that have been validated and are // non-nullable. We only use non-nullable check constraints because they @@ -187,50 +425,178 @@ func (c *CustomFuncs) checkConstraintFilters(tabID opt.TableID) memo.FiltersExpr return checkFilters } -// columnComparison returns a filter that compares the index columns to the -// given values. The comp parameter can be -1, 0 or 1 to indicate whether the -// comparison type of the filter should be a Lt, Eq or Gt. -func (c *CustomFuncs) columnComparison( - tabID opt.TableID, index cat.Index, values tree.Datums, comp int, -) opt.ScalarExpr { - colTypes := make([]types.T, len(values)) - for i := range values { - colTypes[i] = *values[i].ResolvedType() +// computedColFilters generates all filters that can be derived from the list of +// computed column expressions from the given table. A computed column can be +// used as a filter when it has a constant value. That is true when: +// +// 1. All other columns it references are constant, because other filters in +// the query constrain them to be so. +// 2. All functions in the computed column expression can be folded into +// constants (i.e. they do not have problematic side effects). +// +// Note that computed columns can depend on other computed columns; in general +// the dependencies form an acyclic directed graph. computedColFilters will +// return filters for all constant computed columns, regardless of the order of +// their dependencies. +// +// As with checkConstraintFilters, computedColFilters do not really filter any +// rows, they are rather facts or guarantees about the data. Treating them as +// filters may allow some indexes to be constrained and used. Consider the +// following example: +// +// CREATE TABLE t ( +// k INT NOT NULL, +// hash INT AS (k % 4) STORED, +// PRIMARY KEY (hash, k) +// ) +// +// SELECT * FROM t WHERE k = 5 +// +// Notice that the filter provided explicitly wouldn't allow the optimizer to +// seek using the primary index (it would have to fall back to a table scan). +// However, column "hash" can be proven to have the constant value of 1, since +// it's dependent on column "k", which has the constant value of 5. This enables +// usage of the primary index: +// +// scan t +// ├── columns: k:1(int!null) hash:2(int!null) +// ├── constraint: /2/1: [/1/5 - /1/5] +// ├── key: (2) +// └── fd: ()-->(1) +// +// The values of both columns in that index are known, enabling a single value +// constraint to be generated. +func (c *CustomFuncs) computedColFilters( + tabID opt.TableID, filters memo.FiltersExpr, +) memo.FiltersExpr { + tabMeta := c.e.mem.Metadata().TableMeta(tabID) + if len(tabMeta.ComputedCols) == 0 { + return nil } - columnVariables := make(memo.ScalarListExpr, len(values)) - scalarValues := make(memo.ScalarListExpr, len(values)) - - for i, val := range values { - colID := tabID.ColumnID(index.Column(i).Ordinal) - columnVariables[i] = c.e.f.ConstructVariable(colID) - scalarValues[i] = c.e.f.ConstructConstVal(val, val.ResolvedType()) + // Start with set of constant columns, as derived from the list of filter + // conditions. + constCols := c.findConstantFilterCols(tabID, filters) + if len(constCols) == 0 { + // No constant values could be derived from filters, so assume that there + // are also no constant computed columns. + return nil } - colsTuple := c.e.f.ConstructTuple(columnVariables, types.MakeTuple(colTypes)) - valsTuple := c.e.f.ConstructTuple(scalarValues, types.MakeTuple(colTypes)) - if comp == 0 { - return c.e.f.ConstructEq(colsTuple, valsTuple) - } else if comp > 0 { - return c.e.f.ConstructGt(colsTuple, valsTuple) + // Construct a new filter condition for each computed column that is + // constant (i.e. all of its variables are in the constCols set). + var computedColFilters memo.FiltersExpr + for colID := range tabMeta.ComputedCols { + if c.tryFoldComputedCol(tabMeta, colID, constCols) { + constVal := constCols[colID] + eqOp := c.e.f.ConstructEq(c.e.f.ConstructVariable(colID), constVal) + computedColFilters = append(computedColFilters, c.e.f.ConstructFiltersItem(eqOp)) + } } + return computedColFilters +} - return c.e.f.ConstructLt(colsTuple, valsTuple) +// findConstantFilterCols returns a mapping from table column ID to the constant +// value of that column. It does this by iterating over the given list of +// filters and finding expressions that constrain columns to a single constant +// value. For example: +// +// x = 5 AND y = 'foo' +// +// This would return a mapping from x => 5 and y => 'foo', which constants can +// then be used to prove that dependent computed columns are also constant. +func (c *CustomFuncs) findConstantFilterCols( + tabID opt.TableID, filters memo.FiltersExpr, +) map[opt.ColumnID]opt.ScalarExpr { + tab := c.e.mem.Metadata().Table(tabID) + constFilterCols := make(map[opt.ColumnID]opt.ScalarExpr) + for i := range filters { + // If filter constraints are not tight, then no way to derive constant + // values. + props := filters[i].ScalarProps() + if !props.TightConstraints { + continue + } + + // Iterate over constraint conjuncts with a single column and single + // span having a single key. + for i, n := 0, props.Constraints.Length(); i < n; i++ { + cons := props.Constraints.Constraint(i) + if cons.Columns.Count() != 1 || cons.Spans.Count() != 1 { + continue + } + + // Skip columns with a data type that uses a composite key encoding. + // Each of these data types can have multiple distinct values that + // compare equal. For example, 0 == -0 for the FLOAT data type. It's + // not safe to treat these as constant inputs to computed columns, + // since the computed expression may differentiate between the + // different forms of the same value. + colID := cons.Columns.Get(0).ID() + colTyp := tab.Column(tabID.ColumnOrdinal(colID)).DatumType() + if sqlbase.DatumTypeHasCompositeKeyEncoding(colTyp) { + continue + } + + span := cons.Spans.Get(0) + if !span.HasSingleKey(c.e.evalCtx) { + continue + } + + datum := span.StartKey().Value(0) + if datum != tree.DNull { + constFilterCols[colID] = c.e.f.ConstructConstVal(datum, colTyp) + } + } + } + return constFilterCols } -// isPrefixOf returns whether pre is a prefix of other. -func (c *CustomFuncs) isPrefixOf(pre []tree.Datum, other []tree.Datum) bool { - if len(pre) > len(other) { - // Pre can't be a prefix of other as it is larger. - return false +// 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. +func (c *CustomFuncs) tryFoldComputedCol( + tabMeta *opt.TableMeta, computedColID opt.ColumnID, constCols map[opt.ColumnID]opt.ScalarExpr, +) bool { + // Check whether computed column has already been folded. + if _, ok := constCols[computedColID]; ok { + return true } - for i := range pre { - if pre[i].Compare(c.e.evalCtx, other[i]) != 0 { - return false + + var replace func(e opt.Expr) opt.Expr + replace = func(e opt.Expr) opt.Expr { + if variable, ok := e.(*memo.VariableExpr); ok { + // Can variable be folded? + if constVal, ok := constCols[variable.Col]; ok { + // Yes, so replace it with its constant value. + return constVal + } + + // No, but that may be because the variable refers to a dependent + // computed column. In that case, try to recursively fold that + // computed column. There are no infinite loops possible because the + // dependency graph is guaranteed to be acyclic. + if _, ok := tabMeta.ComputedCols[variable.Col]; ok { + if c.tryFoldComputedCol(tabMeta, variable.Col, constCols) { + return constCols[variable.Col] + } + } + + return e } + return c.e.f.Replace(e, replace) } - return true + computedCol := tabMeta.ComputedCols[computedColID] + replaced := replace(computedCol).(opt.ScalarExpr) + + // If the computed column is constant, enter it into the constCols map. + if opt.IsConstValueOp(replaced) { + constCols[computedColID] = replaced + return true + } + return false } // inBetweenFilters returns a set of filters that are required to cover all the @@ -279,15 +645,46 @@ func (c *CustomFuncs) inBetweenFilters( largerThanLower = c.columnComparison(tabID, index, lowerPartition, 1) } - smallerThanHigher := c.columnComparison(tabID, index, higherPartition, -1) + smallerThanHigher := c.columnComparison(tabID, index, higherPartition, -1) + + // Add the in-between span to the list of inBetween spans. + betweenExpr := c.e.f.ConstructAnd(largerThanLower, smallerThanHigher) + inBetween = append(inBetween, betweenExpr) + } + + // Return an Or expression between all the expressions. + return memo.FiltersExpr{c.e.f.ConstructFiltersItem(c.constructOr(inBetween))} +} + +// columnComparison returns a filter that compares the index columns to the +// given values. The comp parameter can be -1, 0 or 1 to indicate whether the +// comparison type of the filter should be a Lt, Eq or Gt. +func (c *CustomFuncs) columnComparison( + tabID opt.TableID, index cat.Index, values tree.Datums, comp int, +) opt.ScalarExpr { + colTypes := make([]types.T, len(values)) + for i := range values { + colTypes[i] = *values[i].ResolvedType() + } + + columnVariables := make(memo.ScalarListExpr, len(values)) + scalarValues := make(memo.ScalarListExpr, len(values)) + + for i, val := range values { + colID := tabID.ColumnID(index.Column(i).Ordinal) + columnVariables[i] = c.e.f.ConstructVariable(colID) + scalarValues[i] = c.e.f.ConstructConstVal(val, val.ResolvedType()) + } - // Add the in-between span to the list of inBetween spans. - betweenExpr := c.e.f.ConstructAnd(largerThanLower, smallerThanHigher) - inBetween = append(inBetween, betweenExpr) + colsTuple := c.e.f.ConstructTuple(columnVariables, types.MakeTuple(colTypes)) + valsTuple := c.e.f.ConstructTuple(scalarValues, types.MakeTuple(colTypes)) + if comp == 0 { + return c.e.f.ConstructEq(colsTuple, valsTuple) + } else if comp > 0 { + return c.e.f.ConstructGt(colsTuple, valsTuple) } - // Return an Or expression between all the expressions. - return memo.FiltersExpr{c.e.f.ConstructFiltersItem(c.constructOr(inBetween))} + return c.e.f.ConstructLt(colsTuple, valsTuple) } // inPartitionFilters returns a FiltersExpr that is required to cover @@ -339,6 +736,21 @@ func (c *CustomFuncs) inPartitionFilters( return memo.FiltersExpr{c.e.f.ConstructFiltersItem(c.constructOr(partitions))} } +// isPrefixOf returns whether pre is a prefix of other. +func (c *CustomFuncs) isPrefixOf(pre []tree.Datum, other []tree.Datum) bool { + if len(pre) > len(other) { + // Pre can't be a prefix of other as it is larger. + return false + } + for i := range pre { + if pre[i].Compare(c.e.evalCtx, other[i]) != 0 { + return false + } + } + + return true +} + // constructOr constructs an expression that is an OR between all the // provided conditions func (c *CustomFuncs) constructOr(conditions memo.ScalarListExpr) opt.ScalarExpr { @@ -416,241 +828,6 @@ func (c *CustomFuncs) partitionValuesFilters( return inPartition, inBetween } -// GenerateConstrainedScans enumerates all secondary indexes on the Scan -// operator's table and tries to push the given Select filter into new -// constrained Scan operators using those indexes. Since this only needs to be -// done once per table, GenerateConstrainedScans should only be called on the -// original unaltered primary index Scan operator (i.e. not constrained or -// limited). -// -// For each secondary index that "covers" the columns needed by the scan, there -// are three cases: -// -// - a filter that can be completely converted to a constraint over that index -// generates a single constrained Scan operator (to be added to the same -// group as the original Select operator): -// -// (Scan $scanDef) -// -// - a filter that can be partially converted to a constraint over that index -// generates a constrained Scan operator in a new memo group, wrapped in a -// Select operator having the remaining filter (to be added to the same group -// as the original Select operator): -// -// (Select (Scan $scanDef) $filter) -// -// - a filter that cannot be converted to a constraint generates nothing -// -// And for a secondary index that does not cover the needed columns: -// -// - a filter that can be completely converted to a constraint over that index -// generates a single constrained Scan operator in a new memo group, wrapped -// in an IndexJoin operator that looks up the remaining needed columns (and -// is added to the same group as the original Select operator) -// -// (IndexJoin (Scan $scanDef) $indexJoinDef) -// -// - a filter that can be partially converted to a constraint over that index -// generates a constrained Scan operator in a new memo group, wrapped in an -// IndexJoin operator that looks up the remaining needed columns; the -// remaining filter is distributed above and/or below the IndexJoin, -// depending on which columns it references: -// -// (IndexJoin -// (Select (Scan $scanDef) $filter) -// $indexJoinDef -// ) -// -// (Select -// (IndexJoin (Scan $scanDef) $indexJoinDef) -// $filter -// ) -// -// (Select -// (IndexJoin -// (Select (Scan $scanDef) $innerFilter) -// $indexJoinDef -// ) -// $outerFilter -// ) -// -// GenerateConstrainedScans will further constrain the enumerated index scans -// by trying to use the check constraints that apply to the table being -// scanned and the partitioning defined for the index. See comments above -// checkColumnFilters and partitionValuesFilters respectively for more -// detail. -func (c *CustomFuncs) GenerateConstrainedScans( - grp memo.RelExpr, scanPrivate *memo.ScanPrivate, explicitFilters memo.FiltersExpr, -) { - var sb indexScanBuilder - sb.init(c, scanPrivate.Table) - - // Generate appropriate filters from constraints. - checkFilters := c.checkConstraintFilters(scanPrivate.Table) - - // Consider the checkFilters as well to constrain each of the indexes. - explicitAndCheckFilters := append(explicitFilters, checkFilters...) - - // Iterate over all indexes. - var iter scanIndexIter - md := c.e.mem.Metadata() - tabMeta := md.TableMeta(scanPrivate.Table) - iter.init(c.e.mem, scanPrivate) - for iter.next() { - // We may append to this slice below; avoid any potential aliasing by - // limiting its capacity (forcing append to reallocate). - filters := explicitAndCheckFilters[:len(explicitAndCheckFilters):len(explicitAndCheckFilters)] - indexColumns := tabMeta.IndexKeyColumns(iter.indexOrdinal) - filterColumns := c.FilterOuterCols(filters) - firstIndexCol := scanPrivate.Table.ColumnID(iter.index.Column(0).Ordinal) - - // We only consider the partition values when a particular index can otherwise - // not be constrained. For indexes that are constrained, the partitioned values - // add no benefit as they don't really constrain anything. - // Furthermore, if the filters don't take advantage of the index (use any of the - // index columns), using the partition values add no benefit. - var constrainedInBetweenFilters memo.FiltersExpr - var isIndexPartitioned bool - if !filterColumns.Contains(firstIndexCol) && indexColumns.Intersects(filterColumns) { - // Add any partition filters if appropriate. - partitionFilters, inBetweenFilters := c.partitionValuesFilters(scanPrivate.Table, iter.index) - - if len(partitionFilters) > 0 { - // We must add the filters so when we generate the inBetween spans, they are - // also constrained. This is also needed so the remaining filters are generated - // correctly using the in between spans (some remaining filters may be blown - // by the partition constraints). - constrainedInBetweenFilters = append(inBetweenFilters, filters...) - filters = append(filters, partitionFilters...) - isIndexPartitioned = true - } - } - - // Check whether the filter can constrain the index. - constraint, remainingFilters, ok := c.tryConstrainIndex( - filters, scanPrivate.Table, iter.indexOrdinal, false /* isInverted */) - if !ok { - continue - } - - // If the index is partitioned (by list), then the constraints above only - // contain spans within the partition ranges. For correctness, we must - // also add the spans for the in between ranges. Consider the following index - // and its partition: - // - // CREATE INDEX orders_by_created_at - // ON orders (region, created_at, id) - // STORING (total) - // PARTITION BY LIST (region) - // ( - // PARTITION us_east1 VALUES IN ('us-east1'), - // PARTITION us_west1 VALUES IN ('us-west1'), - // PARTITION europe_west2 VALUES IN ('europe-west2') - // ) - // - // The constraint generated for the query: - // SELECT sum(total) FROM orders WHERE created_at >= '2019-05-04' AND created_at < '2019-05-05' - // is: - // - // [/'europe-west2'/'2019-05-04 00:00:00+00:00' - /'europe-west2'/'2019-05-04 23:59:59.999999+00:00'] [/'us-east1'/'2019-05-04 00:00:00+00:00' - /'us-east1'/'2019-05-04 23:59:59.999999+00:00'] [/'us-west1'/'2019-05-04 00:00:00+00:00' - /'us-west1'/'2019-05-04 23:59:59.999999+00:00'] - // - // You'll notice that the spans before europe-west2, after us-west1 and in between - // the defined partitions are missing. We must add these spans now, appropriately - // constrained using the filters. - // - // It is important that we add these spans after the partition spans are generated - // because otherwise these spans would merge with the partition spans and would - // disallow the partition spans (and the in between ones) to be constrained further. - // Using the partitioning example and the query above, if we added the in between - // spans at the same time as the partitioned ones, we would end up with a span that - // looked like: - // - // [ - /'europe-west2'/'2019-05-04 23:59:59.999999+00:00'] ... - // - // However, allowing the partition spans to be constrained further and then adding the - // spans give us a more constrained index scan as shown below: - // - // [ - /'europe-west2') [/'europe-west2'/'2019-05-04 00:00:00+00:00' - /'europe-west2'/'2019-05-04 23:59:59.999999+00:00'] ... - // - // Notice how we 'skip' all the europe-west2 values that satisfy (created_at < '2019-05-04') - if isIndexPartitioned { - inBetweenConstraint, inBetweenRemainingFilters, ok := c.tryConstrainIndex( - constrainedInBetweenFilters, scanPrivate.Table, iter.indexOrdinal, false /* isInverted */) - if !ok { - panic(errors.AssertionFailedf("constraining index should not failed with the in between filters")) - } - - constraint.UnionWith(c.e.evalCtx, inBetweenConstraint) - - // Even though the partitioned constrains and the inBetween constraints - // were consolidated, we must make sure their Union is as well. - constraint.ConsolidateSpans(c.e.evalCtx) - - // Add all remaining filters that need to be present in the - // inBetween spans. Some of the remaining filters are common - // between them, so we must deduplicate them. - remainingFilters = c.ConcatFilters(remainingFilters, inBetweenRemainingFilters) - remainingFilters.Sort() - remainingFilters.Deduplicate() - } - - // If a check constraint filter or a partition filter wasn't able to - // constrain the index, it should not be used anymore for this group - // expression. - // TODO(ridwanmsharif): Does it ever make sense for us to continue - // using any constraint filter that wasn't able to constrain a scan? - // Maybe once we have more information about data distribution, we may - // use it to further constrain an index scan. We should revisit this - // once we have index skip scans. A constraint that may not constrain - // an index scan may still allow the index to be used more effectively - // if an index skip scan is possible. - if len(checkFilters) != 0 || isIndexPartitioned { - remainingFilters.RetainCommonFilters(explicitFilters) - } - - // Construct new constrained ScanPrivate. - newScanPrivate := *scanPrivate - newScanPrivate.Index = iter.indexOrdinal - newScanPrivate.Constraint = constraint - // Record whether we were able to use partitions to constrain the scan. - newScanPrivate.PartitionConstrainedScan = isIndexPartitioned - - // If the alternate index includes the set of needed columns, then construct - // a new Scan operator using that index. - if iter.isCovering() { - sb.setScan(&newScanPrivate) - - // If there are remaining filters, then the constrained Scan operator - // will be created in a new group, and a Select operator will be added - // to the same group as the original operator. - sb.addSelect(remainingFilters) - - sb.build(grp) - continue - } - - // Otherwise, construct an IndexJoin operator that provides the columns - // missing from the index. - if scanPrivate.Flags.NoIndexJoin { - continue - } - - // Scan whatever columns we need which are available from the index, plus - // the PK columns. - newScanPrivate.Cols = iter.indexCols().Intersection(scanPrivate.Cols) - newScanPrivate.Cols.UnionWith(sb.primaryKeyCols()) - sb.setScan(&newScanPrivate) - - // If remaining filter exists, split it into one part that can be pushed - // below the IndexJoin, and one part that needs to stay above. - remainingFilters = sb.addSelectAfterSplit(remainingFilters, newScanPrivate.Cols) - sb.addIndexJoin(scanPrivate.Cols) - sb.addSelect(remainingFilters) - - sb.build(grp) - } -} - // HasInvertedIndexes returns true if at least one inverted index is defined on // the Scan operator's table. func (c *CustomFuncs) HasInvertedIndexes(scanPrivate *memo.ScanPrivate) bool { diff --git a/pkg/sql/opt/xform/testdata/rules/computed b/pkg/sql/opt/xform/testdata/rules/computed new file mode 100644 index 000000000000..d5556cf9dd43 --- /dev/null +++ b/pkg/sql/opt/xform/testdata/rules/computed @@ -0,0 +1,137 @@ +# -------------------------------------------------- +# GenerateConstrainedScans + Computed Cols +# -------------------------------------------------- + +exec-ddl +CREATE TABLE t_int ( + k_int INT, + c_int INT AS (k_int % 4) STORED, + c_int_2 INT AS (k_int % 4) STORED, + INDEX c_int_index (c_int, k_int) +) +---- + +exec-ddl +CREATE TABLE t_float ( + k_float FLOAT, + c_float FLOAT AS (k_float + 1) STORED, + INDEX c_float_index (c_float, k_float) +) +---- + +exec-ddl +CREATE TABLE t_now ( + k_interval INTERVAL, + c_ts TIMESTAMP AS (now() + k_interval) STORED, + INDEX c_ts_index (c_ts, k_interval) +) +---- + +exec-ddl +CREATE TABLE t_mult ( + k_int INT, + k_int_2 INT, + c_int INT AS (k_int % 4) STORED, + c_mult INT AS (c_mult_2 * c_int * k_int * k_int_2) STORED, + c_mult_2 INT AS (k_int + 1) STORED, + INDEX c_mult_index (c_mult, c_mult_2, c_int, k_int, k_int_2) +) +---- + +exec-ddl +CREATE TABLE hashed ( + k STRING, + hash INT AS (fnv32(k) % 4) STORED CHECK (hash IN (0, 1, 2, 3)), + INDEX (hash, k) +) +---- + +# Constrain the index using computed column. Ensure that another computed column +# depending on the same base column isn't included as a filter (c_int_2). +opt +SELECT k_int FROM t_int WHERE k_int = 5 +---- +scan t_int@c_int_index + ├── columns: k_int:1(int!null) + ├── constraint: /2/1/4: [/1/5 - /1/5] + └── fd: ()-->(1) + +# Use index with multiple computed columns, based on multiple input columns in +# acyclic graph. +opt +SELECT k_int, k_int_2, c_mult, c_mult_2, c_int FROM t_mult WHERE k_int = 5 AND k_int_2 = 10 +---- +scan t_mult@c_mult_index + ├── columns: k_int:1(int!null) k_int_2:2(int!null) c_mult:4(int) c_mult_2:5(int) c_int:3(int) + ├── constraint: /4/5/3/1/2/6: [/300/6/1/5/10 - /300/6/1/5/10] + └── fd: ()-->(1,2) + +# Test computed + check columns in same table. +opt +SELECT * FROM hashed WHERE k = 'andy' +---- +scan hashed@secondary + ├── columns: k:1(string!null) hash:2(int) + ├── constraint: /2/1/3: [/1/'andy' - /1/'andy'] + └── fd: ()-->(1) + +# Don't constrain when filter has multiple columns. +opt +SELECT k_int FROM t_mult WHERE (k_int, k_int_2) > (1, 2) +---- +project + ├── columns: k_int:1(int!null) + └── select + ├── columns: k_int:1(int!null) k_int_2:2(int) + ├── scan t_mult + │ └── columns: k_int:1(int) k_int_2:2(int) + └── filters + └── (k_int, k_int_2) > (1, 2) [type=bool, outer=(1,2), constraints=(/1/2: [/1/3 - ]; tight)] + +# Don't constrain when filter has multiple spans. +opt +SELECT k_int FROM t_mult WHERE k_int = 2 OR k_int = 3 +---- +select + ├── columns: k_int:1(int!null) + ├── scan t_mult + │ └── columns: k_int:1(int) + └── filters + └── (k_int = 2) OR (k_int = 3) [type=bool, outer=(1), constraints=(/1: [/2 - /2] [/3 - /3])] + +# Don't constrain the index for a NULL value. +opt +SELECT k_int FROM t_int WHERE k_int IS NULL +---- +select + ├── columns: k_int:1(int) + ├── fd: ()-->(1) + ├── scan t_int@c_int_index + │ └── columns: k_int:1(int) + └── filters + └── k_int IS NULL [type=bool, outer=(1), constraints=(/1: [/NULL - /NULL]; tight), fd=()-->(1)] + +# Don't constrain the index for a FLOAT column, since the FLOAT data type uses +# a composite key encoding. +opt +SELECT k_float FROM t_float WHERE k_float = 5.0 +---- +select + ├── columns: k_float:1(float!null) + ├── fd: ()-->(1) + ├── scan t_float + │ └── columns: k_float:1(float) + └── filters + └── k_float = 5.0 [type=bool, outer=(1), constraints=(/1: [/5.0 - /5.0]; tight), fd=()-->(1)] + +# Don't constrain the index when the computed column has a non-pure function. +opt +SELECT k_interval FROM t_now WHERE k_interval = '3 hours' +---- +select + ├── columns: k_interval:1(interval!null) + ├── fd: ()-->(1) + ├── scan t_now + │ └── columns: k_interval:1(interval) + └── filters + └── k_interval = '03:00:00' [type=bool, outer=(1), constraints=(/1: [/'03:00:00' - /'03:00:00']; tight), fd=()-->(1)]