From 64bad8e2f7da4569df53709ad2f777c4e27835db Mon Sep 17 00:00:00 2001
From: Marcus Gartner <marcus@cockroachlabs.com>
Date: Tue, 28 Jan 2025 16:16:49 -0500
Subject: [PATCH] opt: add `optimizer_prefer_bounded_cardinality` session
 setting

Release note (sql change): The `optimizer_prefer_bounded_cardinality`
session setting has been added which instructs the optimizer to prefer
query plans where every expression has a guaranteed upper-bound on the
number of rows it will process. This may help the optimizer produce
better query plans in some cases. This setting is disabled by default.
---
 pkg/sql/exec_util.go                          |   4 +
 .../testdata/logic_test/information_schema    |   1 +
 .../logictest/testdata/logic_test/pg_catalog  |   3 +
 .../logictest/testdata/logic_test/show_source |   1 +
 pkg/sql/opt/memo/cost.go                      |  18 +-
 pkg/sql/opt/memo/cost_test.go                 |   4 +
 pkg/sql/opt/memo/expr_format.go               |   3 +
 pkg/sql/opt/memo/memo.go                      |   3 +
 pkg/sql/opt/memo/memo_test.go                 |   5 +
 pkg/sql/opt/xform/coster.go                   |   5 +
 .../xform/testdata/coster/outside-histogram   | 484 ++++++++++++++++++
 pkg/sql/prepared_stmt.go                      |   5 +
 .../local_only_session_data.proto             |   4 +
 pkg/sql/vars.go                               |  18 +
 14 files changed, 555 insertions(+), 3 deletions(-)
 create mode 100644 pkg/sql/opt/xform/testdata/coster/outside-histogram

diff --git a/pkg/sql/exec_util.go b/pkg/sql/exec_util.go
index a8028c2282b1..8c057b18dcb8 100644
--- a/pkg/sql/exec_util.go
+++ b/pkg/sql/exec_util.go
@@ -4007,6 +4007,10 @@ func (m *sessionDataMutator) SetCatalogDigestStalenessCheckEnabled(b bool) {
 	m.data.CatalogDigestStalenessCheckEnabled = b
 }
 
+func (m *sessionDataMutator) SetOptimizerPreferBoundedCardinality(b bool) {
+	m.data.OptimizerPreferBoundedCardinality = b
+}
+
 // Utility functions related to scrubbing sensitive information on SQL Stats.
 
 // quantizeCounts ensures that the Count field in the
diff --git a/pkg/sql/logictest/testdata/logic_test/information_schema b/pkg/sql/logictest/testdata/logic_test/information_schema
index c6edb6e13b2c..c1bd12635769 100644
--- a/pkg/sql/logictest/testdata/logic_test/information_schema
+++ b/pkg/sql/logictest/testdata/logic_test/information_schema
@@ -4007,6 +4007,7 @@ optimizer                                                  on
 optimizer_always_use_histograms                            on
 optimizer_hoist_uncorrelated_equality_subqueries           on
 optimizer_merge_joins_enabled                              on
+optimizer_prefer_bounded_cardinality                       off
 optimizer_prove_implication_with_virtual_computed_columns  on
 optimizer_push_limit_into_project_filtered_scan            on
 optimizer_push_offset_into_index_join                      on
diff --git a/pkg/sql/logictest/testdata/logic_test/pg_catalog b/pkg/sql/logictest/testdata/logic_test/pg_catalog
index 8499f75e0678..9c8346704a77 100644
--- a/pkg/sql/logictest/testdata/logic_test/pg_catalog
+++ b/pkg/sql/logictest/testdata/logic_test/pg_catalog
@@ -3007,6 +3007,7 @@ opt_split_scan_limit                                       2048                N
 optimizer_always_use_histograms                            on                  NULL      NULL        NULL        string
 optimizer_hoist_uncorrelated_equality_subqueries           on                  NULL      NULL        NULL        string
 optimizer_merge_joins_enabled                              on                  NULL      NULL        NULL        string
+optimizer_prefer_bounded_cardinality                       off                 NULL      NULL        NULL        string
 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
@@ -3212,6 +3213,7 @@ opt_split_scan_limit                                       2048                N
 optimizer_always_use_histograms                            on                  NULL  user     NULL      on                  on
 optimizer_hoist_uncorrelated_equality_subqueries           on                  NULL  user     NULL      on                  on
 optimizer_merge_joins_enabled                              on                  NULL  user     NULL      on                  on
+optimizer_prefer_bounded_cardinality                       off                 NULL  user     NULL      off                 off
 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
@@ -3416,6 +3418,7 @@ optimizer                                                  NULL    NULL     NULL
 optimizer_always_use_histograms                            NULL    NULL     NULL     NULL        NULL
 optimizer_hoist_uncorrelated_equality_subqueries           NULL    NULL     NULL     NULL        NULL
 optimizer_merge_joins_enabled                              NULL    NULL     NULL     NULL        NULL
+optimizer_prefer_bounded_cardinality                       NULL    NULL     NULL     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
diff --git a/pkg/sql/logictest/testdata/logic_test/show_source b/pkg/sql/logictest/testdata/logic_test/show_source
index b884b2f2a6f4..8ae571dd5dfe 100644
--- a/pkg/sql/logictest/testdata/logic_test/show_source
+++ b/pkg/sql/logictest/testdata/logic_test/show_source
@@ -138,6 +138,7 @@ opt_split_scan_limit                                       2048
 optimizer_always_use_histograms                            on
 optimizer_hoist_uncorrelated_equality_subqueries           on
 optimizer_merge_joins_enabled                              on
+optimizer_prefer_bounded_cardinality                       off
 optimizer_prove_implication_with_virtual_computed_columns  on
 optimizer_push_limit_into_project_filtered_scan            on
 optimizer_push_offset_into_index_join                      on
diff --git a/pkg/sql/opt/memo/cost.go b/pkg/sql/opt/memo/cost.go
index 6dfd70265abc..8dbcda97903a 100644
--- a/pkg/sql/opt/memo/cost.go
+++ b/pkg/sql/opt/memo/cost.go
@@ -19,8 +19,12 @@ type Cost struct {
 // group members during testing, by setting their cost so high that any other
 // member will have a lower cost.
 var MaxCost = Cost{
-	C:     math.Inf(+1),
-	Flags: CostFlags{FullScanPenalty: true, HugeCostPenalty: true},
+	C: math.Inf(+1),
+	Flags: CostFlags{
+		FullScanPenalty:      true,
+		HugeCostPenalty:      true,
+		UnboundedCardinality: true,
+	},
 }
 
 // Less returns true if this cost is lower than the given cost.
@@ -57,6 +61,10 @@ type CostFlags struct {
 	// used when the optimizer is forced to use a particular plan, and will error
 	// if it cannot be used.
 	HugeCostPenalty bool
+	// UnboundedCardinality is true if the operator or any of its descendants
+	// have no guaranteed upperbound on the number of rows that they can
+	// produce. See props.AnyCardinality.
+	UnboundedCardinality bool
 }
 
 // Less returns true if these flags indicate a lower penalty than the other
@@ -71,6 +79,9 @@ func (c CostFlags) Less(other CostFlags) bool {
 	if c.FullScanPenalty != other.FullScanPenalty {
 		return !c.FullScanPenalty
 	}
+	if c.UnboundedCardinality != other.UnboundedCardinality {
+		return !c.UnboundedCardinality
+	}
 	return false
 }
 
@@ -78,9 +89,10 @@ func (c CostFlags) Less(other CostFlags) bool {
 func (c *CostFlags) Add(other CostFlags) {
 	c.FullScanPenalty = c.FullScanPenalty || other.FullScanPenalty
 	c.HugeCostPenalty = c.HugeCostPenalty || other.HugeCostPenalty
+	c.UnboundedCardinality = c.UnboundedCardinality || other.UnboundedCardinality
 }
 
 // Empty returns true if these flags are empty.
 func (c CostFlags) Empty() bool {
-	return !c.FullScanPenalty && !c.HugeCostPenalty
+	return !c.FullScanPenalty && !c.HugeCostPenalty && !c.UnboundedCardinality
 }
diff --git a/pkg/sql/opt/memo/cost_test.go b/pkg/sql/opt/memo/cost_test.go
index ed8235731ad2..c44e3247c7ad 100644
--- a/pkg/sql/opt/memo/cost_test.go
+++ b/pkg/sql/opt/memo/cost_test.go
@@ -35,6 +35,8 @@ func TestCostLess(t *testing.T) {
 		{memo.MaxCost, memo.MaxCost, false},
 		{memo.MaxCost, memo.Cost{C: 1.0, Flags: memo.CostFlags{FullScanPenalty: true}}, false},
 		{memo.Cost{C: 1.0, Flags: memo.CostFlags{HugeCostPenalty: true}}, memo.MaxCost, true},
+		{memo.Cost{C: 2.0, Flags: memo.CostFlags{}}, memo.Cost{C: 1.0, Flags: memo.CostFlags{UnboundedCardinality: true}}, true},
+		{memo.Cost{C: 1.0, Flags: memo.CostFlags{UnboundedCardinality: true}}, memo.Cost{C: 2.0, Flags: memo.CostFlags{}}, false},
 	}
 	for _, tc := range testCases {
 		if tc.left.Less(tc.right) != tc.expected {
@@ -72,6 +74,8 @@ func TestCostFlagsLess(t *testing.T) {
 		{memo.CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, memo.CostFlags{FullScanPenalty: true, HugeCostPenalty: true}, false},
 		{memo.CostFlags{FullScanPenalty: false}, memo.CostFlags{FullScanPenalty: true}, true},
 		{memo.CostFlags{HugeCostPenalty: false}, memo.CostFlags{HugeCostPenalty: true}, true},
+		{memo.CostFlags{UnboundedCardinality: false}, memo.CostFlags{UnboundedCardinality: true}, true},
+		{memo.CostFlags{UnboundedCardinality: true}, memo.CostFlags{UnboundedCardinality: false}, false},
 	}
 	for _, tc := range testCases {
 		if tc.left.Less(tc.right) != tc.expected {
diff --git a/pkg/sql/opt/memo/expr_format.go b/pkg/sql/opt/memo/expr_format.go
index 2029e911a3a0..adc2af70d31d 100644
--- a/pkg/sql/opt/memo/expr_format.go
+++ b/pkg/sql/opt/memo/expr_format.go
@@ -913,6 +913,9 @@ func (f *ExprFmtCtx) formatRelational(e RelExpr, tp treeprinter.Node) {
 			if cost.Flags.HugeCostPenalty {
 				b.WriteString(" huge-cost-penalty")
 			}
+			if cost.Flags.UnboundedCardinality {
+				b.WriteString(" unbounded-cardinality")
+			}
 			tp.Child(b.String())
 		}
 	}
diff --git a/pkg/sql/opt/memo/memo.go b/pkg/sql/opt/memo/memo.go
index fa24b707c9e1..5bf19798d081 100644
--- a/pkg/sql/opt/memo/memo.go
+++ b/pkg/sql/opt/memo/memo.go
@@ -200,6 +200,7 @@ type Memo struct {
 	pushLimitIntoProjectFilteredScan           bool
 	unsafeAllowTriggersModifyingCascades       bool
 	legacyVarcharTyping                        bool
+	preferBoundedCardinality                   bool
 	internal                                   bool
 
 	// txnIsoLevel is the isolation level under which the plan was created. This
@@ -293,6 +294,7 @@ func (m *Memo) Init(ctx context.Context, evalCtx *eval.Context) {
 		pushLimitIntoProjectFilteredScan:           evalCtx.SessionData().OptimizerPushLimitIntoProjectFilteredScan,
 		unsafeAllowTriggersModifyingCascades:       evalCtx.SessionData().UnsafeAllowTriggersModifyingCascades,
 		legacyVarcharTyping:                        evalCtx.SessionData().LegacyVarcharTyping,
+		preferBoundedCardinality:                   evalCtx.SessionData().OptimizerPreferBoundedCardinality,
 		internal:                                   evalCtx.SessionData().Internal,
 		txnIsoLevel:                                evalCtx.TxnIsoLevel,
 	}
@@ -463,6 +465,7 @@ func (m *Memo) IsStale(
 		m.pushLimitIntoProjectFilteredScan != evalCtx.SessionData().OptimizerPushLimitIntoProjectFilteredScan ||
 		m.unsafeAllowTriggersModifyingCascades != evalCtx.SessionData().UnsafeAllowTriggersModifyingCascades ||
 		m.legacyVarcharTyping != evalCtx.SessionData().LegacyVarcharTyping ||
+		m.preferBoundedCardinality != evalCtx.SessionData().OptimizerPreferBoundedCardinality ||
 		m.internal != evalCtx.SessionData().Internal ||
 		m.txnIsoLevel != evalCtx.TxnIsoLevel {
 		return true, nil
diff --git a/pkg/sql/opt/memo/memo_test.go b/pkg/sql/opt/memo/memo_test.go
index 34f654726a15..65fc371965f7 100644
--- a/pkg/sql/opt/memo/memo_test.go
+++ b/pkg/sql/opt/memo/memo_test.go
@@ -538,6 +538,11 @@ func TestMemoIsStale(t *testing.T) {
 	evalCtx.SessionData().LegacyVarcharTyping = false
 	notStale()
 
+	evalCtx.SessionData().OptimizerPreferBoundedCardinality = true
+	stale()
+	evalCtx.SessionData().OptimizerPreferBoundedCardinality = false
+	notStale()
+
 	evalCtx.SessionData().Internal = true
 	stale()
 	evalCtx.SessionData().Internal = false
diff --git a/pkg/sql/opt/xform/coster.go b/pkg/sql/opt/xform/coster.go
index 07391cf4724e..f5a59789fbf2 100644
--- a/pkg/sql/opt/xform/coster.go
+++ b/pkg/sql/opt/xform/coster.go
@@ -625,8 +625,13 @@ func (c *coster) ComputeCost(candidate memo.RelExpr, required *physical.Required
 	// Add a one-time cost for any operator with unbounded cardinality. This
 	// ensures we prefer plans that push limits as far down the tree as possible,
 	// all else being equal.
+	//
+	// Also add a cost flag for unbounded cardinality.
 	if candidate.Relational().Cardinality.IsUnbounded() {
 		cost.C += cpuCostFactor
+		if c.evalCtx.SessionData().OptimizerPreferBoundedCardinality {
+			cost.Flags.UnboundedCardinality = true
+		}
 	}
 
 	if !cost.Less(memo.MaxCost) {
diff --git a/pkg/sql/opt/xform/testdata/coster/outside-histogram b/pkg/sql/opt/xform/testdata/coster/outside-histogram
new file mode 100644
index 000000000000..0acd1fe544f4
--- /dev/null
+++ b/pkg/sql/opt/xform/testdata/coster/outside-histogram
@@ -0,0 +1,484 @@
+# This file contains tests for queries that filter columns beyond the max/min
+# values in their histograms. Each query is tested multiple times with different
+# settings to show the effect those settings have on the plan.
+#
+
+exec-ddl
+CREATE TABLE t (
+  k INT PRIMARY KEY,
+  i INT,
+  s STRING,
+  INDEX (i),
+  INDEX (s)
+)
+----
+
+exec-ddl
+ALTER TABLE t INJECT STATISTICS '[
+  {
+    "columns": ["k"],
+    "created_at": "2018-01-01 1:00:00.00000+00:00",
+    "row_count": 1000,
+    "distinct_count": 1000,
+    "null_count": 0,
+    "avg_size": 2,
+    "histo_col_type": "int",
+    "histo_buckets": [
+      {"num_eq": 0, "num_range": 0, "distinct_range": 0, "upper_bound": "0"},
+      {"num_eq": 1, "num_range": 99, "distinct_range": 99, "upper_bound": "100"},
+      {"num_eq": 1, "num_range": 199, "distinct_range": 199, "upper_bound": "200"},
+      {"num_eq": 1, "num_range": 299, "distinct_range": 299, "upper_bound": "300"},
+      {"num_eq": 1, "num_range": 399, "distinct_range": 399, "upper_bound": "400"}
+    ]
+  },
+  {
+    "columns": ["i"],
+    "created_at": "2018-01-01 1:00:00.00000+00:00",
+    "row_count": 1000,
+    "distinct_count": 44,
+    "null_count": 0,
+    "avg_size": 2,
+    "histo_col_type": "int",
+    "histo_buckets": [
+      {"num_eq": 0, "num_range": 0, "distinct_range": 0, "upper_bound": "0"},
+      {"num_eq": 10, "num_range": 90, "distinct_range": 10, "upper_bound": "100"},
+      {"num_eq": 10, "num_range": 190, "distinct_range": 10, "upper_bound": "200"},
+      {"num_eq": 20, "num_range": 280, "distinct_range": 10, "upper_bound": "300"},
+      {"num_eq": 30, "num_range": 370, "distinct_range": 10, "upper_bound": "400"}
+    ]
+  },
+  {
+    "columns": ["s"],
+    "created_at": "2018-01-01 1:00:00.00000+00:00",
+    "row_count": 1000,
+    "distinct_count": 40,
+    "null_count": 0,
+    "avg_size": 3,
+    "histo_col_type": "string",
+    "histo_buckets": [
+      {"num_eq": 0, "num_range": 0, "distinct_range": 0, "upper_bound": "apple"},
+      {"num_eq": 100, "num_range": 100, "distinct_range": 10, "upper_bound": "banana"},
+      {"num_eq": 100, "num_range": 100, "distinct_range": 10, "upper_bound": "cherry"},
+      {"num_eq": 200, "num_range": 100, "distinct_range": 10, "upper_bound": "mango"},
+      {"num_eq": 200, "num_range": 100, "distinct_range": 10, "upper_bound": "pineapple"}
+    ]
+  }
+]'
+----
+
+# --------------------------------------------------
+# Q1
+# --------------------------------------------------
+
+opt set=(optimizer_prefer_bounded_cardinality=false)
+SELECT * FROM t WHERE k IN (110, 120, 130, 140) AND i = 500
+----
+index-join t
+ ├── columns: k:1!null i:2!null s:3
+ ├── cardinality: [0 - 4]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+ │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+ │                <--- 110 --- 120 --- 130 --- 140
+ │   histogram(2)=
+ ├── cost: 24.0200012
+ ├── key: (1)
+ ├── fd: ()-->(2), (1)-->(3)
+ └── scan t@t_i_idx
+      ├── columns: k:1!null i:2!null
+      ├── constraint: /2/1
+      │    ├── [/500/110 - /500/110]
+      │    ├── [/500/120 - /500/120]
+      │    ├── [/500/130 - /500/130]
+      │    └── [/500/140 - /500/140]
+      ├── cardinality: [0 - 4]
+      ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+      │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+      │                <--- 110 --- 120 --- 130 --- 140
+      │   histogram(2)=
+      ├── cost: 24.01
+      ├── key: (1)
+      └── fd: ()-->(2)
+
+opt set=(optimizer_prefer_bounded_cardinality=true)
+SELECT * FROM t WHERE k IN (110, 120, 130, 140) AND i = 500
+----
+index-join t
+ ├── columns: k:1!null i:2!null s:3
+ ├── cardinality: [0 - 4]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+ │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+ │                <--- 110 --- 120 --- 130 --- 140
+ │   histogram(2)=
+ ├── cost: 24.0200012
+ ├── key: (1)
+ ├── fd: ()-->(2), (1)-->(3)
+ └── scan t@t_i_idx
+      ├── columns: k:1!null i:2!null
+      ├── constraint: /2/1
+      │    ├── [/500/110 - /500/110]
+      │    ├── [/500/120 - /500/120]
+      │    ├── [/500/130 - /500/130]
+      │    └── [/500/140 - /500/140]
+      ├── cardinality: [0 - 4]
+      ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+      │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+      │                <--- 110 --- 120 --- 130 --- 140
+      │   histogram(2)=
+      ├── cost: 24.01
+      ├── key: (1)
+      └── fd: ()-->(2)
+
+# --------------------------------------------------
+# Q2
+# --------------------------------------------------
+
+opt set=(optimizer_prefer_bounded_cardinality=false)
+SELECT * FROM t WHERE k IN (100, 110, 120, 130) AND i > 500
+----
+index-join t
+ ├── columns: k:1!null i:2!null s:3
+ ├── cardinality: [0 - 4]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+ │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+ │                <--- 100 --- 110 --- 120 --- 130
+ │   histogram(2)=
+ ├── cost: 18.0500006
+ ├── key: (1)
+ ├── fd: (1)-->(2,3)
+ └── select
+      ├── columns: k:1!null i:2!null
+      ├── cardinality: [0 - 4]
+      ├── stats: [rows=8e-10, distinct(1)=8e-10, null(1)=0]
+      │   histogram(1)=  0 2e-10 0 2e-10 0 2e-10 0 2e-10
+      │                <--- 100 --- 110 --- 120 --- 130
+      ├── cost: 18.0400002
+      ├── key: (1)
+      ├── fd: (1)-->(2)
+      ├── scan t@t_i_idx
+      │    ├── columns: k:1!null i:2!null
+      │    ├── constraint: /2/1: [/501/100 - ]
+      │    ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0]
+      │    │   histogram(1)=  0  0  1.98e-08 2e-10 3.98e-08 2e-10 5.98e-08 2e-10 7.98e-08 2e-10
+      │    │                <--- 0 ---------- 100 ---------- 200 ---------- 300 ---------- 400
+      │    │   histogram(2)=
+      │    ├── cost: 18.0200002
+      │    ├── key: (1)
+      │    └── fd: (1)-->(2)
+      └── filters
+           └── k:1 IN (100, 110, 120, 130) [outer=(1), constraints=(/1: [/100 - /100] [/110 - /110] [/120 - /120] [/130 - /130]; tight)]
+
+opt set=(optimizer_prefer_bounded_cardinality=true)
+SELECT * FROM t WHERE k IN (100, 110, 120, 130) AND i > 500
+----
+select
+ ├── columns: k:1!null i:2!null s:3
+ ├── cardinality: [0 - 4]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+ │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+ │                <--- 100 --- 110 --- 120 --- 130
+ │   histogram(2)=
+ ├── cost: 24.21
+ ├── key: (1)
+ ├── fd: (1)-->(2,3)
+ ├── scan t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── constraint: /1
+ │    │    ├── [/100 - /100]
+ │    │    ├── [/110 - /110]
+ │    │    ├── [/120 - /120]
+ │    │    └── [/130 - /130]
+ │    ├── cardinality: [0 - 4]
+ │    ├── stats: [rows=4, distinct(1)=4, null(1)=0]
+ │    │   histogram(1)=  0   1   0   1   0   1   0   1
+ │    │                <--- 100 --- 110 --- 120 --- 130
+ │    ├── cost: 24.15
+ │    ├── key: (1)
+ │    └── fd: (1)-->(2,3)
+ └── filters
+      └── i:2 > 500 [outer=(2), constraints=(/2: [/501 - ]; tight)]
+
+# --------------------------------------------------
+# Q3
+# --------------------------------------------------
+
+opt set=(optimizer_prefer_bounded_cardinality=false)
+SELECT * FROM t WHERE k IN (410, 420, 430) AND i > 500
+----
+index-join t
+ ├── columns: k:1!null i:2!null s:3
+ ├── cardinality: [0 - 3]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0]
+ │   histogram(1)=
+ │   histogram(2)=
+ ├── cost: 18.0500006
+ ├── key: (1)
+ ├── fd: (1)-->(2,3)
+ └── select
+      ├── columns: k:1!null i:2!null
+      ├── cardinality: [0 - 3]
+      ├── stats: [rows=4e-17, distinct(1)=4e-17, null(1)=0]
+      │   histogram(1)=
+      ├── cost: 18.0400002
+      ├── key: (1)
+      ├── fd: (1)-->(2)
+      ├── scan t@t_i_idx
+      │    ├── columns: k:1!null i:2!null
+      │    ├── constraint: /2/1: [/501/410 - ]
+      │    ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0]
+      │    │   histogram(1)=  0  0  1.98e-08 2e-10 3.98e-08 2e-10 5.98e-08 2e-10 7.98e-08 2e-10
+      │    │                <--- 0 ---------- 100 ---------- 200 ---------- 300 ---------- 400
+      │    │   histogram(2)=
+      │    ├── cost: 18.0200002
+      │    ├── key: (1)
+      │    └── fd: (1)-->(2)
+      └── filters
+           └── k:1 IN (410, 420, 430) [outer=(1), constraints=(/1: [/410 - /410] [/420 - /420] [/430 - /430]; tight)]
+
+opt set=(optimizer_prefer_bounded_cardinality=true)
+SELECT * FROM t WHERE k IN (410, 420, 430) AND i > 500
+----
+select
+ ├── columns: k:1!null i:2!null s:3
+ ├── cardinality: [0 - 3]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0]
+ │   histogram(1)=
+ │   histogram(2)=
+ ├── cost: 19.03
+ ├── key: (1)
+ ├── fd: (1)-->(2,3)
+ ├── scan t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── constraint: /1
+ │    │    ├── [/410 - /410]
+ │    │    ├── [/420 - /420]
+ │    │    └── [/430 - /430]
+ │    ├── cardinality: [0 - 3]
+ │    ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0]
+ │    │   histogram(1)=
+ │    ├── cost: 19.01
+ │    ├── key: (1)
+ │    └── fd: (1)-->(2,3)
+ └── filters
+      └── i:2 > 500 [outer=(2), constraints=(/2: [/501 - ]; tight)]
+
+# --------------------------------------------------
+# Q4
+# --------------------------------------------------
+
+opt set=(optimizer_prefer_bounded_cardinality=false)
+SELECT * FROM t WHERE k IN (100, 110, 120, 130) AND i = 400 AND s < 'apple'
+----
+select
+ ├── columns: k:1!null i:2!null s:3!null
+ ├── cardinality: [0 - 4]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(3)=2e-07, null(3)=0, distinct(2,3)=2e-07, null(2,3)=0, distinct(1-3)=2e-07, null(1-3)=0]
+ │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+ │                <--- 100 --- 110 --- 120 --- 130
+ │   histogram(2)=  0 2e-07
+ │                <--- 400
+ │   histogram(3)=
+ ├── cost: 18.0700002
+ ├── key: (1)
+ ├── fd: ()-->(2), (1)-->(3)
+ ├── index-join t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── cardinality: [0 - 4]
+ │    ├── stats: [rows=8e-10]
+ │    ├── cost: 18.0500002
+ │    ├── key: (1)
+ │    ├── fd: (1)-->(2,3)
+ │    └── select
+ │         ├── columns: k:1!null s:3!null
+ │         ├── cardinality: [0 - 4]
+ │         ├── stats: [rows=8e-10, distinct(1)=8e-10, null(1)=0]
+ │         │   histogram(1)=  0 2e-10 0 2e-10 0 2e-10 0 2e-10
+ │         │                <--- 100 --- 110 --- 120 --- 130
+ │         ├── cost: 18.0400002
+ │         ├── key: (1)
+ │         ├── fd: (1)-->(3)
+ │         ├── scan t@t_s_idx
+ │         │    ├── columns: k:1!null s:3!null
+ │         │    ├── constraint: /3/1: (/NULL - /'apple')
+ │         │    ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(3)=2e-07, null(3)=0]
+ │         │    │   histogram(1)=  0  0  1.98e-08 2e-10 3.98e-08 2e-10 5.98e-08 2e-10 7.98e-08 2e-10
+ │         │    │                <--- 0 ---------- 100 ---------- 200 ---------- 300 ---------- 400
+ │         │    │   histogram(3)=
+ │         │    ├── cost: 18.0200002
+ │         │    ├── key: (1)
+ │         │    └── fd: (1)-->(3)
+ │         └── filters
+ │              └── k:1 IN (100, 110, 120, 130) [outer=(1), constraints=(/1: [/100 - /100] [/110 - /110] [/120 - /120] [/130 - /130]; tight)]
+ └── filters
+      └── i:2 = 400 [outer=(2), constraints=(/2: [/400 - /400]; tight), fd=()-->(2)]
+
+opt set=(optimizer_prefer_bounded_cardinality=true)
+SELECT * FROM t WHERE k IN (100, 110, 120, 130) AND i = 400 AND s < 'apple'
+----
+select
+ ├── columns: k:1!null i:2!null s:3!null
+ ├── cardinality: [0 - 4]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(3)=2e-07, null(3)=0, distinct(2,3)=2e-07, null(2,3)=0, distinct(1-3)=2e-07, null(1-3)=0]
+ │   histogram(1)=  0 5e-08 0 5e-08 0 5e-08 0 5e-08
+ │                <--- 100 --- 110 --- 120 --- 130
+ │   histogram(2)=  0 2e-07
+ │                <--- 400
+ │   histogram(3)=
+ ├── cost: 24.22
+ ├── key: (1)
+ ├── fd: ()-->(2), (1)-->(3)
+ ├── scan t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── constraint: /1
+ │    │    ├── [/100 - /100]
+ │    │    ├── [/110 - /110]
+ │    │    ├── [/120 - /120]
+ │    │    └── [/130 - /130]
+ │    ├── cardinality: [0 - 4]
+ │    ├── stats: [rows=4, distinct(1)=4, null(1)=0]
+ │    │   histogram(1)=  0   1   0   1   0   1   0   1
+ │    │                <--- 100 --- 110 --- 120 --- 130
+ │    ├── cost: 24.15
+ │    ├── key: (1)
+ │    └── fd: (1)-->(2,3)
+ └── filters
+      ├── i:2 = 400 [outer=(2), constraints=(/2: [/400 - /400]; tight), fd=()-->(2)]
+      └── s:3 < 'apple' [outer=(3), constraints=(/3: (/NULL - /'apple'); tight)]
+
+# --------------------------------------------------
+# Q5
+# --------------------------------------------------
+
+opt set=(enable_zigzag_join=false,optimizer_prefer_bounded_cardinality=false)
+SELECT * FROM t WHERE i = 400 AND s > 'z'
+----
+select
+ ├── columns: k:1!null i:2!null s:3!null
+ ├── stats: [rows=2e-07, distinct(2)=2e-07, null(2)=0, distinct(3)=2e-07, null(3)=0, distinct(2,3)=2e-07, null(2,3)=0]
+ │   histogram(2)=  0 2e-07
+ │                <--- 400
+ │   histogram(3)=
+ ├── cost: 18.0700014
+ ├── key: (1)
+ ├── fd: ()-->(2), (1)-->(3)
+ ├── index-join t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── stats: [rows=2e-07]
+ │    ├── cost: 18.0400014
+ │    ├── key: (1)
+ │    ├── fd: (1)-->(2,3)
+ │    └── scan t@t_s_idx
+ │         ├── columns: k:1!null s:3!null
+ │         ├── constraint: /3/1: [/e'z\x00' - ]
+ │         ├── stats: [rows=2e-07, distinct(3)=2e-07, null(3)=0]
+ │         │   histogram(3)=
+ │         ├── cost: 18.0200002
+ │         ├── key: (1)
+ │         └── fd: (1)-->(3)
+ └── filters
+      └── i:2 = 400 [outer=(2), constraints=(/2: [/400 - /400]; tight), fd=()-->(2)]
+
+opt set=(enable_zigzag_join=false,optimizer_prefer_bounded_cardinality=true)
+SELECT * FROM t WHERE i = 400 AND s > 'z'
+----
+select
+ ├── columns: k:1!null i:2!null s:3!null
+ ├── stats: [rows=2e-07, distinct(2)=2e-07, null(2)=0, distinct(3)=2e-07, null(3)=0, distinct(2,3)=2e-07, null(2,3)=0]
+ │   histogram(2)=  0 2e-07
+ │                <--- 400
+ │   histogram(3)=
+ ├── cost: 18.0700014
+ ├── cost-flags: unbounded-cardinality
+ ├── key: (1)
+ ├── fd: ()-->(2), (1)-->(3)
+ ├── index-join t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── stats: [rows=2e-07]
+ │    ├── cost: 18.0400014
+ │    ├── cost-flags: unbounded-cardinality
+ │    ├── key: (1)
+ │    ├── fd: (1)-->(2,3)
+ │    └── scan t@t_s_idx
+ │         ├── columns: k:1!null s:3!null
+ │         ├── constraint: /3/1: [/e'z\x00' - ]
+ │         ├── stats: [rows=2e-07, distinct(3)=2e-07, null(3)=0]
+ │         │   histogram(3)=
+ │         ├── cost: 18.0200002
+ │         ├── cost-flags: unbounded-cardinality
+ │         ├── key: (1)
+ │         └── fd: (1)-->(3)
+ └── filters
+      └── i:2 = 400 [outer=(2), constraints=(/2: [/400 - /400]; tight), fd=()-->(2)]
+
+# --------------------------------------------------
+# Q6
+# --------------------------------------------------
+
+opt set=(optimizer_prefer_bounded_cardinality=false)
+SELECT * FROM t WHERE k = 100 AND i = 500 AND s = 'zzz'
+----
+select
+ ├── columns: k:1!null i:2!null s:3!null
+ ├── cardinality: [0 - 1]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(3)=2e-07, null(3)=0]
+ │   histogram(1)=  0 2e-07
+ │                <--- 100
+ │   histogram(2)=
+ │   histogram(3)=
+ ├── cost: 9.04000121
+ ├── key: ()
+ ├── fd: ()-->(1-3)
+ ├── index-join t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── cardinality: [0 - 1]
+ │    ├── stats: [rows=2e-07]
+ │    ├── cost: 9.02000121
+ │    ├── key: ()
+ │    ├── fd: ()-->(1-3)
+ │    └── scan t@t_i_idx
+ │         ├── columns: k:1!null i:2!null
+ │         ├── constraint: /2/1: [/500/100 - /500/100]
+ │         ├── cardinality: [0 - 1]
+ │         ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+ │         │   histogram(1)=  0 2e-07
+ │         │                <--- 100
+ │         │   histogram(2)=
+ │         ├── cost: 9.01
+ │         ├── key: ()
+ │         └── fd: ()-->(1,2)
+ └── filters
+      └── s:3 = 'zzz' [outer=(3), constraints=(/3: [/'zzz' - /'zzz']; tight), fd=()-->(3)]
+
+opt set=(optimizer_prefer_bounded_cardinality=true)
+SELECT * FROM t WHERE k = 100 AND i = 500 AND s = 'zzz'
+----
+select
+ ├── columns: k:1!null i:2!null s:3!null
+ ├── cardinality: [0 - 1]
+ ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(3)=2e-07, null(3)=0]
+ │   histogram(1)=  0 2e-07
+ │                <--- 100
+ │   histogram(2)=
+ │   histogram(3)=
+ ├── cost: 9.04000121
+ ├── key: ()
+ ├── fd: ()-->(1-3)
+ ├── index-join t
+ │    ├── columns: k:1!null i:2 s:3
+ │    ├── cardinality: [0 - 1]
+ │    ├── stats: [rows=2e-07]
+ │    ├── cost: 9.02000121
+ │    ├── key: ()
+ │    ├── fd: ()-->(1-3)
+ │    └── scan t@t_i_idx
+ │         ├── columns: k:1!null i:2!null
+ │         ├── constraint: /2/1: [/500/100 - /500/100]
+ │         ├── cardinality: [0 - 1]
+ │         ├── stats: [rows=2e-07, distinct(1)=2e-07, null(1)=0, distinct(2)=2e-07, null(2)=0, distinct(1,2)=2e-07, null(1,2)=0]
+ │         │   histogram(1)=  0 2e-07
+ │         │                <--- 100
+ │         │   histogram(2)=
+ │         ├── cost: 9.01
+ │         ├── key: ()
+ │         └── fd: ()-->(1,2)
+ └── filters
+      └── s:3 = 'zzz' [outer=(3), constraints=(/3: [/'zzz' - /'zzz']; tight), fd=()-->(3)]
diff --git a/pkg/sql/prepared_stmt.go b/pkg/sql/prepared_stmt.go
index 967fb36b487a..681222ca6122 100644
--- a/pkg/sql/prepared_stmt.go
+++ b/pkg/sql/prepared_stmt.go
@@ -164,6 +164,11 @@ func (p *planCosts) NumCustom() int {
 
 // AvgCustom returns the average cost of all the custom plan costs in planCosts.
 // If there are no custom plan costs, it returns 0.
+//
+// TODO(mgartner): Figure out how this should incorporate cost flags. Some of
+// them, like UnboundedCardinality, are only set if session settings are set.
+// When those session settings change, do we need to clear and recompute the
+// average cost of custom plans?
 func (p *planCosts) AvgCustom() memo.Cost {
 	if p.custom.length == 0 {
 		return memo.Cost{C: 0}
diff --git a/pkg/sql/sessiondatapb/local_only_session_data.proto b/pkg/sql/sessiondatapb/local_only_session_data.proto
index f07b3c86e0da..63694ead622e 100644
--- a/pkg/sql/sessiondatapb/local_only_session_data.proto
+++ b/pkg/sql/sessiondatapb/local_only_session_data.proto
@@ -594,6 +594,10 @@ message LocalOnlySessionData {
   // CatalogDigestStalenessCheckEnabled is used to enable using the catalog
   // digest information to do fast memo checks.
   bool catalog_digest_staleness_check_enabled = 153;
+  // OptimizerPreferBoundedCardinality instructs the optimizer to prefer query
+  // plans in which every expression has a bounded cardinality over plans with
+  // one or more expressions with unbounded cardinality.
+  bool optimizer_prefer_bounded_cardinality = 154;
 
   ///////////////////////////////////////////////////////////////////////////
   // WARNING: consider whether a session parameter you're adding needs to  //
diff --git a/pkg/sql/vars.go b/pkg/sql/vars.go
index b4456217ca82..b85c2aed493d 100644
--- a/pkg/sql/vars.go
+++ b/pkg/sql/vars.go
@@ -3724,6 +3724,7 @@ var varGen = map[string]sessionVar{
 		},
 		GlobalDefault: globalFalse,
 	},
+
 	// CockroachDB extension.
 	`catalog_digest_staleness_check_enabled`: {
 		GetStringVal: makePostgresBoolGetStringValFn(`catalog_digest_staleness_check_enabled`),
@@ -3741,6 +3742,23 @@ var varGen = map[string]sessionVar{
 		GlobalDefault: globalTrue,
 		Hidden:        true,
 	},
+
+	// CockroachDB extension.
+	`optimizer_prefer_bounded_cardinality`: {
+		GetStringVal: makePostgresBoolGetStringValFn(`optimizer_prefer_bounded_cardinality`),
+		Set: func(_ context.Context, m sessionDataMutator, s string) error {
+			b, err := paramparse.ParseBoolVar("optimizer_prefer_bounded_cardinality", s)
+			if err != nil {
+				return err
+			}
+			m.SetOptimizerPreferBoundedCardinality(b)
+			return nil
+		},
+		Get: func(evalCtx *extendedEvalContext, _ *kv.Txn) (string, error) {
+			return formatBoolAsPostgresSetting(evalCtx.SessionData().OptimizerPreferBoundedCardinality), nil
+		},
+		GlobalDefault: globalFalse,
+	},
 }
 
 func ReplicationModeFromString(s string) (sessiondatapb.ReplicationMode, error) {