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_columns | Controls whether the optimizer should use virtual computed columns to prove partial index implication. | on | No | - |
optimizer_push_limit_into_project_filtered_scan | Controls whether the optimizer should push limit expressions into projects of filtered scans. | on | No | - |
optimizer_push_offset_into_index_join | Controls whether the optimizer should push offset expressions into index joins. | on | No | - |
+optimizer_span_limit | Sets the maximum number of constraint spans allowed in a scan during query optimization. 0 means no limit. | 0 | No | - |
optimizer_use_conditional_hoist_fix | Prevents the optimizer from hoisting volatile expressions that are conditionally executed by CASE, COALESCE, or IFERR expressions. | on | No | - |
optimizer_use_delete_range_fast_path | Controls whether the optimizer uses the fast path for DELETE operations using range deletions. | on | No | - |
optimizer_use_exists_filter_hoist_rule | Controls whether the optimizer hoists filters out of EXISTS subqueries. | on | No | - |
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),