From d4b920d9e1e50ada2e13a0f56bc44ef6fe01a264 Mon Sep 17 00:00:00 2001 From: Michael Erickson Date: Mon, 13 Apr 2026 22:43:20 -0700 Subject: [PATCH] opt: add optimizer_span_limit and use in GenerateConstrainedScans This commit creates a new session variable `optimizer_span_limit`, and uses it to bound the number of spans in constrained scans produced by `GenerateConstrainedScans`. Fixes: #167620 Release note (sql change): Add a new session variable `optimizer_span_limit` which bounds the number of spans the optimizer will allow in a single constrained index scan. If a single IN set has more items than this limit, that IN set will not be used to build a constrained index scan. If the cross product of two or more IN sets would produce more spans than this limit for a composite index, then only a prefix of the IN sets will be used to produce spans. For example, for the following table and query, only the predicates on columns `a` and `b` will be used to construct the constrained scan of `abc_idx`, because including the predicate on column `c` would produce more spans than `optimizer_span_limit`: ```sql CREATE TABLE abc (a INT, b INT, c INT, INDEX abc_idx (a, b, c)); SET optimizer_span_limit = 10; SELECT * FROM abc WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11); ``` Co-Authored-By: roachdev-claude --- docs/generated/sql/session_vars.md | 1 + .../testdata/logic_test/information_schema | 1 + .../logictest/testdata/logic_test/pg_catalog | 3 + .../testdata/logic_test/select_index | 56 ++++++- .../logictest/testdata/logic_test/show_source | 1 + .../exec/execbuilder/testdata/select_index | 97 +++++++++++ pkg/sql/opt/idxconstraint/BUILD.bazel | 1 + .../opt/idxconstraint/index_constraints.go | 60 ++++++- .../idxconstraint/index_constraints_test.go | 17 ++ pkg/sql/opt/idxconstraint/testdata/span-limit | 158 ++++++++++++++++++ .../opt/invertedidx/inverted_index_expr.go | 3 + pkg/sql/opt/memo/memo.go | 3 + pkg/sql/opt/memo/memo_test.go | 6 + pkg/sql/opt/optbuilder/misc_statements.go | 4 +- pkg/sql/opt/xform/general_funcs.go | 7 +- pkg/sql/opt/xform/scan_funcs.go | 5 + pkg/sql/opt/xform/select_funcs.go | 10 +- pkg/sql/opt/xform/testdata/rules/select | 129 ++++++++++++++ pkg/sql/session_var_descriptions.go | 1 + .../local_only_session_data.proto | 3 + pkg/sql/sessionmutator/mutator.go | 4 + pkg/sql/vars.go | 29 ++++ 22 files changed, 590 insertions(+), 9 deletions(-) create mode 100644 pkg/sql/opt/idxconstraint/testdata/span-limit diff --git a/docs/generated/sql/session_vars.md b/docs/generated/sql/session_vars.md index 837836b79fd0..5c68b8610a41 100644 --- a/docs/generated/sql/session_vars.md +++ b/docs/generated/sql/session_vars.md @@ -161,6 +161,7 @@ SHOW application_name; optimizer_prove_implication_with_virtual_computed_columnsControls whether the optimizer should use virtual computed columns to prove partial index implication.onNo- optimizer_push_limit_into_project_filtered_scanControls whether the optimizer should push limit expressions into projects of filtered scans.onNo- optimizer_push_offset_into_index_joinControls whether the optimizer should push offset expressions into index joins.onNo- +optimizer_span_limitSets the maximum number of constraint spans allowed in a scan during query optimization. 0 means no limit.0No- optimizer_use_conditional_hoist_fixPrevents the optimizer from hoisting volatile expressions that are conditionally executed by CASE, COALESCE, or IFERR expressions.onNo- optimizer_use_delete_range_fast_pathControls whether the optimizer uses the fast path for DELETE operations using range deletions.onNo- optimizer_use_exists_filter_hoist_ruleControls whether the optimizer hoists filters out of EXISTS subqueries.onNo- diff --git a/pkg/sql/logictest/testdata/logic_test/information_schema b/pkg/sql/logictest/testdata/logic_test/information_schema index 430f741a82b5..eff20facc343 100644 --- a/pkg/sql/logictest/testdata/logic_test/information_schema +++ b/pkg/sql/logictest/testdata/logic_test/information_schema @@ -3914,6 +3914,7 @@ optimizer_prefer_bounded_cardinality on optimizer_prove_implication_with_virtual_computed_columns on optimizer_push_limit_into_project_filtered_scan on optimizer_push_offset_into_index_join on +optimizer_span_limit 0 optimizer_use_conditional_hoist_fix on optimizer_use_delete_range_fast_path on optimizer_use_exists_filter_hoist_rule on diff --git a/pkg/sql/logictest/testdata/logic_test/pg_catalog b/pkg/sql/logictest/testdata/logic_test/pg_catalog index b7242e289b08..8ae79e91a064 100644 --- a/pkg/sql/logictest/testdata/logic_test/pg_catalog +++ b/pkg/sql/logictest/testdata/logic_test/pg_catalog @@ -3155,6 +3155,7 @@ optimizer_prefer_bounded_cardinality on optimizer_prove_implication_with_virtual_computed_columns on NULL NULL NULL string optimizer_push_limit_into_project_filtered_scan on NULL NULL NULL string optimizer_push_offset_into_index_join on NULL NULL NULL string +optimizer_span_limit 0 NULL NULL NULL string optimizer_use_conditional_hoist_fix on NULL NULL NULL string optimizer_use_delete_range_fast_path on NULL NULL NULL string optimizer_use_exists_filter_hoist_rule on NULL NULL NULL string @@ -3413,6 +3414,7 @@ optimizer_prefer_bounded_cardinality on optimizer_prove_implication_with_virtual_computed_columns on NULL user NULL on on optimizer_push_limit_into_project_filtered_scan on NULL user NULL on on optimizer_push_offset_into_index_join on NULL user NULL on on +optimizer_span_limit 0 NULL user NULL 0 0 optimizer_use_conditional_hoist_fix on NULL user NULL on on optimizer_use_delete_range_fast_path on NULL user NULL on on optimizer_use_exists_filter_hoist_rule on NULL user NULL on on @@ -3662,6 +3664,7 @@ optimizer_prefer_bounded_cardinality NULL NULL optimizer_prove_implication_with_virtual_computed_columns NULL NULL NULL NULL NULL optimizer_push_limit_into_project_filtered_scan NULL NULL NULL NULL NULL optimizer_push_offset_into_index_join NULL NULL NULL NULL NULL +optimizer_span_limit NULL NULL NULL NULL NULL optimizer_use_conditional_hoist_fix NULL NULL NULL NULL NULL optimizer_use_delete_range_fast_path NULL NULL NULL NULL NULL optimizer_use_exists_filter_hoist_rule NULL NULL NULL NULL NULL diff --git a/pkg/sql/logictest/testdata/logic_test/select_index b/pkg/sql/logictest/testdata/logic_test/select_index index 94843c33c648..fb57fceef92f 100644 --- a/pkg/sql/logictest/testdata/logic_test/select_index +++ b/pkg/sql/logictest/testdata/logic_test/select_index @@ -1,4 +1,3 @@ - statement ok CREATE TABLE t ( a INT PRIMARY KEY, @@ -721,3 +720,58 @@ query I SELECT DISTINCT _int FROM t88110 WHERE NOT _bool; ---- NULL + +subtest optimizer_span_limit + +# Correctness tests for the optimizer_span_limit session setting. + +statement ok +CREATE TABLE t167620 (a INT, b INT, c INT, INDEX (a, b, c)) + +statement ok +INSERT INTO t167620 VALUES + (1, 2, 7), (1, 4, 9), (3, 6, 11), + (5, 2, 9), (3, 4, 7), (1, 6, 11), + (1, 2, 99), (1, 3, 7), (2, 2, 7) + +# Verify that results are the same regardless of span limit. +query III rowsort +SELECT * FROM t167620@t167620_a_b_c_idx WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +1 2 7 +1 4 9 +1 6 11 +3 4 7 +3 6 11 +5 2 9 + +statement ok +SET optimizer_span_limit = 10 + +query III rowsort +SELECT * FROM t167620@t167620_a_b_c_idx WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +1 2 7 +1 4 9 +1 6 11 +3 4 7 +3 6 11 +5 2 9 + +statement ok +SET optimizer_span_limit = 8 + +query III rowsort +SELECT * FROM t167620@t167620_a_b_c_idx WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +1 2 7 +1 4 9 +1 6 11 +3 4 7 +3 6 11 +5 2 9 + +statement ok +RESET optimizer_span_limit + +subtest end diff --git a/pkg/sql/logictest/testdata/logic_test/show_source b/pkg/sql/logictest/testdata/logic_test/show_source index fde38f1acb12..78b05afe8b4e 100644 --- a/pkg/sql/logictest/testdata/logic_test/show_source +++ b/pkg/sql/logictest/testdata/logic_test/show_source @@ -168,6 +168,7 @@ optimizer_prefer_bounded_cardinality on optimizer_prove_implication_with_virtual_computed_columns on Controls whether the optimizer should use virtual computed columns to prove partial index implication. optimizer_push_limit_into_project_filtered_scan on Controls whether the optimizer should push limit expressions into projects of filtered scans. optimizer_push_offset_into_index_join on Controls whether the optimizer should push offset expressions into index joins. +optimizer_span_limit 0 Sets the maximum number of constraint spans allowed in a scan during query optimization. 0 means no limit. optimizer_use_conditional_hoist_fix on Prevents the optimizer from hoisting volatile expressions that are conditionally executed by CASE, COALESCE, or IFERR expressions. optimizer_use_delete_range_fast_path on Controls whether the optimizer uses the fast path for DELETE operations using range deletions. optimizer_use_exists_filter_hoist_rule on Controls whether the optimizer hoists filters out of EXISTS subqueries. diff --git a/pkg/sql/opt/exec/execbuilder/testdata/select_index b/pkg/sql/opt/exec/execbuilder/testdata/select_index index 91dd592e3c5a..ab9218b6c131 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/select_index +++ b/pkg/sql/opt/exec/execbuilder/testdata/select_index @@ -1925,3 +1925,100 @@ distribution: local missing stats table: t5@t5_a_idx spans: [/'A.B.C' - /'A.B.C-') + +subtest optimizer_span_limit + +# Tests for the optimizer_span_limit session setting, which limits the number of +# index constraint spans generated during optimization. When the cross-product +# of spans across index columns would exceed the limit, the optimizer stops +# extending spans and adds remaining filters instead. + +statement ok +CREATE TABLE t167620 (a INT, b INT, c INT, INDEX (a, b, c)); +INSERT INTO t167620 VALUES + (1, 2, 7), (1, 4, 9), (3, 6, 11), + (5, 2, 9), (3, 4, 7), (1, 6, 11), + (1, 2, 99), (1, 3, 7), (2, 2, 7) + +# No limit (default): 27 spans for the full 3x3x3 cross-product. +query T +EXPLAIN (OPT) SELECT * FROM t167620@t167620_a_b_c_idx WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +scan t167620@t167620_a_b_c_idx + ├── constraint: /1/2/3/4 + │ ├── [/1/2/7 - /1/2/7] + │ ├── [/1/2/9 - /1/2/9] + │ ├── [/1/2/11 - /1/2/11] + │ ├── [/1/4/7 - /1/4/7] + │ ├── [/1/4/9 - /1/4/9] + │ ├── [/1/4/11 - /1/4/11] + │ ├── [/1/6/7 - /1/6/7] + │ ├── [/1/6/9 - /1/6/9] + │ ├── [/1/6/11 - /1/6/11] + │ ├── [/3/2/7 - /3/2/7] + │ ├── [/3/2/9 - /3/2/9] + │ ├── [/3/2/11 - /3/2/11] + │ ├── [/3/4/7 - /3/4/7] + │ ├── [/3/4/9 - /3/4/9] + │ ├── [/3/4/11 - /3/4/11] + │ ├── [/3/6/7 - /3/6/7] + │ ├── [/3/6/9 - /3/6/9] + │ ├── [/3/6/11 - /3/6/11] + │ ├── [/5/2/7 - /5/2/7] + │ ├── [/5/2/9 - /5/2/9] + │ ├── [/5/2/11 - /5/2/11] + │ ├── [/5/4/7 - /5/4/7] + │ ├── [/5/4/9 - /5/4/9] + │ ├── [/5/4/11 - /5/4/11] + │ ├── [/5/6/7 - /5/6/7] + │ ├── [/5/6/9 - /5/6/9] + │ └── [/5/6/11 - /5/6/11] + └── flags: force-index=t167620_a_b_c_idx + +# With limit=10, the final 9*3 cross-product is blocked. We get 9 (a, b) spans +# with a remaining filter on c. +statement ok +SET optimizer_span_limit = 10 + +query T +EXPLAIN (OPT) SELECT * FROM t167620@t167620_a_b_c_idx WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +select + ├── scan t167620@t167620_a_b_c_idx + │ ├── constraint: /1/2/3/4 + │ │ ├── [/1/2 - /1/2] + │ │ ├── [/1/4 - /1/4] + │ │ ├── [/1/6 - /1/6] + │ │ ├── [/3/2 - /3/2] + │ │ ├── [/3/4 - /3/4] + │ │ ├── [/3/6 - /3/6] + │ │ ├── [/5/2 - /5/2] + │ │ ├── [/5/4 - /5/4] + │ │ └── [/5/6 - /5/6] + │ └── flags: force-index=t167620_a_b_c_idx + └── filters + └── c IN (7, 9, 11) + +# With limit=8, the 3*3 cross-product for b is also blocked. We get 3 (a) spans +# with remaining filters on b and c. +statement ok +SET optimizer_span_limit = 8 + +query T +EXPLAIN (OPT) SELECT * FROM t167620@t167620_a_b_c_idx WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +select + ├── scan t167620@t167620_a_b_c_idx + │ ├── constraint: /1/2/3/4 + │ │ ├── [/1 - /1] + │ │ ├── [/3 - /3] + │ │ └── [/5 - /5] + │ └── flags: force-index=t167620_a_b_c_idx + └── filters + ├── b IN (2, 4, 6) + └── c IN (7, 9, 11) + +statement ok +RESET optimizer_span_limit + +subtest end diff --git a/pkg/sql/opt/idxconstraint/BUILD.bazel b/pkg/sql/opt/idxconstraint/BUILD.bazel index 7744294c93d0..f2f83ad9d03d 100644 --- a/pkg/sql/opt/idxconstraint/BUILD.bazel +++ b/pkg/sql/opt/idxconstraint/BUILD.bazel @@ -16,6 +16,7 @@ go_library( "//pkg/sql/sem/tree", "//pkg/sql/types", "//pkg/util", + "//pkg/util/log", "//pkg/util/ltree", "@com_github_cockroachdb_errors//:errors", ], diff --git a/pkg/sql/opt/idxconstraint/index_constraints.go b/pkg/sql/opt/idxconstraint/index_constraints.go index 782ffefd0b62..671f110d9643 100644 --- a/pkg/sql/opt/idxconstraint/index_constraints.go +++ b/pkg/sql/opt/idxconstraint/index_constraints.go @@ -20,6 +20,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util" + "github.com/cockroachdb/cockroach/pkg/util/log" "github.com/cockroachdb/cockroach/pkg/util/ltree" "github.com/cockroachdb/errors" ) @@ -145,6 +146,16 @@ func (c *indexConstraintCtx) makeSpansForSingleColumn( ) (tight bool) { if op == opt.InOp && memo.CanExtractConstTuple(val) { tupVal := val.(*memo.TupleExpr) + // If the IN list has more elements than the span limit, return + // unconstrained. + if c.spanLimit > 0 && len(tupVal.Elems) > c.spanLimit { + log.VEventf(c.ctx, 2, + "not building spans for IN list: has %d elements, exceeds optimizer_span_limit of %d", + len(tupVal.Elems), c.spanLimit, + ) + c.unconstrained(offset, out) + return false + } keyCtx := &c.keyCtx[offset] var spans constraint.Spans spans.Alloc(len(tupVal.Elems)) @@ -608,6 +619,16 @@ func (c *indexConstraintCtx) makeSpansForTupleIn( return false } + // If the tuple has more elements than the span limit, return unconstrained. + if c.spanLimit > 0 && len(rhs.Elems) > c.spanLimit { + log.VEventf(c.ctx, 2, + "not building spans for tuple: has %d elements, exceeds optimizer_span_limit of %d", + len(rhs.Elems), c.spanLimit, + ) + c.unconstrained(offset, out) + return false + } + // Create a span for each (tuple) value inside the right-hand side tuple. keyCtx := &c.keyCtx[offset] var spans constraint.Spans @@ -891,17 +912,32 @@ func (c *indexConstraintCtx) makeSpansForAnd( break } + var tightFilters util.FastIntMap tight := c.makeSpansForExpr(offset+delta, filters[0].Condition, &ofsC) if tight { - tightDeltaMap.Set(0, delta) + tightFilters.Set(0, delta) } for j := 1; j < len(filters); j++ { tight := c.makeSpansForExpr(offset+delta, filters[j].Condition, &exprConstraint) if tight { - tightDeltaMap.Set(j, delta) + tightFilters.Set(j, delta) } ofsC.IntersectWith(c.ctx, c.evalCtx, &exprConstraint) } + // If combining could produce more spans than the limit (e.g. via + // cross-product of exact-match prefix spans with suffix spans), skip the + // suffix extension. + if c.spanLimit > 0 && out.Spans.Count()*ofsC.Spans.Count() > c.spanLimit { + log.VEventf(c.ctx, 2, + "limiting index span tightness: combining %d spans with %d suffix spans could exceed optimizer_span_limit of %d", + out.Spans.Count(), ofsC.Spans.Count(), c.spanLimit, + ) + break + } + // Record tightness after we've confirmed we'll actually combine. + tightFilters.ForEach(func(j, delta int) { + tightDeltaMap.Set(j, delta) + }) out.Combine(c.ctx, c.evalCtx, &ofsC, c.checkCancellation) numIterations++ // In case we can't exit this loop, allow the cancel checker to cancel @@ -1155,6 +1191,11 @@ type Instance struct { // they need not generate remaining filters. This is e.g. used for check // constraints that can help generate better spans but don't actually need to be // enforced. +// +// spanLimit limits the number of spans that will be generated during constraint +// building. When a span-generating operation would produce more spans than this +// limit, the constraint builder returns a looser result instead (in some cases +// fully unconstrained). A value of 0 means no limit. func (ic *Instance) Init( ctx context.Context, requiredFilters memo.FiltersExpr, @@ -1167,6 +1208,7 @@ func (ic *Instance) Init( evalCtx *eval.Context, factory *norm.Factory, ps partition.PrefixSorter, + spanLimit int, checkCancellation func(), ) { // This initialization pattern ensures that fields are not unwittingly @@ -1183,7 +1225,7 @@ func (ic *Instance) Init( ic.allFilters = requiredFilters[:len(requiredFilters):len(requiredFilters)] ic.allFilters = append(ic.allFilters, optionalFilters...) } - ic.indexConstraintCtx.init(ctx, columns, notNullCols, computedCols, colsInComputedColsExpressions, evalCtx, factory, checkCancellation) + ic.indexConstraintCtx.init(ctx, columns, notNullCols, computedCols, colsInComputedColsExpressions, evalCtx, factory, spanLimit, checkCancellation) ic.tight = ic.makeSpansForExpr(0 /* offset */, &ic.allFilters, &ic.constraint) // Note: If consolidate is true, we only consolidate spans at the @@ -1295,6 +1337,10 @@ type indexConstraintCtx struct { ctx context.Context evalCtx *eval.Context + // spanLimit limits the number of spans that will be generated during + // constraint building. A value of 0 means no limit. + spanLimit int + // We pre-initialize the KeyContext for each suffix of the index columns. keyCtx []constraint.KeyContext @@ -1311,6 +1357,7 @@ func (c *indexConstraintCtx) init( colsInComputedColsExpressions opt.ColSet, evalCtx *eval.Context, factory *norm.Factory, + spanLimit int, checkCancellation func(), ) { var keyCols, computedColSet opt.ColSet @@ -1332,6 +1379,7 @@ func (c *indexConstraintCtx) init( colsInComputedColsExpressions: colsInComputedColsExpressions, ctx: ctx, evalCtx: evalCtx, + spanLimit: spanLimit, factory: factory, keyCtx: make([]constraint.KeyContext, len(columns)), checkCancellation: checkCancellation, @@ -1422,6 +1470,8 @@ func IndexPrefixCols( // spans have the same start and end keys for all prefix columns. This is // required for building spans for scanning multi-column inverted/vector indexes // (see span.Builder.SpansFromInvertedSpans). +// +// TODO(michae2): Accept and use optimizer_span_limit. func ConstrainIndexPrefixCols( ctx context.Context, evalCtx *eval.Context, @@ -1461,7 +1511,9 @@ func ConstrainIndexPrefixCols( columns, notNullCols, tabMeta.ComputedCols, tabMeta.ColsInComputedColsExpressions, false, /* consolidate */ - evalCtx, factory, ps, checkCancellation, + evalCtx, factory, ps, + 0, /* spanLimit */ + checkCancellation, ) var c constraint.Constraint ic.UnconsolidatedConstraint(&c) diff --git a/pkg/sql/opt/idxconstraint/index_constraints_test.go b/pkg/sql/opt/idxconstraint/index_constraints_test.go index 02e4e219f629..846ead30bc13 100644 --- a/pkg/sql/opt/idxconstraint/index_constraints_test.go +++ b/pkg/sql/opt/idxconstraint/index_constraints_test.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "fmt" + "strconv" "strings" "testing" @@ -53,6 +54,10 @@ import ( // - semtree-normalize // // Run TypedExpr normalization before building the memo. +// +// - span-limit= +// +// Set a limit on the number of spans generated. func TestIndexConstraints(t *testing.T) { defer leaktest.AfterTest(t)() @@ -64,6 +69,7 @@ func TestIndexConstraints(t *testing.T) { datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { var sv testutils.ScalarVars var indexCols []opt.OrderingColumn + var spanLimit int var err error var f norm.Factory @@ -90,6 +96,15 @@ func TestIndexConstraints(t *testing.T) { case "nonormalize": f.DisableOptimizations() + case "span-limit": + if len(vals) != 1 { + d.Fatalf(t, "span-limit requires a single value") + } + spanLimit, err = strconv.Atoi(vals[0]) + if err != nil { + d.Fatalf(t, "invalid span-limit: %v", err) + } + default: d.Fatalf(t, "unknown argument: %s", key) } @@ -133,6 +148,7 @@ func TestIndexConstraints(t *testing.T) { ctx, filters, optionalFilters, indexCols, sv.NotNullCols(), computedCols, colsInComputedColsExpressions, true /* consolidate */, &evalCtx, &f, partition.PrefixSorter{}, + spanLimit, func() {}, /* checkCancellation */ ) var result constraint.Constraint @@ -254,6 +270,7 @@ func BenchmarkIndexConstraints(b *testing.B) { nil /* computedCols */, opt.ColSet{}, /* colsInComputedColsExpressions */ true, /* consolidate */ &evalCtx, &f, partition.PrefixSorter{}, + 0, /* spanLimit */ func() {}, /* checkCancellation */ ) var result constraint.Constraint diff --git a/pkg/sql/opt/idxconstraint/testdata/span-limit b/pkg/sql/opt/idxconstraint/testdata/span-limit new file mode 100644 index 000000000000..6d842380f939 --- /dev/null +++ b/pkg/sql/opt/idxconstraint/testdata/span-limit @@ -0,0 +1,158 @@ +# Tests for the span-limit argument, which limits the number of spans generated +# by the index constraint code. When the cross-product of spans across columns +# would exceed the limit, the suffix extension loop stops early, producing wider +# (less precise) spans with a remaining filter. + +# 3x3x3 = 27 spans with no limit. +index-constraints vars=(a int, b int, c int) index=(a, b, c) +a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +[/1/2/7 - /1/2/7] +[/1/2/9 - /1/2/9] +[/1/2/11 - /1/2/11] +[/1/4/7 - /1/4/7] +[/1/4/9 - /1/4/9] +[/1/4/11 - /1/4/11] +[/1/6/7 - /1/6/7] +[/1/6/9 - /1/6/9] +[/1/6/11 - /1/6/11] +[/3/2/7 - /3/2/7] +[/3/2/9 - /3/2/9] +[/3/2/11 - /3/2/11] +[/3/4/7 - /3/4/7] +[/3/4/9 - /3/4/9] +[/3/4/11 - /3/4/11] +[/3/6/7 - /3/6/7] +[/3/6/9 - /3/6/9] +[/3/6/11 - /3/6/11] +[/5/2/7 - /5/2/7] +[/5/2/9 - /5/2/9] +[/5/2/11 - /5/2/11] +[/5/4/7 - /5/4/7] +[/5/4/9 - /5/4/9] +[/5/4/11 - /5/4/11] +[/5/6/7 - /5/6/7] +[/5/6/9 - /5/6/9] +[/5/6/11 - /5/6/11] + +# With span-limit=27, the 27-span cross-product is still allowed. +index-constraints vars=(a int, b int, c int) index=(a, b, c) span-limit=27 +a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +[/1/2/7 - /1/2/7] +[/1/2/9 - /1/2/9] +[/1/2/11 - /1/2/11] +[/1/4/7 - /1/4/7] +[/1/4/9 - /1/4/9] +[/1/4/11 - /1/4/11] +[/1/6/7 - /1/6/7] +[/1/6/9 - /1/6/9] +[/1/6/11 - /1/6/11] +[/3/2/7 - /3/2/7] +[/3/2/9 - /3/2/9] +[/3/2/11 - /3/2/11] +[/3/4/7 - /3/4/7] +[/3/4/9 - /3/4/9] +[/3/4/11 - /3/4/11] +[/3/6/7 - /3/6/7] +[/3/6/9 - /3/6/9] +[/3/6/11 - /3/6/11] +[/5/2/7 - /5/2/7] +[/5/2/9 - /5/2/9] +[/5/2/11 - /5/2/11] +[/5/4/7 - /5/4/7] +[/5/4/9 - /5/4/9] +[/5/4/11 - /5/4/11] +[/5/6/7 - /5/6/7] +[/5/6/9 - /5/6/9] +[/5/6/11 - /5/6/11] + +# With span-limit=26, the final 9*3 cross-product is blocked. We get 9 spans +# for (a, b) with a remaining filter on c. +index-constraints vars=(a int, b int, c int) index=(a, b, c) span-limit=26 +a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +[/1/2 - /1/2] +[/1/4 - /1/4] +[/1/6 - /1/6] +[/3/2 - /3/2] +[/3/4 - /3/4] +[/3/6 - /3/6] +[/5/2 - /5/2] +[/5/4 - /5/4] +[/5/6 - /5/6] +Remaining filter: c IN (7, 9, 11) + +# With span-limit=9, the 9-span (a, b) cross-product is still allowed. +index-constraints vars=(a int, b int, c int) index=(a, b, c) span-limit=9 +a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +[/1/2 - /1/2] +[/1/4 - /1/4] +[/1/6 - /1/6] +[/3/2 - /3/2] +[/3/4 - /3/4] +[/3/6 - /3/6] +[/5/2 - /5/2] +[/5/4 - /5/4] +[/5/6 - /5/6] +Remaining filter: c IN (7, 9, 11) + +# With span-limit=8, even the 3*3 cross-product for (a, b) is blocked. We get +# just 3 spans for (a) with remaining filters on b and c. +index-constraints vars=(a int, b int, c int) index=(a, b, c) span-limit=8 +a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +[/1 - /1] +[/3 - /3] +[/5 - /5] +Remaining filter: (b IN (2, 4, 6)) AND (c IN (7, 9, 11)) + +# With span-limit=3, the initial 3 spans for (a) are still within the limit. +index-constraints vars=(a int, b int, c int) index=(a, b, c) span-limit=3 +a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +[/1 - /1] +[/3 - /3] +[/5 - /5] +Remaining filter: (b IN (2, 4, 6)) AND (c IN (7, 9, 11)) + +# With span-limit=1, the limit also applies to the initial IN list expansion in +# makeSpansForSingleColumn, so we get an unconstrained span. +index-constraints vars=(a int, b int, c int) index=(a, b, c) span-limit=1 +a IN (1, 3, 5) AND b IN (2, 4, 6) AND c IN (7, 9, 11) +---- +[ - ] +Remaining filter: ((a IN (1, 3, 5)) AND (b IN (2, 4, 6))) AND (c IN (7, 9, 11)) + +# Two-column case: 3x3 = 9 spans with no limit. +index-constraints vars=(a int, b int) index=(a, b) +a IN (1, 3, 5) AND b IN (2, 4, 6) +---- +[/1/2 - /1/2] +[/1/4 - /1/4] +[/1/6 - /1/6] +[/3/2 - /3/2] +[/3/4 - /3/4] +[/3/6 - /3/6] +[/5/2 - /5/2] +[/5/4 - /5/4] +[/5/6 - /5/6] + +# Two-column case with span-limit=5: cross-product blocked. +index-constraints vars=(a int, b int) index=(a, b) span-limit=5 +a IN (1, 3, 5) AND b IN (2, 4, 6) +---- +[/1 - /1] +[/3 - /3] +[/5 - /5] +Remaining filter: b IN (2, 4, 6) + +# Range filter on second column: span limit doesn't affect range tightening +# since Combine doesn't multiply for range spans. +index-constraints vars=(a int, b int) index=(a, b) span-limit=5 +a IN (1, 3, 5) AND b > 2 AND b < 6 +---- +[/1/3 - /1/5] +[/3/3 - /3/5] +[/5/3 - /5/5] diff --git a/pkg/sql/opt/invertedidx/inverted_index_expr.go b/pkg/sql/opt/invertedidx/inverted_index_expr.go index 4263850cc77b..fd6b92b34fff 100644 --- a/pkg/sql/opt/invertedidx/inverted_index_expr.go +++ b/pkg/sql/opt/invertedidx/inverted_index_expr.go @@ -71,6 +71,9 @@ func NewBoundPreFilterer(typ *types.T, expr tree.TypedExpr) (*PreFilterer, inter // and // - pre-filterer state that can be used by the invertedFilterer operator to // reduce the number of false positives returned by the span expression. +// +// TODO(michae2): Pass optimizer_span_limit to constrain prefix columns of +// multi-column inverted indexes. func TryFilterInvertedIndex( ctx context.Context, evalCtx *eval.Context, diff --git a/pkg/sql/opt/memo/memo.go b/pkg/sql/opt/memo/memo.go index d790683dbaa2..d9e1239838e0 100644 --- a/pkg/sql/opt/memo/memo.go +++ b/pkg/sql/opt/memo/memo.go @@ -193,6 +193,7 @@ type Memo struct { useImprovedTrigramSimilaritySelectivity bool trigramSimilarityThreshold float64 splitScanLimit int32 + spanLimit int32 useImprovedZigzagJoinCosting bool useImprovedMultiColumnSelectivityEstimate bool proveImplicationWithVirtualComputedCols bool @@ -340,6 +341,7 @@ func (m *Memo) Init(ctx context.Context, evalCtx *eval.Context) { useImprovedTrigramSimilaritySelectivity: evalCtx.SessionData().OptimizerUseImprovedTrigramSimilaritySelectivity, trigramSimilarityThreshold: evalCtx.SessionData().TrigramSimilarityThreshold, splitScanLimit: evalCtx.SessionData().OptSplitScanLimit, + spanLimit: evalCtx.SessionData().OptimizerSpanLimit, useImprovedZigzagJoinCosting: evalCtx.SessionData().OptimizerUseImprovedZigzagJoinCosting, useImprovedMultiColumnSelectivityEstimate: evalCtx.SessionData().OptimizerUseImprovedMultiColumnSelectivityEstimate, proveImplicationWithVirtualComputedCols: evalCtx.SessionData().OptimizerProveImplicationWithVirtualComputedColumns, @@ -528,6 +530,7 @@ func (m *Memo) IsStale( m.useImprovedTrigramSimilaritySelectivity != evalCtx.SessionData().OptimizerUseImprovedTrigramSimilaritySelectivity || m.trigramSimilarityThreshold != evalCtx.SessionData().TrigramSimilarityThreshold || m.splitScanLimit != evalCtx.SessionData().OptSplitScanLimit || + m.spanLimit != evalCtx.SessionData().OptimizerSpanLimit || m.useImprovedZigzagJoinCosting != evalCtx.SessionData().OptimizerUseImprovedZigzagJoinCosting || m.useImprovedMultiColumnSelectivityEstimate != evalCtx.SessionData().OptimizerUseImprovedMultiColumnSelectivityEstimate || m.proveImplicationWithVirtualComputedCols != evalCtx.SessionData().OptimizerProveImplicationWithVirtualComputedColumns || diff --git a/pkg/sql/opt/memo/memo_test.go b/pkg/sql/opt/memo/memo_test.go index 0693179f1f12..bb3f662e39cc 100644 --- a/pkg/sql/opt/memo/memo_test.go +++ b/pkg/sql/opt/memo/memo_test.go @@ -495,6 +495,12 @@ func TestMemoIsStale(t *testing.T) { evalCtx.SessionData().OptSplitScanLimit = 0 notStale() + // Stale optimizer_span_limit. + evalCtx.SessionData().OptimizerSpanLimit = 100 + stale() + evalCtx.SessionData().OptimizerSpanLimit = 0 + notStale() + // Stale optimizer_use_improved_zigzag_join_costing. evalCtx.SessionData().OptimizerUseImprovedZigzagJoinCosting = true stale() diff --git a/pkg/sql/opt/optbuilder/misc_statements.go b/pkg/sql/opt/optbuilder/misc_statements.go index 93cb54b89d01..1021d33f59e5 100644 --- a/pkg/sql/opt/optbuilder/misc_statements.go +++ b/pkg/sql/opt/optbuilder/misc_statements.go @@ -325,7 +325,9 @@ func (b *Builder) buildWhereForStatistics( ic.Init( b.ctx, fe, nil /* optionalFilters */, indexCols, notNullCols, computedCols, colsInComputedColsExpressions, true, /* consolidate */ - b.evalCtx, b.factory, ps, nil, /* checkCancellation */ + b.evalCtx, b.factory, ps, + 0, /* spanLimit */ + nil, /* checkCancellation */ ) var cons constraint.Constraint ic.Constraint(&cons) diff --git a/pkg/sql/opt/xform/general_funcs.go b/pkg/sql/opt/xform/general_funcs.go index 305120c5ee28..435ad0f41563 100644 --- a/pkg/sql/opt/xform/general_funcs.go +++ b/pkg/sql/opt/xform/general_funcs.go @@ -168,7 +168,7 @@ func (c *CustomFuncs) checkConstraintFilters(tabID opt.TableID) memo.FiltersExpr } func (c *CustomFuncs) initIdxConstraintForIndex( - requiredFilters, optionalFilters memo.FiltersExpr, tabID opt.TableID, indexOrd int, + requiredFilters, optionalFilters memo.FiltersExpr, tabID opt.TableID, indexOrd int, spanLimit int, ) (ic *idxconstraint.Instance) { ic = &idxconstraint.Instance{} @@ -200,6 +200,7 @@ func (c *CustomFuncs) initIdxConstraintForIndex( columns, notNullCols, tabMeta.ComputedCols, tabMeta.ColsInComputedColsExpressions, true /* consolidate */, c.e.evalCtx, c.e.f, ps, + spanLimit, c.checkCancellation, ) return ic @@ -266,6 +267,8 @@ func (c *CustomFuncs) splitScanIntoUnionScansOrSelects( // This is configurable using a session setting opt_split_scan_limit. // If OptSplitScanLimit is below maxScanCount, we will decrease maxScanCount // to that value because a hard limit should never be lower than a soft limit. + // + // TODO(michae2): Consider also bounding this by optimizer_span_limit. hardMaxScanCount := int(c.e.evalCtx.SessionData().OptSplitScanLimit) if hardMaxScanCount < maxScanCount { maxScanCount = hardMaxScanCount @@ -671,11 +674,13 @@ func (c *CustomFuncs) getKnownScanConstraint( // Build a constraint set with the check constraints of the underlying // table. filters := c.checkConstraintFilters(sp.Table) + spanLimit := int(c.e.evalCtx.SessionData().OptimizerSpanLimit) instance := c.initIdxConstraintForIndex( nil, /* requiredFilters */ filters, sp.Table, sp.Index, + spanLimit, ) var cons constraint.Constraint instance.Constraint(&cons) diff --git a/pkg/sql/opt/xform/scan_funcs.go b/pkg/sql/opt/xform/scan_funcs.go index e81a8235f6a2..35cb2a15c2ab 100644 --- a/pkg/sql/opt/xform/scan_funcs.go +++ b/pkg/sql/opt/xform/scan_funcs.go @@ -136,6 +136,7 @@ func (c *CustomFuncs) CanMaybeGenerateLocalityOptimizedScan(scanPrivate *memo.Sc // Don't apply the rule if there are too many spans, since the rule code is // O(# spans * # prefixes * # datums per prefix). + // TODO(michae2): Consider also bounding this by optimizer_span_limit. if scanPrivate.Constraint.Spans.Count() > 10000 { return false } @@ -428,9 +429,13 @@ func (c *CustomFuncs) buildAllPartitionsConstraint( optionalFilters, filterColumns := c.GetOptionalFiltersAndFilterColumns(nil /* explicitFilters */, sp) + // TODO(michae2): We should probably be passing optimizer_span_limit to + // MakeCombinedFiltersConstraint, but it doesn't seem too dangerous to assume + // the number of partitions will be reasonable. if _, remainingFilters, combinedConstraint, ok = c.MakeCombinedFiltersConstraint( tabMeta, index, sp, ps, nil /* explicitFilters */, optionalFilters, filterColumns, + 0, /* spanLimit */ ); !ok { return nil, false } diff --git a/pkg/sql/opt/xform/select_funcs.go b/pkg/sql/opt/xform/select_funcs.go index 94e84e6369cf..6940e41fa112 100644 --- a/pkg/sql/opt/xform/select_funcs.go +++ b/pkg/sql/opt/xform/select_funcs.go @@ -183,6 +183,7 @@ func (c *CustomFuncs) MakeCombinedFiltersConstraint( explicitFilters memo.FiltersExpr, optionalFilters memo.FiltersExpr, filterColumns opt.ColSet, + spanLimit int, ) ( partitionFilters memo.FiltersExpr, remainingFilters memo.FiltersExpr, @@ -298,6 +299,7 @@ func (c *CustomFuncs) MakeCombinedFiltersConstraint( append(optionalFilters, partitionFilters...), scanPrivate.Table, index.Ordinal(), + spanLimit, ) if !ok { return nil, nil, nil, false @@ -309,6 +311,7 @@ func (c *CustomFuncs) MakeCombinedFiltersConstraint( append(optionalFilters, inBetweenFilters...), scanPrivate.Table, index.Ordinal(), + spanLimit, ) if !ok { // If there are multiple partitioning columns on the index with different @@ -444,11 +447,14 @@ func (c *CustomFuncs) GenerateConstrainedScans( // local to the gateway region. prefixSorter := tabMeta.IndexPartitionLocality(index.Ordinal()) + spanLimit := int(c.e.evalCtx.SessionData().OptimizerSpanLimit) + // Build Constraints to scan a subset of the table Spans. if partitionFilters, remainingFilters, combinedConstraint, ok = c.MakeCombinedFiltersConstraint( tabMeta, index, scanPrivate, prefixSorter, filters, optionalFilters, filterColumns, + spanLimit, ); !ok { return } @@ -1201,7 +1207,7 @@ func (c *CustomFuncs) GenerateTrigramSimilarityInvertedIndexScans( // filter remaining after extracting the constraint. If no constraint can be // derived, then tryConstrainIndex returns ok = false. func (c *CustomFuncs) tryConstrainIndex( - requiredFilters, optionalFilters memo.FiltersExpr, tabID opt.TableID, indexOrd int, + requiredFilters, optionalFilters memo.FiltersExpr, tabID opt.TableID, indexOrd int, spanLimit int, ) (_ *constraint.Constraint, remainingFilters memo.FiltersExpr, ok bool) { // Start with fast check to rule out indexes that cannot be constrained. if !c.canMaybeConstrainNonInvertedIndex(requiredFilters, tabID, indexOrd) && @@ -1209,7 +1215,7 @@ func (c *CustomFuncs) tryConstrainIndex( return nil, nil, false } - ic := c.initIdxConstraintForIndex(requiredFilters, optionalFilters, tabID, indexOrd) + ic := c.initIdxConstraintForIndex(requiredFilters, optionalFilters, tabID, indexOrd, spanLimit) var cons constraint.Constraint ic.Constraint(&cons) if cons.IsUnconstrained() { diff --git a/pkg/sql/opt/xform/testdata/rules/select b/pkg/sql/opt/xform/testdata/rules/select index a49be96e8e67..9bec32c88674 100644 --- a/pkg/sql/opt/xform/testdata/rules/select +++ b/pkg/sql/opt/xform/testdata/rules/select @@ -14386,3 +14386,132 @@ project ├── [???, "a"/"b") ├── ["a"/"b"/PrefixEnd, "a"/"b"/Arr) └── ["a"/"b"/Arr/PrefixEnd, "a"/Arr/) + +# -------------------------------------------------- +# optimizer_span_limit +# -------------------------------------------------- + +exec-ddl +CREATE TABLE span_limit ( + k INT PRIMARY KEY, + a INT, + b INT, + c INT, + INDEX ab (a, b), + INDEX abc (a, b, c) +) +---- + +# With optimizer_span_limit=0 (the default, meaning no limit), +# GenerateConstrainedScans should fire normally for IN lists. +# Using non-contiguous values on both columns generates 3 * 3 = 9 individual +# spans since they cannot be consolidated into ranges. +opt expect=GenerateConstrainedScans +SELECT k FROM span_limit WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) +---- +project + ├── columns: k:1!null + ├── key: (1) + └── scan span_limit@ab + ├── columns: k:1!null a:2!null b:3!null + ├── constraint: /2/3/1 + │ ├── [/1/2 - /1/2] + │ ├── [/1/4 - /1/4] + │ ├── [/1/6 - /1/6] + │ ├── [/3/2 - /3/2] + │ ├── [/3/4 - /3/4] + │ ├── [/3/6 - /3/6] + │ ├── [/5/2 - /5/2] + │ ├── [/5/4 - /5/4] + │ └── [/5/6 - /5/6] + ├── key: (1) + └── fd: (1)-->(2,3) + +# Setting optimizer_span_limit=10 still allows 9 spans. +opt expect=GenerateConstrainedScans set=(optimizer_span_limit=10) +SELECT k FROM span_limit WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) +---- +project + ├── columns: k:1!null + ├── key: (1) + └── scan span_limit@ab + ├── columns: k:1!null a:2!null b:3!null + ├── constraint: /2/3/1 + │ ├── [/1/2 - /1/2] + │ ├── [/1/4 - /1/4] + │ ├── [/1/6 - /1/6] + │ ├── [/3/2 - /3/2] + │ ├── [/3/4 - /3/4] + │ ├── [/3/6 - /3/6] + │ ├── [/5/2 - /5/2] + │ ├── [/5/4 - /5/4] + │ └── [/5/6 - /5/6] + ├── key: (1) + └── fd: (1)-->(2,3) + +# Setting optimizer_span_limit=8 would produce 3*3=9 spans without the limit. +# The cross-product is skipped during span building, so we get a partial +# constraint on just column a (3 spans). Column b is applied as a remaining +# filter after the scan. +opt expect=GenerateConstrainedScans set=(optimizer_span_limit=8) +SELECT k FROM span_limit WHERE a IN (1, 3, 5) AND b IN (2, 4, 6) +---- +project + ├── columns: k:1!null + ├── key: (1) + └── select + ├── columns: k:1!null a:2!null b:3!null + ├── key: (1) + ├── fd: (1)-->(2,3) + ├── scan span_limit@ab + │ ├── columns: k:1!null a:2!null b:3 + │ ├── constraint: /2/3/1 + │ │ ├── [/1 - /1] + │ │ ├── [/3 - /3] + │ │ └── [/5 - /5] + │ ├── key: (1) + │ └── fd: (1)-->(2,3) + └── filters + └── b:3 IN (2, 4, 6) [outer=(3), constraints=(/3: [/2 - /2] [/4 - /4] [/6 - /6]; tight)] + +# 4 non-contiguous values on each column would produce 4*4=16 spans. +# With optimizer_span_limit=10, the cross-product is skipped, and we get a +# partial constraint on just column a (4 spans). +opt expect=GenerateConstrainedScans set=(optimizer_span_limit=10) +SELECT k FROM span_limit WHERE a IN (1, 3, 5, 7) AND b IN (2, 4, 6, 8) +---- +project + ├── columns: k:1!null + ├── key: (1) + └── select + ├── columns: k:1!null a:2!null b:3!null + ├── key: (1) + ├── fd: (1)-->(2,3) + ├── scan span_limit@ab + │ ├── columns: k:1!null a:2!null b:3 + │ ├── constraint: /2/3/1 + │ │ ├── [/1 - /1] + │ │ ├── [/3 - /3] + │ │ ├── [/5 - /5] + │ │ └── [/7 - /7] + │ ├── key: (1) + │ └── fd: (1)-->(2,3) + └── filters + └── b:3 IN (2, 4, 6, 8) [outer=(3), constraints=(/3: [/2 - /2] [/4 - /4] [/6 - /6] [/8 - /8]; tight)] + +# A filter on just one column produces fewer spans (3 spans for 3 values). +# With optimizer_span_limit=5, the constrained scan is within the limit. +opt expect=GenerateConstrainedScans set=(optimizer_span_limit=5) +SELECT k FROM span_limit WHERE a IN (1, 3, 5) +---- +project + ├── columns: k:1!null + ├── key: (1) + └── scan span_limit@ab + ├── columns: k:1!null a:2!null + ├── constraint: /2/3/1 + │ ├── [/1 - /1] + │ ├── [/3 - /3] + │ └── [/5 - /5] + ├── key: (1) + └── fd: (1)-->(2) diff --git a/pkg/sql/session_var_descriptions.go b/pkg/sql/session_var_descriptions.go index 0112e3e20b3b..ff68ec85905b 100644 --- a/pkg/sql/session_var_descriptions.go +++ b/pkg/sql/session_var_descriptions.go @@ -135,6 +135,7 @@ var sessionVarDescriptions = map[string]string{ "null_ordered_last": "Controls whether NULL values are ordered last. When true, NULL values appear after non-NULL values in ordered results.", "on_update_rehome_row_enabled": "Controls whether the ON UPDATE rehome_row() will actually trigger on row updates.", "opt_split_scan_limit": "Sets the maximum number of UNION ALL statements a Scan may be split into during query optimization to avoid a sort.", + "optimizer_span_limit": "Sets the maximum number of constraint spans allowed in a scan during query optimization. 0 means no limit.", "optimizer": "Controls whether the cost-based optimizer is enabled.", "optimizer_always_use_histograms": "Ensures that the optimizer always uses histograms to calculate statistics if available.", "optimizer_check_input_min_row_count": "Sets a lower bound on row count estimates for the buffer scan of foreign key and uniqueness checks.", diff --git a/pkg/sql/sessiondatapb/local_only_session_data.proto b/pkg/sql/sessiondatapb/local_only_session_data.proto index 0cb0ac009f47..f04e5f7f6a9a 100644 --- a/pkg/sql/sessiondatapb/local_only_session_data.proto +++ b/pkg/sql/sessiondatapb/local_only_session_data.proto @@ -807,6 +807,9 @@ message LocalOnlySessionData { // BufferedWritesImplicitTxnsEnabled, if set, will make it so that the // buffered writes feature is used for implicit txns. bool buffered_writes_implicit_txns_enabled = 205; + // OptimizerSpanLimit sets the maximum number of constraint spans allowed in + // a scan during query optimization. 0 means no limit. + int32 optimizer_span_limit = 207; /////////////////////////////////////////////////////////////////////////// // WARNING: consider whether a session parameter you're adding needs to // diff --git a/pkg/sql/sessionmutator/mutator.go b/pkg/sql/sessionmutator/mutator.go index e190111e8d49..f20036b9afd0 100644 --- a/pkg/sql/sessionmutator/mutator.go +++ b/pkg/sql/sessionmutator/mutator.go @@ -616,6 +616,10 @@ func (m *SessionDataMutator) SetOptSplitScanLimit(val int32) { m.Data.OptSplitScanLimit = val } +func (m *SessionDataMutator) SetOptimizerSpanLimit(val int32) { + m.Data.OptimizerSpanLimit = val +} + func (m *SessionDataMutator) SetStreamReplicationEnabled(val bool) { m.Data.EnableStreamReplication = val } diff --git a/pkg/sql/vars.go b/pkg/sql/vars.go index a24a16022ccf..7d8dc854bdff 100644 --- a/pkg/sql/vars.go +++ b/pkg/sql/vars.go @@ -2964,6 +2964,35 @@ var varGen = map[string]sessionVar{ }, }, + // CockroachDB extension. + `optimizer_span_limit`: { + Description: sessionVarDescriptions["optimizer_span_limit"], + GetStringVal: makeIntGetStringValFn(`optimizer_span_limit`), + Set: func(_ context.Context, m sessionmutator.SessionDataMutator, s string) error { + b, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + if b < 0 { + return pgerror.Newf(pgcode.InvalidParameterValue, + "cannot set optimizer_span_limit to a negative value: %d", b) + } + if b > math.MaxInt32 { + return pgerror.Newf(pgcode.InvalidParameterValue, + "cannot set optimizer_span_limit to a value greater than %d", math.MaxInt32) + } + + m.SetOptimizerSpanLimit(int32(b)) + return nil + }, + Get: func(evalCtx *extendedEvalContext, _ *kv.Txn) (string, error) { + return strconv.FormatInt(int64(evalCtx.SessionData().OptimizerSpanLimit), 10), nil + }, + GlobalDefault: func(_ *settings.Values) string { + return "0" + }, + }, + // CockroachDB extension. `enable_super_regions`: makeBackwardsCompatBoolVar("enable_super_regions", true),