From 2916833e1b514a2bdab4577d972e60f99ac107e5 Mon Sep 17 00:00:00 2001 From: Lohit Kolluri Date: Tue, 23 Jun 2026 04:15:41 +0530 Subject: [PATCH 1/4] opt: drop provably-non-null COALESCE operands This commit adds normalization rules to simplify COALESCE expressions when one or more operands are provably non-null. For example: COALESCE(NULL, x, y) -> COALESCE(x, y) COALESCE(x::INT, y) -> x::INT (if x is non-null) The key changes are: - Added CanSimplifyCoalesceInScalar helper function that checks whether a COALESCE expression has any provably-non-null operands - Added SimplifyCoalesceInProjections and SimplifyCoalesceInFilters normalization rules - Updated optgen patterns for project.opt and select.opt to use inlined match patterns This is a minor optimization that can eliminate unnecessary COALESCE evaluations during query planning. Epic: none Release note (sql change): The optimizer can now simplify COALESCE expressions by dropping operands that are provably non-null. Signed-off-by: Lohit Kolluri --- pkg/sql/opt/norm/project_funcs.go | 20 ++++++ pkg/sql/opt/norm/rules/project.opt | 21 +++++++ pkg/sql/opt/norm/rules/select.opt | 16 +++++ pkg/sql/opt/norm/scalar_funcs.go | 99 +++++++++++++++++++++++++++--- pkg/sql/opt/norm/select_funcs.go | 21 +++++++ 5 files changed, 168 insertions(+), 9 deletions(-) diff --git a/pkg/sql/opt/norm/project_funcs.go b/pkg/sql/opt/norm/project_funcs.go index 3a0e64337ed7..6c6bcc8685de 100644 --- a/pkg/sql/opt/norm/project_funcs.go +++ b/pkg/sql/opt/norm/project_funcs.go @@ -1008,3 +1008,23 @@ func (c *CustomFuncs) HasVolatileProjection(projections memo.ProjectionsExpr) bo } return false } + +// SimplifyCoalesceInProjections simplifies Coalesce expressions in projections +// using the given not-null columns. Projections whose element is unchanged are +// reused as-is to avoid unnecessary memo invalidation. +func (c *CustomFuncs) SimplifyCoalesceInProjections( + projections memo.ProjectionsExpr, notNullCols opt.ColSet, +) memo.ProjectionsExpr { + newProjections := make(memo.ProjectionsExpr, len(projections)) + for i := range projections { + p := &projections[i] + simplified := c.SimplifyCoalesceInScalar(p.Element, notNullCols) + if simplified == p.Element { + // No change; reuse the original ProjectionsItem. + newProjections[i] = *p + } else { + newProjections[i] = c.f.ConstructProjectionsItem(simplified, p.Col) + } + } + return newProjections +} diff --git a/pkg/sql/opt/norm/rules/project.opt b/pkg/sql/opt/norm/rules/project.opt index 012f32f11c9b..c013d55ee8cb 100644 --- a/pkg/sql/opt/norm/rules/project.opt +++ b/pkg/sql/opt/norm/rules/project.opt @@ -331,6 +331,27 @@ $input $passthrough ) +# SimplifyCoalesceProject simplifies Coalesce projections by eliminating leading +# operands that are guaranteed to be non-null according to the input's +# relational properties. +[SimplifyCoalesceProject, Normalize, LowPriority] +(Project + $input:* + $projections:[ + ... + (ProjectionsItem $scalar:*) & + (CanSimplifyCoalesceInScalar $scalar (NotNullCols $input)) + ... + ] + $passthrough:* +) +=> +(Project + $input + (SimplifyCoalesceInProjections $projections (NotNullCols $input)) + $passthrough +) + # FoldIsNullProject folds "x IS NULL" projections to false if "x" is not null in # the Project's input. It matches if there is at least one projection that can # be folded, and it replaces all projections that can be folded. diff --git a/pkg/sql/opt/norm/rules/select.opt b/pkg/sql/opt/norm/rules/select.opt index 544a8cbcf8b1..37c31945a515 100644 --- a/pkg/sql/opt/norm/rules/select.opt +++ b/pkg/sql/opt/norm/rules/select.opt @@ -31,6 +31,22 @@ => (Select $input (SimplifyFilters $filters)) +# SimplifyCoalesceSelect simplifies Coalesce expressions in Select filters by +# eliminating leading operands that are guaranteed to be non-null according to +# the input's relational properties. +[SimplifyCoalesceSelect, Normalize, LowPriority] +(Select + $input:* + $filters:[ + ... + (FiltersItem $cond:*) & + (CanSimplifyCoalesceInScalar $cond (NotNullCols $input)) + ... + ] +) +=> +(Select $input (SimplifyCoalesceInFilters $filters (NotNullCols $input))) + # ConsolidateSelectFilters consolidates filters that constrain a single # variable. For example, filters x >= 5 and x <= 10 would be combined into a # single Range operation. diff --git a/pkg/sql/opt/norm/scalar_funcs.go b/pkg/sql/opt/norm/scalar_funcs.go index 10d9fd33a9bd..8cdc745890f5 100644 --- a/pkg/sql/opt/norm/scalar_funcs.go +++ b/pkg/sql/opt/norm/scalar_funcs.go @@ -74,25 +74,106 @@ func (c *CustomFuncs) ConstructSortedUniqueList( // SimplifyCoalesce discards any leading null operands, and then if the next // operand is a constant, replaces with that constant. func (c *CustomFuncs) SimplifyCoalesce(args memo.ScalarListExpr) opt.ScalarExpr { - for i := 0; i < len(args)-1; i++ { - item := args[i] + return c.simplifyCoalesce(args, opt.ColSet{}) +} + +func (c *CustomFuncs) simplifyCoalesce( + args memo.ScalarListExpr, notNullCols opt.ColSet, +) opt.ScalarExpr { + // Iterate over all args (including the last) so that ExprIsNeverNull can + // fire for any position: once a provably-non-null arg is found, COALESCE + // will always return that arg, so we can replace the whole expression. + for i := 0; i < len(args); i++ { + if memo.ExprIsNeverNull(args[i], notNullCols) { + return args[i] + } - // If item is not a constant value, then its value may turn out to be - // null, so no more folding. Return operands from then on. - if !c.IsConstValueOrGroupOfConstValues(item) { + if !c.IsConstValueOrGroupOfConstValues(args[i]) { + if i >= len(args)-1 { + return args[i] + } return c.f.ConstructCoalesce(args[i:]) } - if item.Op() != opt.NullOp { - return item + if args[i].Op() != opt.NullOp { + return args[i] } } - // All operands up to the last were null (or the last is the only operand), - // so return the last operand without the wrapping COALESCE function. return args[len(args)-1] } +// CanSimplifyCoalesce returns true if simplifyCoalesce would change the given +// Coalesce expression. It mirrors the logic of simplifyCoalesce directly to +// avoid the cost of constructing a new expression just to compare arg counts. +func (c *CustomFuncs) CanSimplifyCoalesce(args memo.ScalarListExpr, notNullCols opt.ColSet) bool { + for i, arg := range args { + if memo.ExprIsNeverNull(arg, notNullCols) { + return true + } + if !c.IsConstValueOrGroupOfConstValues(arg) { + // Non-constant arg at position i blocks further simplification. The + // expression changes only if leading nulls (i > 0) were already + // stripped. + return i > 0 + } + if arg.Op() != opt.NullOp { + // Non-null constant: simplifyCoalesce returns it directly. + return true + } + // NullOp constant: will be stripped; continue to next arg. + } + // The loop exhausted all args, meaning every arg was a null constant. + // simplifyCoalesce returns args[last] directly (unwrapping the COALESCE), + // which is a simplification whenever there is more than one arg. The + // single-arg case is already handled by EliminateCoalesce before this + // function is called, so we can safely return true here. + return true +} + +// SimplifyCoalesceInScalar recursively simplifies Coalesce expressions in the +// given scalar expression tree using the provided not-null columns. It handles +// nested Coalesce expressions by recursing into the simplified result. +func (c *CustomFuncs) SimplifyCoalesceInScalar( + e opt.ScalarExpr, notNullCols opt.ColSet, +) opt.ScalarExpr { + var replace ReplaceFunc + replace = func(e opt.Expr) opt.Expr { + if co, ok := e.(*memo.CoalesceExpr); ok { + simplified := c.simplifyCoalesce(co.Args, notNullCols) + // Recurse into the simplified result so that nested Coalesce + // expressions (e.g. COALESCE(a, COALESCE(b, c))) are also handled + // when the outer simplification does not fire. + return c.f.Replace(simplified, replace) + } + return c.f.Replace(e, replace) + } + return replace(e).(opt.ScalarExpr) +} + +// CanSimplifyCoalesceInScalar returns true if the scalar expression tree +// contains any Coalesce expression that can be simplified using notNullCols. It +// recurses into Coalesce args so that nested Coalesce expressions are not +// missed when the outer Coalesce is not itself simplifiable. +func (c *CustomFuncs) CanSimplifyCoalesceInScalar(e opt.ScalarExpr, notNullCols opt.ColSet) bool { + found := false + var replace ReplaceFunc + replace = func(e opt.Expr) opt.Expr { + if co, ok := e.(*memo.CoalesceExpr); ok { + if c.CanSimplifyCoalesce(co.Args, notNullCols) { + found = true + return co + } + // Even if the outer Coalesce is not simplifiable, recurse into its + // args to find simplifiable nested Coalesce expressions. + return c.f.Replace(co, replace) + } + return c.f.Replace(e, replace) + } + replace(e) + return found +} + // IsConstValueEqual returns whether const1 and const2 are equal. func (c *CustomFuncs) IsConstValueEqual(const1, const2 opt.ScalarExpr) bool { op1 := const1.Op() diff --git a/pkg/sql/opt/norm/select_funcs.go b/pkg/sql/opt/norm/select_funcs.go index c9b5ab8c7d90..dd5859bcd477 100644 --- a/pkg/sql/opt/norm/select_funcs.go +++ b/pkg/sql/opt/norm/select_funcs.go @@ -419,3 +419,24 @@ func (c *CustomFuncs) addConjuncts( func (c *CustomFuncs) ForDuplicateRemoval(private *memo.OrdinalityPrivate) (ok bool) { return private.ForDuplicateRemoval } + +// SimplifyCoalesceInFilters simplifies Coalesce expressions in filter +// conditions using the given not-null columns. Filters whose condition is +// unchanged are reused as-is to avoid unnecessary memo invalidation. +func (c *CustomFuncs) SimplifyCoalesceInFilters( + filters memo.FiltersExpr, notNullCols opt.ColSet, +) memo.FiltersExpr { + newFilters := make(memo.FiltersExpr, len(filters)) + for i := range filters { + f := &filters[i] + simplified := c.SimplifyCoalesceInScalar(f.Condition, notNullCols) + if simplified == f.Condition { + // No change; reuse the original FiltersItem. + newFilters[i] = *f + } else { + newFilters[i] = c.f.ConstructFiltersItem(simplified) + } + } + return newFilters +} + From 8206f9e9b6a61f48d2d9fa33f8044b50275e5cba Mon Sep 17 00:00:00 2001 From: Lohit Kolluri Date: Tue, 23 Jun 2026 04:23:47 +0530 Subject: [PATCH 2/4] opt: add testdata and tpcds expected-output updates for COALESCE simplification Updates test expectations for the new SimplifyCoalesceProject and SimplifyCoalesceSelect normalization rules: - pkg/sql/opt/norm/testdata/rules/scalar: new test cases for SimplifyCoalesceProject (positive and negative cases, nested COALESCE) - pkg/sql/opt/norm/testdata/rules/select: new test cases for SimplifyCoalesceSelect (positive and negative cases, nested COALESCE) - pkg/sql/opt/norm/testdata/rules/decorrelate: disable SimplifyCoalesceProject for an existing decorrelation test to preserve expected output - pkg/sql/opt/xform/testdata/external/tpcds/q41-q50 and pkg/sql/opt/xform/testdata/external/tpcds/q41-q50-no-stats: columns that were previously COALESCE-wrapped are now provably non-null (!null annotation) Epic: none Signed-off-by: Lohit Kolluri --- pkg/sql/opt/norm/testdata/rules/decorrelate | 2 +- pkg/sql/opt/norm/testdata/rules/scalar | 109 +++++++++++++++++- pkg/sql/opt/norm/testdata/rules/select | 64 ++++++++++ .../opt/xform/testdata/external/tpcds/q41-q50 | 60 +++++----- .../testdata/external/tpcds/q41-q50-no-stats | 60 +++++----- 5 files changed, 233 insertions(+), 62 deletions(-) diff --git a/pkg/sql/opt/norm/testdata/rules/decorrelate b/pkg/sql/opt/norm/testdata/rules/decorrelate index 821c33265641..abb2b5198258 100644 --- a/pkg/sql/opt/norm/testdata/rules/decorrelate +++ b/pkg/sql/opt/norm/testdata/rules/decorrelate @@ -3006,7 +3006,7 @@ group-by (hash) └── y:2 # Right input of SemiJoin is Project. -norm expect=TryDecorrelateSemiJoin +norm expect=TryDecorrelateSemiJoin disable=SimplifyCoalesceProject SELECT k FROM a WHERE EXISTS ( diff --git a/pkg/sql/opt/norm/testdata/rules/scalar b/pkg/sql/opt/norm/testdata/rules/scalar index a6bea921cd82..aaadb9115432 100644 --- a/pkg/sql/opt/norm/testdata/rules/scalar +++ b/pkg/sql/opt/norm/testdata/rules/scalar @@ -2,6 +2,10 @@ exec-ddl CREATE TABLE a (k INT PRIMARY KEY, i INT, f FLOAT, s STRING, arr int[]) ---- +exec-ddl +CREATE TABLE ij (i INT NOT NULL, j INT) +---- + exec-ddl CREATE TABLE xy (x INT PRIMARY KEY, y INT) ---- @@ -172,7 +176,9 @@ project └── projections └── COALESCE(s:4, s:4 || 'foo') [as=coalesce:8, outer=(4), immutable] -# Trailing null can't be removed. +# Trailing null can't be removed when leading arg is a non-constant (nullable +# variable): the original SimplifyCoalesce rule only fires when the leading arg +# is a constant, so no folding happens here. norm SELECT COALESCE(i, NULL, NULL) FROM a ---- @@ -183,6 +189,44 @@ project └── projections └── COALESCE(i:2, CAST(NULL AS INT8), CAST(NULL AS INT8)) [as=coalesce:8, outer=(2)] +# All-null COALESCE collapses to a single null via SimplifyCoalesce (the scalar +# rule fires when the leading arg is constant). CanSimplifyCoalesce correctly +# returns true when the loop exhausts all-null args, matching simplifyCoalesce's +# behaviour of returning args[last] (unwrapping the COALESCE). +norm expect=SimplifyCoalesce +SELECT COALESCE(NULL::INT, NULL::INT) FROM a +---- +project + ├── columns: coalesce:8 + ├── fd: ()-->(8) + ├── scan a + └── projections + └── CAST(NULL AS INT8) [as=coalesce:8] + +# -------------------------------------------------- +# SimplifyCoalesceProject +# -------------------------------------------------- + +norm expect=SimplifyCoalesceProject +SELECT COALESCE(i, j) FROM ij +---- +project + ├── columns: coalesce:6!null + ├── scan ij + │ └── columns: i:1!null + └── projections + └── i:1 [as=coalesce:6, outer=(1)] + +norm expect=SimplifyCoalesceProject +SELECT COALESCE(i, NULL, NULL) FROM ij +---- +project + ├── columns: coalesce:6!null + ├── scan ij + │ └── columns: i:1!null + └── projections + └── i:1 [as=coalesce:6, outer=(1)] + norm expect=SimplifyCoalesce SELECT COALESCE((1, 2, 3), (2, 3, 4)) FROM a ---- @@ -193,6 +237,69 @@ project └── projections └── (1, 2, 3) [as=coalesce:8] +# Negative test: should not fire when no arg is provably non-null. +norm expect-not=SimplifyCoalesceProject +SELECT COALESCE(i, f) FROM a +---- +project + ├── columns: coalesce:8 + ├── immutable + ├── scan a + │ └── columns: i:2 f:3 + └── projections + └── COALESCE(i:2::FLOAT8, f:3) [as=coalesce:8, outer=(2,3), immutable] + +# Middle argument is provably non-null: leading NULLs are stripped, then the +# NOT NULL arg is returned directly. +norm expect=SimplifyCoalesceProject +SELECT COALESCE(NULL::INT, i, j) FROM ij +---- +project + ├── columns: coalesce:6!null + ├── scan ij + │ └── columns: i:1!null + └── projections + └── i:1 [as=coalesce:6, outer=(1)] + +# Nested COALESCE: outer is not directly simplifiable, but the inner +# COALESCE(i, j) can be simplified to i (since i is NOT NULL). The fix +# ensures the inner Coalesce is not missed. +norm expect=SimplifyCoalesceProject +SELECT COALESCE(j, COALESCE(i, j)) FROM ij +---- +project + ├── columns: coalesce:6 + ├── scan ij + │ └── columns: i:1!null j:2 + └── projections + └── COALESCE(j:2, i:1) [as=coalesce:6, outer=(1,2)] + +# COALESCE inside an IF expression: the inner nested Coalesce is simplified +# via SimplifyCoalesceProject even though the outer Coalesce is not itself +# directly simplifiable. +norm expect=SimplifyCoalesceProject +SELECT IF(j > 0, COALESCE(j, COALESCE(i, j)), j) FROM ij +---- +project + ├── columns: if:6 + ├── scan ij + │ └── columns: i:1!null j:2 + └── projections + └── CASE j:2 > 0 WHEN true THEN COALESCE(j:2, i:1) ELSE j:2 END [as=if:6, outer=(1,2)] + +# SimplifyCoalesceProject does not fire when the first arg is a non-constant +# nullable variable: CanSimplifyCoalesce exits early with false because i is +# not a constant, and no leading nulls were stripped (i > 0 = false). +norm expect-not=SimplifyCoalesceProject +SELECT COALESCE(i, NULL, NULL) FROM a +---- +project + ├── columns: coalesce:8 + ├── scan a + │ └── columns: i:2 + └── projections + └── COALESCE(i:2, CAST(NULL AS INT8), CAST(NULL AS INT8)) [as=coalesce:8, outer=(2)] + # -------------------------------------------------- # EliminateCast diff --git a/pkg/sql/opt/norm/testdata/rules/select b/pkg/sql/opt/norm/testdata/rules/select index 47821e278b2d..23c5c81f1b06 100644 --- a/pkg/sql/opt/norm/testdata/rules/select +++ b/pkg/sql/opt/norm/testdata/rules/select @@ -1945,6 +1945,70 @@ select └── a:2 IS NOT DISTINCT FROM d:5 [outer=(2,5)] +# -------------------------------------------------- +# SimplifyCoalesceSelect +# -------------------------------------------------- + +norm expect=SimplifyCoalesceSelect +SELECT * FROM d WHERE COALESCE(a, b) = 1 +---- +select + ├── columns: k:1!null a:2!null b:3 c:4 d:5 + ├── key: (1) + ├── fd: ()-->(2), (1)-->(3-5) + ├── scan d + │ ├── columns: k:1!null a:2!null b:3 c:4 d:5 + │ ├── key: (1) + │ └── fd: (1)-->(2-5) + └── filters + └── a:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)] + +# Negative test: should not fire when no argument is provably non-null. +norm expect-not=SimplifyCoalesceSelect +SELECT * FROM a WHERE COALESCE(i, 0) > 0 +---- +select + ├── columns: k:1!null i:2 f:3 s:4 j:5 + ├── key: (1) + ├── fd: (1)-->(2-5) + ├── scan a + │ ├── columns: k:1!null i:2 f:3 s:4 j:5 + │ ├── key: (1) + │ └── fd: (1)-->(2-5) + └── filters + └── COALESCE(i:2, 0) > 0 [outer=(2)] + +# Leading NULLs stripped, then NOT NULL arg is reached. +norm expect=SimplifyCoalesceSelect +SELECT * FROM d WHERE COALESCE(NULL::INT, a, b) = 1 +---- +select + ├── columns: k:1!null a:2!null b:3 c:4 d:5 + ├── key: (1) + ├── fd: ()-->(2), (1)-->(3-5) + ├── scan d + │ ├── columns: k:1!null a:2!null b:3 c:4 d:5 + │ ├── key: (1) + │ └── fd: (1)-->(2-5) + └── filters + └── a:2 = 1 [outer=(2), constraints=(/2: [/1 - /1]; tight), fd=()-->(2)] + +# Nested COALESCE: outer is not directly simplifiable, but the inner +# COALESCE(a, c) can be simplified to a (since a is NOT NULL). +norm expect=SimplifyCoalesceSelect +SELECT * FROM d WHERE COALESCE(b, COALESCE(a, c)) = 1 +---- +select + ├── columns: k:1!null a:2!null b:3 c:4 d:5 + ├── key: (1) + ├── fd: (1)-->(2-5) + ├── scan d + │ ├── columns: k:1!null a:2!null b:3 c:4 d:5 + │ ├── key: (1) + │ └── fd: (1)-->(2-5) + └── filters + └── COALESCE(b:3, a:2) = 1 [outer=(2,3)] + # -------------------------------------------------- # EliminateCaseTrailingFalsyBranch # -------------------------------------------------- diff --git a/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50 b/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50 index 66874dbb4ebe..fb3271653bb5 100644 --- a/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50 +++ b/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50 @@ -1792,39 +1792,39 @@ limit │ │ │ ├── key: (4) │ │ │ ├── fd: ()-->(107), (4)-->(101,103,104) │ │ │ ├── select - │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102 rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 + │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102!null rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null │ │ │ │ ├── immutable │ │ │ │ ├── key: (4) │ │ │ │ ├── fd: (4)-->(101-104), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ ├── window partition=() ordering=+(102|106) - │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102 rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 + │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102!null rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null │ │ │ │ │ ├── immutable │ │ │ │ │ ├── key: (4) │ │ │ │ │ ├── fd: (4)-->(101-104), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ │ ├── window partition=() ordering=+(101|105) - │ │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102 rank:103 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 + │ │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102!null rank:103 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ ├── fd: (4)-->(101-103), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 ws_item_sk:4!null return_ratio:101 currency_ratio:102 + │ │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null ws_item_sk:4!null return_ratio:101 currency_ratio:102!null │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ │ ├── fd: (4)-->(101,102), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ ├── columns: return_ratio:101 currency_ratio:102 ws_item_sk:4!null + │ │ │ │ │ │ │ │ ├── columns: return_ratio:101 currency_ratio:102!null ws_item_sk:4!null │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ │ │ ├── fd: (4)-->(101,102) │ │ │ │ │ │ │ │ ├── group-by (streaming) - │ │ │ │ │ │ │ │ │ ├── columns: ws_item_sk:4!null sum:94 sum:96 sum:98 sum:100 + │ │ │ │ │ │ │ │ │ ├── columns: ws_item_sk:4!null sum:94 sum:96!null sum:98!null sum:100!null │ │ │ │ │ │ │ │ │ ├── grouping columns: ws_item_sk:4!null │ │ │ │ │ │ │ │ │ ├── internal-ordering: +4 │ │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ │ │ │ ├── fd: (4)-->(94,96,98,100) │ │ │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ │ │ ├── columns: column93:93 column95:95 column97:97 column99:99 ws_item_sk:4!null + │ │ │ │ │ │ │ │ │ │ ├── columns: column93:93 column95:95!null column97:97!null column99:99!null ws_item_sk:4!null │ │ │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ │ │ ├── ordering: +4 │ │ │ │ │ │ │ │ │ │ ├── inner-join (lookup date_dim) @@ -1865,9 +1865,9 @@ limit │ │ │ │ │ │ │ │ │ │ │ └── d_moy:71 = 11 [outer=(71), constraints=(/71: [/11 - /11]; tight), fd=()-->(71)] │ │ │ │ │ │ │ │ │ │ └── projections │ │ │ │ │ │ │ │ │ │ ├── COALESCE(wr_return_quantity:51, 0) [as=column93:93, outer=(51)] - │ │ │ │ │ │ │ │ │ │ ├── COALESCE(ws_quantity:19, 0) [as=column95:95, outer=(19)] - │ │ │ │ │ │ │ │ │ │ ├── COALESCE(wr_return_amt:52::DECIMAL, 0) [as=column97:97, outer=(52), immutable] - │ │ │ │ │ │ │ │ │ │ └── COALESCE(ws_net_paid:30::DECIMAL, 0) [as=column99:99, outer=(30), immutable] + │ │ │ │ │ │ │ │ │ │ ├── ws_quantity:19 [as=column95:95, outer=(19)] + │ │ │ │ │ │ │ │ │ │ ├── wr_return_amt:52::DECIMAL [as=column97:97, outer=(52), immutable] + │ │ │ │ │ │ │ │ │ │ └── ws_net_paid:30::DECIMAL [as=column99:99, outer=(30), immutable] │ │ │ │ │ │ │ │ │ └── aggregations │ │ │ │ │ │ │ │ │ ├── sum [as=sum:94, outer=(93)] │ │ │ │ │ │ │ │ │ │ └── column93:93 @@ -1904,39 +1904,39 @@ limit │ │ ├── key: (123) │ │ ├── fd: ()-->(217), (123)-->(211,213,214) │ │ ├── select - │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212 rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 + │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212!null rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null │ │ │ ├── immutable │ │ │ ├── key: (123) │ │ │ ├── fd: (123)-->(211-214), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ ├── window partition=() ordering=+(212|216) - │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212 rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 + │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212!null rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null │ │ │ │ ├── immutable │ │ │ │ ├── key: (123) │ │ │ │ ├── fd: (123)-->(211-214), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ │ ├── window partition=() ordering=+(211|215) - │ │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212 rank:213 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 + │ │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212!null rank:213 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null │ │ │ │ │ ├── immutable │ │ │ │ │ ├── key: (123) │ │ │ │ │ ├── fd: (123)-->(211-213), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ │ │ ├── project - │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 cs_item_sk:123!null return_ratio:211 currency_ratio:212 + │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null cs_item_sk:123!null return_ratio:211 currency_ratio:212!null │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ ├── key: (123) │ │ │ │ │ │ ├── fd: (123)-->(211,212), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ ├── columns: return_ratio:211 currency_ratio:212 cs_item_sk:123!null + │ │ │ │ │ │ │ ├── columns: return_ratio:211 currency_ratio:212!null cs_item_sk:123!null │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ ├── key: (123) │ │ │ │ │ │ │ ├── fd: (123)-->(211,212) │ │ │ │ │ │ │ ├── group-by (streaming) - │ │ │ │ │ │ │ │ ├── columns: cs_item_sk:123!null sum:204 sum:206 sum:208 sum:210 + │ │ │ │ │ │ │ │ ├── columns: cs_item_sk:123!null sum:204 sum:206!null sum:208!null sum:210!null │ │ │ │ │ │ │ │ ├── grouping columns: cs_item_sk:123!null │ │ │ │ │ │ │ │ ├── internal-ordering: +123 │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ ├── key: (123) │ │ │ │ │ │ │ │ ├── fd: (123)-->(204,206,208,210) │ │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ │ ├── columns: column203:203 column205:205 column207:207 column209:209 cs_item_sk:123!null + │ │ │ │ │ │ │ │ │ ├── columns: column203:203 column205:205!null column207:207!null column209:209!null cs_item_sk:123!null │ │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ │ ├── ordering: +123 │ │ │ │ │ │ │ │ │ ├── inner-join (lookup date_dim) @@ -1977,9 +1977,9 @@ limit │ │ │ │ │ │ │ │ │ │ └── d_moy:181 = 11 [outer=(181), constraints=(/181: [/11 - /11]; tight), fd=()-->(181)] │ │ │ │ │ │ │ │ │ └── projections │ │ │ │ │ │ │ │ │ ├── COALESCE(cr_return_quantity:161, 0) [as=column203:203, outer=(161)] - │ │ │ │ │ │ │ │ │ ├── COALESCE(cs_quantity:126, 0) [as=column205:205, outer=(126)] - │ │ │ │ │ │ │ │ │ ├── COALESCE(cr_return_amount:162::DECIMAL, 0) [as=column207:207, outer=(162), immutable] - │ │ │ │ │ │ │ │ │ └── COALESCE(cs_net_paid:137::DECIMAL, 0) [as=column209:209, outer=(137), immutable] + │ │ │ │ │ │ │ │ │ ├── cs_quantity:126 [as=column205:205, outer=(126)] + │ │ │ │ │ │ │ │ │ ├── cr_return_amount:162::DECIMAL [as=column207:207, outer=(162), immutable] + │ │ │ │ │ │ │ │ │ └── cs_net_paid:137::DECIMAL [as=column209:209, outer=(137), immutable] │ │ │ │ │ │ │ │ └── aggregations │ │ │ │ │ │ │ │ ├── sum [as=sum:204, outer=(203)] │ │ │ │ │ │ │ │ │ └── column203:203 @@ -2016,39 +2016,39 @@ limit │ ├── key: (225) │ ├── fd: ()-->(314), (225)-->(308,310,311) │ ├── select - │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309 rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 + │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309!null rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null │ │ ├── immutable │ │ ├── key: (225) │ │ ├── fd: (225)-->(308-311), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ ├── window partition=() ordering=+(309|313) - │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309 rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 + │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309!null rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null │ │ │ ├── immutable │ │ │ ├── key: (225) │ │ │ ├── fd: (225)-->(308-311), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ │ ├── window partition=() ordering=+(308|312) - │ │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309 rank:310 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 + │ │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309!null rank:310 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null │ │ │ │ ├── immutable │ │ │ │ ├── key: (225) │ │ │ │ ├── fd: (225)-->(308-310), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ │ │ ├── project - │ │ │ │ │ ├── columns: rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 ss_item_sk:225!null return_ratio:308 currency_ratio:309 + │ │ │ │ │ ├── columns: rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null ss_item_sk:225!null return_ratio:308 currency_ratio:309!null │ │ │ │ │ ├── immutable │ │ │ │ │ ├── key: (225) │ │ │ │ │ ├── fd: (225)-->(308,309), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ │ │ │ ├── project - │ │ │ │ │ │ ├── columns: return_ratio:308 currency_ratio:309 ss_item_sk:225!null + │ │ │ │ │ │ ├── columns: return_ratio:308 currency_ratio:309!null ss_item_sk:225!null │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ ├── key: (225) │ │ │ │ │ │ ├── fd: (225)-->(308,309) │ │ │ │ │ │ ├── group-by (streaming) - │ │ │ │ │ │ │ ├── columns: ss_item_sk:225!null sum:301 sum:303 sum:305 sum:307 + │ │ │ │ │ │ │ ├── columns: ss_item_sk:225!null sum:301 sum:303!null sum:305!null sum:307!null │ │ │ │ │ │ │ ├── grouping columns: ss_item_sk:225!null │ │ │ │ │ │ │ ├── internal-ordering: +225 │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ ├── key: (225) │ │ │ │ │ │ │ ├── fd: (225)-->(301,303,305,307) │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ ├── columns: column300:300 column302:302 column304:304 column306:306 ss_item_sk:225!null + │ │ │ │ │ │ │ │ ├── columns: column300:300 column302:302!null column304:304!null column306:306!null ss_item_sk:225!null │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ ├── ordering: +225 │ │ │ │ │ │ │ │ ├── inner-join (lookup date_dim) @@ -2089,9 +2089,9 @@ limit │ │ │ │ │ │ │ │ │ └── d_moy:278 = 11 [outer=(278), constraints=(/278: [/11 - /11]; tight), fd=()-->(278)] │ │ │ │ │ │ │ │ └── projections │ │ │ │ │ │ │ │ ├── COALESCE(sr_return_quantity:258, 0) [as=column300:300, outer=(258)] - │ │ │ │ │ │ │ │ ├── COALESCE(ss_quantity:233, 0) [as=column302:302, outer=(233)] - │ │ │ │ │ │ │ │ ├── COALESCE(sr_return_amt:259::DECIMAL, 0) [as=column304:304, outer=(259), immutable] - │ │ │ │ │ │ │ │ └── COALESCE(ss_net_paid:243::DECIMAL, 0) [as=column306:306, outer=(243), immutable] + │ │ │ │ │ │ │ │ ├── ss_quantity:233 [as=column302:302, outer=(233)] + │ │ │ │ │ │ │ │ ├── sr_return_amt:259::DECIMAL [as=column304:304, outer=(259), immutable] + │ │ │ │ │ │ │ │ └── ss_net_paid:243::DECIMAL [as=column306:306, outer=(243), immutable] │ │ │ │ │ │ │ └── aggregations │ │ │ │ │ │ │ ├── sum [as=sum:301, outer=(300)] │ │ │ │ │ │ │ │ └── column300:300 diff --git a/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50-no-stats b/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50-no-stats index 85a5da539fa9..6c312c8396a2 100644 --- a/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50-no-stats +++ b/pkg/sql/opt/xform/testdata/external/tpcds/q41-q50-no-stats @@ -1697,39 +1697,39 @@ limit │ │ │ ├── key: (4) │ │ │ ├── fd: ()-->(107), (4)-->(101,103,104) │ │ │ ├── select - │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102 rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 + │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102!null rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null │ │ │ │ ├── immutable │ │ │ │ ├── key: (4) │ │ │ │ ├── fd: (4)-->(101-104), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ ├── window partition=() ordering=+(102|106) - │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102 rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 + │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102!null rank:103 rank:104 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null │ │ │ │ │ ├── immutable │ │ │ │ │ ├── key: (4) │ │ │ │ │ ├── fd: (4)-->(101-104), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ │ ├── window partition=() ordering=+(101|105) - │ │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102 rank:103 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 + │ │ │ │ │ │ ├── columns: ws_item_sk:4!null return_ratio:101 currency_ratio:102!null rank:103 rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ ├── fd: (4)-->(101-103), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:105 rank_2_orderby_1_1:106 ws_item_sk:4!null return_ratio:101 currency_ratio:102 + │ │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:105 rank_2_orderby_1_1:106!null ws_item_sk:4!null return_ratio:101 currency_ratio:102!null │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ │ ├── fd: (4)-->(101,102), (101)==(105), (105)==(101), (102)==(106), (106)==(102) │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ ├── columns: return_ratio:101 currency_ratio:102 ws_item_sk:4!null + │ │ │ │ │ │ │ │ ├── columns: return_ratio:101 currency_ratio:102!null ws_item_sk:4!null │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ │ │ ├── fd: (4)-->(101,102) │ │ │ │ │ │ │ │ ├── group-by (streaming) - │ │ │ │ │ │ │ │ │ ├── columns: ws_item_sk:4!null sum:94 sum:96 sum:98 sum:100 + │ │ │ │ │ │ │ │ │ ├── columns: ws_item_sk:4!null sum:94 sum:96!null sum:98!null sum:100!null │ │ │ │ │ │ │ │ │ ├── grouping columns: ws_item_sk:4!null │ │ │ │ │ │ │ │ │ ├── internal-ordering: +4 │ │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ │ ├── key: (4) │ │ │ │ │ │ │ │ │ ├── fd: (4)-->(94,96,98,100) │ │ │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ │ │ ├── columns: column93:93 column95:95 column97:97 column99:99 ws_item_sk:4!null + │ │ │ │ │ │ │ │ │ │ ├── columns: column93:93 column95:95!null column97:97!null column99:99!null ws_item_sk:4!null │ │ │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ │ │ ├── ordering: +4 │ │ │ │ │ │ │ │ │ │ ├── inner-join (lookup date_dim) @@ -1770,9 +1770,9 @@ limit │ │ │ │ │ │ │ │ │ │ │ └── d_moy:71 = 11 [outer=(71), constraints=(/71: [/11 - /11]; tight), fd=()-->(71)] │ │ │ │ │ │ │ │ │ │ └── projections │ │ │ │ │ │ │ │ │ │ ├── COALESCE(wr_return_quantity:51, 0) [as=column93:93, outer=(51)] - │ │ │ │ │ │ │ │ │ │ ├── COALESCE(ws_quantity:19, 0) [as=column95:95, outer=(19)] - │ │ │ │ │ │ │ │ │ │ ├── COALESCE(wr_return_amt:52::DECIMAL, 0) [as=column97:97, outer=(52), immutable] - │ │ │ │ │ │ │ │ │ │ └── COALESCE(ws_net_paid:30::DECIMAL, 0) [as=column99:99, outer=(30), immutable] + │ │ │ │ │ │ │ │ │ │ ├── ws_quantity:19 [as=column95:95, outer=(19)] + │ │ │ │ │ │ │ │ │ │ ├── wr_return_amt:52::DECIMAL [as=column97:97, outer=(52), immutable] + │ │ │ │ │ │ │ │ │ │ └── ws_net_paid:30::DECIMAL [as=column99:99, outer=(30), immutable] │ │ │ │ │ │ │ │ │ └── aggregations │ │ │ │ │ │ │ │ │ ├── sum [as=sum:94, outer=(93)] │ │ │ │ │ │ │ │ │ │ └── column93:93 @@ -1809,39 +1809,39 @@ limit │ │ ├── key: (123) │ │ ├── fd: ()-->(217), (123)-->(211,213,214) │ │ ├── select - │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212 rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 + │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212!null rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null │ │ │ ├── immutable │ │ │ ├── key: (123) │ │ │ ├── fd: (123)-->(211-214), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ ├── window partition=() ordering=+(212|216) - │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212 rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 + │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212!null rank:213 rank:214 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null │ │ │ │ ├── immutable │ │ │ │ ├── key: (123) │ │ │ │ ├── fd: (123)-->(211-214), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ │ ├── window partition=() ordering=+(211|215) - │ │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212 rank:213 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 + │ │ │ │ │ ├── columns: cs_item_sk:123!null return_ratio:211 currency_ratio:212!null rank:213 rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null │ │ │ │ │ ├── immutable │ │ │ │ │ ├── key: (123) │ │ │ │ │ ├── fd: (123)-->(211-213), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ │ │ ├── project - │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:215 rank_2_orderby_1_1:216 cs_item_sk:123!null return_ratio:211 currency_ratio:212 + │ │ │ │ │ │ ├── columns: rank_1_orderby_1_1:215 rank_2_orderby_1_1:216!null cs_item_sk:123!null return_ratio:211 currency_ratio:212!null │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ ├── key: (123) │ │ │ │ │ │ ├── fd: (123)-->(211,212), (211)==(215), (215)==(211), (212)==(216), (216)==(212) │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ ├── columns: return_ratio:211 currency_ratio:212 cs_item_sk:123!null + │ │ │ │ │ │ │ ├── columns: return_ratio:211 currency_ratio:212!null cs_item_sk:123!null │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ ├── key: (123) │ │ │ │ │ │ │ ├── fd: (123)-->(211,212) │ │ │ │ │ │ │ ├── group-by (streaming) - │ │ │ │ │ │ │ │ ├── columns: cs_item_sk:123!null sum:204 sum:206 sum:208 sum:210 + │ │ │ │ │ │ │ │ ├── columns: cs_item_sk:123!null sum:204 sum:206!null sum:208!null sum:210!null │ │ │ │ │ │ │ │ ├── grouping columns: cs_item_sk:123!null │ │ │ │ │ │ │ │ ├── internal-ordering: +123 │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ ├── key: (123) │ │ │ │ │ │ │ │ ├── fd: (123)-->(204,206,208,210) │ │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ │ ├── columns: column203:203 column205:205 column207:207 column209:209 cs_item_sk:123!null + │ │ │ │ │ │ │ │ │ ├── columns: column203:203 column205:205!null column207:207!null column209:209!null cs_item_sk:123!null │ │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ │ ├── ordering: +123 │ │ │ │ │ │ │ │ │ ├── inner-join (lookup date_dim) @@ -1882,9 +1882,9 @@ limit │ │ │ │ │ │ │ │ │ │ └── d_moy:181 = 11 [outer=(181), constraints=(/181: [/11 - /11]; tight), fd=()-->(181)] │ │ │ │ │ │ │ │ │ └── projections │ │ │ │ │ │ │ │ │ ├── COALESCE(cr_return_quantity:161, 0) [as=column203:203, outer=(161)] - │ │ │ │ │ │ │ │ │ ├── COALESCE(cs_quantity:126, 0) [as=column205:205, outer=(126)] - │ │ │ │ │ │ │ │ │ ├── COALESCE(cr_return_amount:162::DECIMAL, 0) [as=column207:207, outer=(162), immutable] - │ │ │ │ │ │ │ │ │ └── COALESCE(cs_net_paid:137::DECIMAL, 0) [as=column209:209, outer=(137), immutable] + │ │ │ │ │ │ │ │ │ ├── cs_quantity:126 [as=column205:205, outer=(126)] + │ │ │ │ │ │ │ │ │ ├── cr_return_amount:162::DECIMAL [as=column207:207, outer=(162), immutable] + │ │ │ │ │ │ │ │ │ └── cs_net_paid:137::DECIMAL [as=column209:209, outer=(137), immutable] │ │ │ │ │ │ │ │ └── aggregations │ │ │ │ │ │ │ │ ├── sum [as=sum:204, outer=(203)] │ │ │ │ │ │ │ │ │ └── column203:203 @@ -1921,39 +1921,39 @@ limit │ ├── key: (225) │ ├── fd: ()-->(314), (225)-->(308,310,311) │ ├── select - │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309 rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 + │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309!null rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null │ │ ├── immutable │ │ ├── key: (225) │ │ ├── fd: (225)-->(308-311), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ ├── window partition=() ordering=+(309|313) - │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309 rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 + │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309!null rank:310 rank:311 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null │ │ │ ├── immutable │ │ │ ├── key: (225) │ │ │ ├── fd: (225)-->(308-311), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ │ ├── window partition=() ordering=+(308|312) - │ │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309 rank:310 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 + │ │ │ │ ├── columns: ss_item_sk:225!null return_ratio:308 currency_ratio:309!null rank:310 rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null │ │ │ │ ├── immutable │ │ │ │ ├── key: (225) │ │ │ │ ├── fd: (225)-->(308-310), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ │ │ ├── project - │ │ │ │ │ ├── columns: rank_1_orderby_1_1:312 rank_2_orderby_1_1:313 ss_item_sk:225!null return_ratio:308 currency_ratio:309 + │ │ │ │ │ ├── columns: rank_1_orderby_1_1:312 rank_2_orderby_1_1:313!null ss_item_sk:225!null return_ratio:308 currency_ratio:309!null │ │ │ │ │ ├── immutable │ │ │ │ │ ├── key: (225) │ │ │ │ │ ├── fd: (225)-->(308,309), (308)==(312), (312)==(308), (309)==(313), (313)==(309) │ │ │ │ │ ├── project - │ │ │ │ │ │ ├── columns: return_ratio:308 currency_ratio:309 ss_item_sk:225!null + │ │ │ │ │ │ ├── columns: return_ratio:308 currency_ratio:309!null ss_item_sk:225!null │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ ├── key: (225) │ │ │ │ │ │ ├── fd: (225)-->(308,309) │ │ │ │ │ │ ├── group-by (streaming) - │ │ │ │ │ │ │ ├── columns: ss_item_sk:225!null sum:301 sum:303 sum:305 sum:307 + │ │ │ │ │ │ │ ├── columns: ss_item_sk:225!null sum:301 sum:303!null sum:305!null sum:307!null │ │ │ │ │ │ │ ├── grouping columns: ss_item_sk:225!null │ │ │ │ │ │ │ ├── internal-ordering: +225 │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ ├── key: (225) │ │ │ │ │ │ │ ├── fd: (225)-->(301,303,305,307) │ │ │ │ │ │ │ ├── project - │ │ │ │ │ │ │ │ ├── columns: column300:300 column302:302 column304:304 column306:306 ss_item_sk:225!null + │ │ │ │ │ │ │ │ ├── columns: column300:300 column302:302!null column304:304!null column306:306!null ss_item_sk:225!null │ │ │ │ │ │ │ │ ├── immutable │ │ │ │ │ │ │ │ ├── ordering: +225 │ │ │ │ │ │ │ │ ├── inner-join (lookup date_dim) @@ -2006,9 +2006,9 @@ limit │ │ │ │ │ │ │ │ │ └── d_moy:278 = 11 [outer=(278), constraints=(/278: [/11 - /11]; tight), fd=()-->(278)] │ │ │ │ │ │ │ │ └── projections │ │ │ │ │ │ │ │ ├── COALESCE(sr_return_quantity:258, 0) [as=column300:300, outer=(258)] - │ │ │ │ │ │ │ │ ├── COALESCE(ss_quantity:233, 0) [as=column302:302, outer=(233)] - │ │ │ │ │ │ │ │ ├── COALESCE(sr_return_amt:259::DECIMAL, 0) [as=column304:304, outer=(259), immutable] - │ │ │ │ │ │ │ │ └── COALESCE(ss_net_paid:243::DECIMAL, 0) [as=column306:306, outer=(243), immutable] + │ │ │ │ │ │ │ │ ├── ss_quantity:233 [as=column302:302, outer=(233)] + │ │ │ │ │ │ │ │ ├── sr_return_amt:259::DECIMAL [as=column304:304, outer=(259), immutable] + │ │ │ │ │ │ │ │ └── ss_net_paid:243::DECIMAL [as=column306:306, outer=(243), immutable] │ │ │ │ │ │ │ └── aggregations │ │ │ │ │ │ │ ├── sum [as=sum:301, outer=(300)] │ │ │ │ │ │ │ │ └── column300:300 From 44e6ad9d134d7a75f078cf9872f81c6f4e90b924 Mon Sep 17 00:00:00 2001 From: Lohit Kolluri Date: Tue, 23 Jun 2026 05:03:24 +0530 Subject: [PATCH 3/4] opt: use filter-derived NOT NULL columns to simplify COALESCE in Select filters Previously, SimplifyCoalesceSelect only considered NOT NULL columns inferred from the input expression (e.g., Join conditions). This meant COALESCE(b, c) could not be simplified to b in a query like: SELECT * FROM d WHERE b > 0 AND COALESCE(b, c) = 1 even though b > 0 null-rejects column b. Now compute NOT NULL columns from all other filter items' constraints and feed them into COALESCE simplification. Also refactors CanSimplifyCoalesceInScalar from Replace-based recursion to a direct recursive walk with proper ScalarExpr type assertion (fixes a panic when encountering subquery relational children). Epic: none Signed-off-by: Lohit Kolluri --- pkg/sql/opt/norm/rules/select.opt | 2 +- pkg/sql/opt/norm/scalar_funcs.go | 29 ++++++++-------- pkg/sql/opt/norm/select_funcs.go | 46 +++++++++++++++++++++++--- pkg/sql/opt/norm/testdata/rules/select | 34 +++++++++++++++++++ 4 files changed, 93 insertions(+), 18 deletions(-) diff --git a/pkg/sql/opt/norm/rules/select.opt b/pkg/sql/opt/norm/rules/select.opt index 37c31945a515..13d9c9736630 100644 --- a/pkg/sql/opt/norm/rules/select.opt +++ b/pkg/sql/opt/norm/rules/select.opt @@ -40,7 +40,7 @@ $filters:[ ... (FiltersItem $cond:*) & - (CanSimplifyCoalesceInScalar $cond (NotNullCols $input)) + (CanSimplifyCoalesceInFilters $filters $cond (NotNullCols $input)) ... ] ) diff --git a/pkg/sql/opt/norm/scalar_funcs.go b/pkg/sql/opt/norm/scalar_funcs.go index 8cdc745890f5..65c03f95dea4 100644 --- a/pkg/sql/opt/norm/scalar_funcs.go +++ b/pkg/sql/opt/norm/scalar_funcs.go @@ -156,22 +156,25 @@ func (c *CustomFuncs) SimplifyCoalesceInScalar( // recurses into Coalesce args so that nested Coalesce expressions are not // missed when the outer Coalesce is not itself simplifiable. func (c *CustomFuncs) CanSimplifyCoalesceInScalar(e opt.ScalarExpr, notNullCols opt.ColSet) bool { - found := false - var replace ReplaceFunc - replace = func(e opt.Expr) opt.Expr { - if co, ok := e.(*memo.CoalesceExpr); ok { - if c.CanSimplifyCoalesce(co.Args, notNullCols) { - found = true - return co + if co, ok := e.(*memo.CoalesceExpr); ok { + if c.CanSimplifyCoalesce(co.Args, notNullCols) { + return true + } + for _, arg := range co.Args { + if c.CanSimplifyCoalesceInScalar(arg, notNullCols) { + return true + } + } + return false + } + for i, n := 0, e.ChildCount(); i < n; i++ { + if sc, ok := e.Child(i).(opt.ScalarExpr); ok { + if c.CanSimplifyCoalesceInScalar(sc, notNullCols) { + return true } - // Even if the outer Coalesce is not simplifiable, recurse into its - // args to find simplifiable nested Coalesce expressions. - return c.f.Replace(co, replace) } - return c.f.Replace(e, replace) } - replace(e) - return found + return false } // IsConstValueEqual returns whether const1 and const2 are equal. diff --git a/pkg/sql/opt/norm/select_funcs.go b/pkg/sql/opt/norm/select_funcs.go index dd5859bcd477..50e9d4a295e4 100644 --- a/pkg/sql/opt/norm/select_funcs.go +++ b/pkg/sql/opt/norm/select_funcs.go @@ -420,18 +420,56 @@ func (c *CustomFuncs) ForDuplicateRemoval(private *memo.OrdinalityPrivate) (ok b return private.ForDuplicateRemoval } +// CanSimplifyCoalesceInFilters returns true if any filter condition contains a +// Coalesce expression that can be simplified using the input's NOT NULL columns +// plus null-rejecting columns from other filter conditions in the same set. +func (c *CustomFuncs) CanSimplifyCoalesceInFilters( + filters memo.FiltersExpr, _ opt.ScalarExpr, notNullCols opt.ColSet, +) bool { + filterNotNullCols := make([]opt.ColSet, len(filters)) + for i := range filters { + constraints := filters[i].ScalarProps().Constraints + if constraints != nil { + constraints.ExtractNotNullCols(c.f.ctx, c.f.evalCtx, &filterNotNullCols[i]) + } + } + totalFilterNotNullCols := opt.ColSet{} + for _, fc := range filterNotNullCols { + totalFilterNotNullCols = totalFilterNotNullCols.Union(fc) + } + for i := range filters { + enriched := notNullCols.Union(totalFilterNotNullCols.Difference(filterNotNullCols[i])) + if c.CanSimplifyCoalesceInScalar(filters[i].Condition, enriched) { + return true + } + } + return false +} + // SimplifyCoalesceInFilters simplifies Coalesce expressions in filter -// conditions using the given not-null columns. Filters whose condition is -// unchanged are reused as-is to avoid unnecessary memo invalidation. +// conditions using the given not-null columns, including null-rejecting columns +// derived from other filter conditions. Filters whose condition is unchanged +// are reused as-is to avoid unnecessary memo invalidation. func (c *CustomFuncs) SimplifyCoalesceInFilters( filters memo.FiltersExpr, notNullCols opt.ColSet, ) memo.FiltersExpr { + filterNotNullCols := make([]opt.ColSet, len(filters)) + for i := range filters { + constraints := filters[i].ScalarProps().Constraints + if constraints != nil { + constraints.ExtractNotNullCols(c.f.ctx, c.f.evalCtx, &filterNotNullCols[i]) + } + } + totalFilterNotNullCols := opt.ColSet{} + for _, fc := range filterNotNullCols { + totalFilterNotNullCols = totalFilterNotNullCols.Union(fc) + } newFilters := make(memo.FiltersExpr, len(filters)) for i := range filters { f := &filters[i] - simplified := c.SimplifyCoalesceInScalar(f.Condition, notNullCols) + enriched := notNullCols.Union(totalFilterNotNullCols.Difference(filterNotNullCols[i])) + simplified := c.SimplifyCoalesceInScalar(f.Condition, enriched) if simplified == f.Condition { - // No change; reuse the original FiltersItem. newFilters[i] = *f } else { newFilters[i] = c.f.ConstructFiltersItem(simplified) diff --git a/pkg/sql/opt/norm/testdata/rules/select b/pkg/sql/opt/norm/testdata/rules/select index 23c5c81f1b06..81afbc369524 100644 --- a/pkg/sql/opt/norm/testdata/rules/select +++ b/pkg/sql/opt/norm/testdata/rules/select @@ -2009,6 +2009,40 @@ select └── filters └── COALESCE(b:3, a:2) = 1 [outer=(2,3)] +# Filter-derived NOT NULL: the condition b > 0 null-rejects b, so +# COALESCE(b, c) simplifies to b even though b is nullable in the schema. +# The b > 0 filter is then elided as redundant since b = 1 implies b > 0. +norm expect=SimplifyCoalesceSelect +SELECT * FROM d WHERE b > 0 AND COALESCE(b, c) = 1 +---- +select + ├── columns: k:1!null a:2!null b:3!null c:4 d:5 + ├── key: (1) + ├── fd: ()-->(3), (1)-->(2,4,5) + ├── scan d + │ ├── columns: k:1!null a:2!null b:3 c:4 d:5 + │ ├── key: (1) + │ └── fd: (1)-->(2-5) + └── filters + └── b:3 = 1 [outer=(3), constraints=(/3: [/1 - /1]; tight), fd=()-->(3)] + +# Nested COALESCE with filter-derived NOT NULL: c > 0 null-rejects c, so the +# outer COALESCE(c, COALESCE(b, c)) simplifies to c. The c > 0 filter is then +# elided as redundant since c = 1 implies c > 0. +norm expect=SimplifyCoalesceSelect +SELECT * FROM d WHERE c > 0 AND COALESCE(c, COALESCE(b, c)) = 1 +---- +select + ├── columns: k:1!null a:2!null b:3 c:4!null d:5 + ├── key: (1) + ├── fd: ()-->(4), (1)-->(2,3,5) + ├── scan d + │ ├── columns: k:1!null a:2!null b:3 c:4 d:5 + │ ├── key: (1) + │ └── fd: (1)-->(2-5) + └── filters + └── c:4 = 1 [outer=(4), constraints=(/4: [/1 - /1]; tight), fd=()-->(4)] + # -------------------------------------------------- # EliminateCaseTrailingFalsyBranch # -------------------------------------------------- From 051e07143d2626d0dfefb602735fad012db2d387 Mon Sep 17 00:00:00 2001 From: Lohit Kolluri Date: Tue, 23 Jun 2026 05:22:12 +0530 Subject: [PATCH 4/4] opt: extract computeFilterNotNullCols helper and optimize NOT NULL derivation Extract the duplicate filter-derived NOT NULL computation into computeFilterNotNullCols, used by both CanSimplifyCoalesceInFilters and SimplifyCoalesceInFilters. Optimize the enriched NOT NULL set computation to avoid ColSet::Union when the cross-filter difference is empty. Update rule and test comments for accuracy. Epic: none Signed-off-by: Lohit Kolluri --- pkg/sql/opt/norm/rules/select.opt | 3 +- pkg/sql/opt/norm/select_funcs.go | 57 +++++++++++++++----------- pkg/sql/opt/norm/testdata/rules/select | 6 +-- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/pkg/sql/opt/norm/rules/select.opt b/pkg/sql/opt/norm/rules/select.opt index 13d9c9736630..b13bdc93917c 100644 --- a/pkg/sql/opt/norm/rules/select.opt +++ b/pkg/sql/opt/norm/rules/select.opt @@ -33,7 +33,8 @@ # SimplifyCoalesceSelect simplifies Coalesce expressions in Select filters by # eliminating leading operands that are guaranteed to be non-null according to -# the input's relational properties. +# the input's relational properties and null-rejecting columns from sibling +# filter conditions. [SimplifyCoalesceSelect, Normalize, LowPriority] (Select $input:* diff --git a/pkg/sql/opt/norm/select_funcs.go b/pkg/sql/opt/norm/select_funcs.go index 50e9d4a295e4..b0a6255b6679 100644 --- a/pkg/sql/opt/norm/select_funcs.go +++ b/pkg/sql/opt/norm/select_funcs.go @@ -420,25 +420,41 @@ func (c *CustomFuncs) ForDuplicateRemoval(private *memo.OrdinalityPrivate) (ok b return private.ForDuplicateRemoval } +// computeFilterNotNullCols returns per-filter NOT NULL column sets and their +// total union for the given filters. It extracts null-rejecting columns from +// each filter item's constraints. Used by both CanSimplifyCoalesceInFilters and +// SimplifyCoalesceInFilters to avoid duplicating constraint extraction. +func computeFilterNotNullCols( + c *CustomFuncs, filters memo.FiltersExpr, +) (perFilter []opt.ColSet, total opt.ColSet) { + perFilter = make([]opt.ColSet, len(filters)) + for i := range filters { + constraints := filters[i].ScalarProps().Constraints + if constraints != nil { + constraints.ExtractNotNullCols(c.f.ctx, c.f.evalCtx, &perFilter[i]) + } + } + for _, fc := range perFilter { + total = total.Union(fc) + } + return +} + // CanSimplifyCoalesceInFilters returns true if any filter condition contains a // Coalesce expression that can be simplified using the input's NOT NULL columns // plus null-rejecting columns from other filter conditions in the same set. +// The _ opt.ScalarExpr parameter is unused; it is required by the optgen rule +// which binds $cond but this function works with the full $filters set. func (c *CustomFuncs) CanSimplifyCoalesceInFilters( filters memo.FiltersExpr, _ opt.ScalarExpr, notNullCols opt.ColSet, ) bool { - filterNotNullCols := make([]opt.ColSet, len(filters)) + perFilter, total := computeFilterNotNullCols(c, filters) for i := range filters { - constraints := filters[i].ScalarProps().Constraints - if constraints != nil { - constraints.ExtractNotNullCols(c.f.ctx, c.f.evalCtx, &filterNotNullCols[i]) + diff := total.Difference(perFilter[i]) + enriched := notNullCols + if !diff.Empty() { + enriched = notNullCols.Union(diff) } - } - totalFilterNotNullCols := opt.ColSet{} - for _, fc := range filterNotNullCols { - totalFilterNotNullCols = totalFilterNotNullCols.Union(fc) - } - for i := range filters { - enriched := notNullCols.Union(totalFilterNotNullCols.Difference(filterNotNullCols[i])) if c.CanSimplifyCoalesceInScalar(filters[i].Condition, enriched) { return true } @@ -453,21 +469,15 @@ func (c *CustomFuncs) CanSimplifyCoalesceInFilters( func (c *CustomFuncs) SimplifyCoalesceInFilters( filters memo.FiltersExpr, notNullCols opt.ColSet, ) memo.FiltersExpr { - filterNotNullCols := make([]opt.ColSet, len(filters)) - for i := range filters { - constraints := filters[i].ScalarProps().Constraints - if constraints != nil { - constraints.ExtractNotNullCols(c.f.ctx, c.f.evalCtx, &filterNotNullCols[i]) - } - } - totalFilterNotNullCols := opt.ColSet{} - for _, fc := range filterNotNullCols { - totalFilterNotNullCols = totalFilterNotNullCols.Union(fc) - } + perFilter, total := computeFilterNotNullCols(c, filters) newFilters := make(memo.FiltersExpr, len(filters)) for i := range filters { f := &filters[i] - enriched := notNullCols.Union(totalFilterNotNullCols.Difference(filterNotNullCols[i])) + diff := total.Difference(perFilter[i]) + enriched := notNullCols + if !diff.Empty() { + enriched = notNullCols.Union(diff) + } simplified := c.SimplifyCoalesceInScalar(f.Condition, enriched) if simplified == f.Condition { newFilters[i] = *f @@ -477,4 +487,3 @@ func (c *CustomFuncs) SimplifyCoalesceInFilters( } return newFilters } - diff --git a/pkg/sql/opt/norm/testdata/rules/select b/pkg/sql/opt/norm/testdata/rules/select index 81afbc369524..72c5d3b950ba 100644 --- a/pkg/sql/opt/norm/testdata/rules/select +++ b/pkg/sql/opt/norm/testdata/rules/select @@ -2011,7 +2011,7 @@ select # Filter-derived NOT NULL: the condition b > 0 null-rejects b, so # COALESCE(b, c) simplifies to b even though b is nullable in the schema. -# The b > 0 filter is then elided as redundant since b = 1 implies b > 0. +# The redundant b > 0 filter is elided by existing normalization (b = 1 implies b > 0). norm expect=SimplifyCoalesceSelect SELECT * FROM d WHERE b > 0 AND COALESCE(b, c) = 1 ---- @@ -2027,8 +2027,8 @@ select └── b:3 = 1 [outer=(3), constraints=(/3: [/1 - /1]; tight), fd=()-->(3)] # Nested COALESCE with filter-derived NOT NULL: c > 0 null-rejects c, so the -# outer COALESCE(c, COALESCE(b, c)) simplifies to c. The c > 0 filter is then -# elided as redundant since c = 1 implies c > 0. +# outer COALESCE(c, COALESCE(b, c)) simplifies to c. The redundant c > 0 filter +# is elided by existing normalization (c = 1 implies c > 0). norm expect=SimplifyCoalesceSelect SELECT * FROM d WHERE c > 0 AND COALESCE(c, COALESCE(b, c)) = 1 ----