Skip to content

Commit eff22df

Browse files
committed
opt: drop provably-non-null COALESCE operands
When a leading COALESCE argument is known non-null from the input's NotNullCols, later arguments aren't needed. Uses NotNullCols to trim leading COALESCE args in Project and Select. Example: `SELECT COALESCE(i, j) FROM ij` with `i NOT NULL` no longer keeps the coalesce in the plan. Implementation details: - Add SimplifyCoalesceProject and SimplifyCoalesceSelect norm rules that call SimplifyCoalesceInScalar on each projection/filter using the input's not-null columns. - Introduce scalarContainsSimplifiableCoalesce (in scalar_funcs.go) as the match guard; it walks the full scalar tree so that nested Coalesce expressions like COALESCE(a, COALESCE(b, c)) are not missed when only the inner Coalesce is simplifiable. - CanSimplifyCoalesce mirrors simplifyCoalesce's logic with a direct scan, avoiding expression construction on the hot path. The final return is true (not false) because when the loop exhausts all-null constant args, simplifyCoalesce returns args[last] unwrapped, which is a genuine simplification. - SimplifyCoalesceInScalar recurses into the simplified result via c.f.Replace so nested Coalesces are handled in a single pass. - The private simplifyCoalesce helper iterates over all args (including the last) so ExprIsNeverNull can fire at any position. Test plan: `bazel test //pkg/sql/opt/norm:norm_test --test_filter=TestNormRules` Fixes #103596 Release note: None
1 parent b852382 commit eff22df

10 files changed

Lines changed: 415 additions & 71 deletions

File tree

pkg/sql/opt/norm/project_funcs.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,3 +1008,34 @@ func (c *CustomFuncs) HasVolatileProjection(projections memo.ProjectionsExpr) bo
10081008
}
10091009
return false
10101010
}
1011+
1012+
// CanSimplifyCoalesceInProjections returns true if any projection contains a
1013+
// Coalesce expression that can be simplified using the input's not-null columns.
1014+
func (c *CustomFuncs) CanSimplifyCoalesceInProjections(
1015+
projections memo.ProjectionsExpr, input memo.RelExpr,
1016+
) bool {
1017+
notNullCols := c.NotNullCols(input)
1018+
for i := range projections {
1019+
if c.scalarContainsSimplifiableCoalesce(projections[i].Element, notNullCols) {
1020+
return true
1021+
}
1022+
}
1023+
return false
1024+
}
1025+
1026+
// SimplifyCoalesceInProjections simplifies Coalesce expressions in projections
1027+
// using the input's not-null columns.
1028+
func (c *CustomFuncs) SimplifyCoalesceInProjections(
1029+
projections memo.ProjectionsExpr, input memo.RelExpr,
1030+
) memo.ProjectionsExpr {
1031+
notNullCols := c.NotNullCols(input)
1032+
newProjections := make(memo.ProjectionsExpr, len(projections))
1033+
for i := range projections {
1034+
p := &projections[i]
1035+
newProjections[i] = c.f.ConstructProjectionsItem(
1036+
c.SimplifyCoalesceInScalar(p.Element, notNullCols),
1037+
p.Col,
1038+
)
1039+
}
1040+
return newProjections
1041+
}

pkg/sql/opt/norm/rules/project.opt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,23 @@ $input
331331
$passthrough
332332
)
333333

334+
# SimplifyCoalesceProject simplifies Coalesce projections by eliminating leading
335+
# operands that are guaranteed to be non-null according to the input's
336+
# relational properties.
337+
[SimplifyCoalesceProject, Normalize, LowPriority]
338+
(Project
339+
$input:*
340+
$projections:* &
341+
(CanSimplifyCoalesceInProjections $projections $input)
342+
$passthrough:*
343+
)
344+
=>
345+
(Project
346+
$input
347+
(SimplifyCoalesceInProjections $projections $input)
348+
$passthrough
349+
)
350+
334351
# FoldIsNullProject folds "x IS NULL" projections to false if "x" is not null in
335352
# the Project's input. It matches if there is at least one projection that can
336353
# be folded, and it replaces all projections that can be folded.

pkg/sql/opt/norm/rules/select.opt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
=>
3232
(Select $input (SimplifyFilters $filters))
3333

34+
# SimplifyCoalesceSelect simplifies Coalesce expressions in Select filters by
35+
# eliminating leading operands that are guaranteed to be non-null according to
36+
# the input's relational properties.
37+
[SimplifyCoalesceSelect, Normalize, LowPriority]
38+
(Select
39+
$input:*
40+
$filters:* & (CanSimplifyCoalesceInFilters $filters $input)
41+
)
42+
=>
43+
(Select $input (SimplifyCoalesceInFilters $filters $input))
44+
3445
# ConsolidateSelectFilters consolidates filters that constrain a single
3546
# variable. For example, filters x >= 5 and x <= 10 would be combined into a
3647
# single Range operation.

pkg/sql/opt/norm/scalar_funcs.go

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,25 +74,109 @@ func (c *CustomFuncs) ConstructSortedUniqueList(
7474
// SimplifyCoalesce discards any leading null operands, and then if the next
7575
// operand is a constant, replaces with that constant.
7676
func (c *CustomFuncs) SimplifyCoalesce(args memo.ScalarListExpr) opt.ScalarExpr {
77-
for i := 0; i < len(args)-1; i++ {
78-
item := args[i]
77+
return c.simplifyCoalesce(args, opt.ColSet{})
78+
}
79+
80+
func (c *CustomFuncs) simplifyCoalesce(
81+
args memo.ScalarListExpr, notNullCols opt.ColSet,
82+
) opt.ScalarExpr {
83+
// Iterate over all args (including the last) so that ExprIsNeverNull can
84+
// fire for any position: once a provably-non-null arg is found, COALESCE
85+
// will always return that arg, so we can replace the whole expression.
86+
for i := 0; i < len(args); i++ {
87+
if memo.ExprIsNeverNull(args[i], notNullCols) {
88+
return args[i]
89+
}
7990

80-
// If item is not a constant value, then its value may turn out to be
81-
// null, so no more folding. Return operands from then on.
82-
if !c.IsConstValueOrGroupOfConstValues(item) {
91+
if !c.IsConstValueOrGroupOfConstValues(args[i]) {
92+
if i >= len(args)-1 {
93+
return args[i]
94+
}
8395
return c.f.ConstructCoalesce(args[i:])
8496
}
8597

86-
if item.Op() != opt.NullOp {
87-
return item
98+
if args[i].Op() != opt.NullOp {
99+
return args[i]
88100
}
89101
}
90102

91-
// All operands up to the last were null (or the last is the only operand),
92-
// so return the last operand without the wrapping COALESCE function.
93103
return args[len(args)-1]
94104
}
95105

106+
// CanSimplifyCoalesce returns true if simplifyCoalesce would change the given
107+
// Coalesce expression. It mirrors the logic of simplifyCoalesce directly to
108+
// avoid the cost of constructing a new expression just to compare arg counts.
109+
func (c *CustomFuncs) CanSimplifyCoalesce(args memo.ScalarListExpr, notNullCols opt.ColSet) bool {
110+
for i, arg := range args {
111+
if memo.ExprIsNeverNull(arg, notNullCols) {
112+
return true
113+
}
114+
if !c.IsConstValueOrGroupOfConstValues(arg) {
115+
// Non-constant arg at position i blocks further simplification. The
116+
// expression changes only if leading nulls (i > 0) were already
117+
// stripped.
118+
return i > 0
119+
}
120+
if arg.Op() != opt.NullOp {
121+
// Non-null constant: simplifyCoalesce returns it directly.
122+
return true
123+
}
124+
// NullOp constant: will be stripped; continue to next arg.
125+
}
126+
// The loop exhausted all args, meaning every arg was a null constant.
127+
// simplifyCoalesce returns args[last] directly (unwrapping the COALESCE),
128+
// which is a simplification whenever there is more than one arg. The
129+
// single-arg case is already handled by EliminateCoalesce before this
130+
// function is called, so we can safely return true here.
131+
return true
132+
}
133+
134+
// SimplifyCoalesceInScalar recursively simplifies Coalesce expressions in the
135+
// given scalar expression tree using the provided not-null columns. It handles
136+
// nested Coalesce expressions by recursing into the simplified result.
137+
func (c *CustomFuncs) SimplifyCoalesceInScalar(
138+
e opt.ScalarExpr, notNullCols opt.ColSet,
139+
) opt.ScalarExpr {
140+
var replace ReplaceFunc
141+
replace = func(e opt.Expr) opt.Expr {
142+
if co, ok := e.(*memo.CoalesceExpr); ok {
143+
simplified := c.simplifyCoalesce(co.Args, notNullCols)
144+
// Recurse into the simplified result so that nested Coalesce
145+
// expressions (e.g. COALESCE(a, COALESCE(b, c))) are also handled
146+
// when the outer simplification does not fire.
147+
return c.f.Replace(simplified, replace)
148+
}
149+
return c.f.Replace(e, replace)
150+
}
151+
return replace(e).(opt.ScalarExpr)
152+
}
153+
154+
// scalarContainsSimplifiableCoalesce returns true if the scalar expression
155+
// tree contains any Coalesce expression that can be simplified using
156+
// notNullCols. It recurses into Coalesce args so that nested Coalesce
157+
// expressions are not missed when the outer Coalesce is not itself
158+
// simplifiable.
159+
func (c *CustomFuncs) scalarContainsSimplifiableCoalesce(
160+
e opt.ScalarExpr, notNullCols opt.ColSet,
161+
) bool {
162+
found := false
163+
var replace ReplaceFunc
164+
replace = func(e opt.Expr) opt.Expr {
165+
if co, ok := e.(*memo.CoalesceExpr); ok {
166+
if c.CanSimplifyCoalesce(co.Args, notNullCols) {
167+
found = true
168+
return co
169+
}
170+
// Even if the outer Coalesce is not simplifiable, recurse into its
171+
// args to find simplifiable nested Coalesce expressions.
172+
return c.f.Replace(co, replace)
173+
}
174+
return c.f.Replace(e, replace)
175+
}
176+
replace(e)
177+
return found
178+
}
179+
96180
// IsConstValueEqual returns whether const1 and const2 are equal.
97181
func (c *CustomFuncs) IsConstValueEqual(const1, const2 opt.ScalarExpr) bool {
98182
op1 := const1.Op()

pkg/sql/opt/norm/select_funcs.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,33 @@ func (c *CustomFuncs) addConjuncts(
419419
func (c *CustomFuncs) ForDuplicateRemoval(private *memo.OrdinalityPrivate) (ok bool) {
420420
return private.ForDuplicateRemoval
421421
}
422+
423+
// CanSimplifyCoalesceInFilters returns true if any filter condition contains a
424+
// Coalesce expression that can be simplified using the input's not-null columns.
425+
func (c *CustomFuncs) CanSimplifyCoalesceInFilters(
426+
filters memo.FiltersExpr, input memo.RelExpr,
427+
) bool {
428+
notNullCols := c.NotNullCols(input)
429+
for i := range filters {
430+
if c.scalarContainsSimplifiableCoalesce(filters[i].Condition, notNullCols) {
431+
return true
432+
}
433+
}
434+
return false
435+
}
436+
437+
// SimplifyCoalesceInFilters simplifies Coalesce expressions in filter conditions
438+
// using the input's not-null columns.
439+
func (c *CustomFuncs) SimplifyCoalesceInFilters(
440+
filters memo.FiltersExpr, input memo.RelExpr,
441+
) memo.FiltersExpr {
442+
notNullCols := c.NotNullCols(input)
443+
newFilters := make(memo.FiltersExpr, len(filters))
444+
for i := range filters {
445+
newFilters[i] = c.f.ConstructFiltersItem(
446+
c.SimplifyCoalesceInScalar(filters[i].Condition, notNullCols),
447+
)
448+
}
449+
return newFilters
450+
}
451+

pkg/sql/opt/norm/testdata/rules/decorrelate

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3006,7 +3006,7 @@ group-by (hash)
30063006
└── y:2
30073007

30083008
# Right input of SemiJoin is Project.
3009-
norm expect=TryDecorrelateSemiJoin
3009+
norm expect=TryDecorrelateSemiJoin disable=SimplifyCoalesceProject
30103010
SELECT k FROM a
30113011
WHERE EXISTS
30123012
(

pkg/sql/opt/norm/testdata/rules/scalar

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ exec-ddl
22
CREATE TABLE a (k INT PRIMARY KEY, i INT, f FLOAT, s STRING, arr int[])
33
----
44

5+
exec-ddl
6+
CREATE TABLE ij (i INT NOT NULL, j INT)
7+
----
8+
59
exec-ddl
610
CREATE TABLE xy (x INT PRIMARY KEY, y INT)
711
----
@@ -172,7 +176,9 @@ project
172176
└── projections
173177
└── COALESCE(s:4, s:4 || 'foo') [as=coalesce:8, outer=(4), immutable]
174178

175-
# Trailing null can't be removed.
179+
# Trailing null can't be removed when leading arg is a non-constant (nullable
180+
# variable): the original SimplifyCoalesce rule only fires when the leading arg
181+
# is a constant, so no folding happens here.
176182
norm
177183
SELECT COALESCE(i, NULL, NULL) FROM a
178184
----
@@ -183,6 +189,44 @@ project
183189
└── projections
184190
└── COALESCE(i:2, CAST(NULL AS INT8), CAST(NULL AS INT8)) [as=coalesce:8, outer=(2)]
185191

192+
# All-null COALESCE collapses to a single null via SimplifyCoalesce (the scalar
193+
# rule fires when the leading arg is constant). CanSimplifyCoalesce correctly
194+
# returns true when the loop exhausts all-null args, matching simplifyCoalesce's
195+
# behaviour of returning args[last] (unwrapping the COALESCE).
196+
norm expect=SimplifyCoalesce
197+
SELECT COALESCE(NULL::INT, NULL::INT) FROM a
198+
----
199+
project
200+
├── columns: coalesce:8
201+
├── fd: ()-->(8)
202+
├── scan a
203+
└── projections
204+
└── CAST(NULL AS INT8) [as=coalesce:8]
205+
206+
# --------------------------------------------------
207+
# SimplifyCoalesceProject
208+
# --------------------------------------------------
209+
210+
norm expect=SimplifyCoalesceProject
211+
SELECT COALESCE(i, j) FROM ij
212+
----
213+
project
214+
├── columns: coalesce:6!null
215+
├── scan ij
216+
│ └── columns: i:1!null
217+
└── projections
218+
└── i:1 [as=coalesce:6, outer=(1)]
219+
220+
norm expect=SimplifyCoalesceProject
221+
SELECT COALESCE(i, NULL, NULL) FROM ij
222+
----
223+
project
224+
├── columns: coalesce:6!null
225+
├── scan ij
226+
│ └── columns: i:1!null
227+
└── projections
228+
└── i:1 [as=coalesce:6, outer=(1)]
229+
186230
norm expect=SimplifyCoalesce
187231
SELECT COALESCE((1, 2, 3), (2, 3, 4)) FROM a
188232
----
@@ -193,6 +237,69 @@ project
193237
└── projections
194238
└── (1, 2, 3) [as=coalesce:8]
195239

240+
# Negative test: should not fire when no arg is provably non-null.
241+
norm expect-not=SimplifyCoalesceProject
242+
SELECT COALESCE(i, f) FROM a
243+
----
244+
project
245+
├── columns: coalesce:8
246+
├── immutable
247+
├── scan a
248+
│ └── columns: i:2 f:3
249+
└── projections
250+
└── COALESCE(i:2::FLOAT8, f:3) [as=coalesce:8, outer=(2,3), immutable]
251+
252+
# Middle argument is provably non-null: leading NULLs are stripped, then the
253+
# NOT NULL arg is returned directly.
254+
norm expect=SimplifyCoalesceProject
255+
SELECT COALESCE(NULL::INT, i, j) FROM ij
256+
----
257+
project
258+
├── columns: coalesce:6!null
259+
├── scan ij
260+
│ └── columns: i:1!null
261+
└── projections
262+
└── i:1 [as=coalesce:6, outer=(1)]
263+
264+
# Nested COALESCE: outer is not directly simplifiable, but the inner
265+
# COALESCE(i, j) can be simplified to i (since i is NOT NULL). The fix
266+
# ensures the inner Coalesce is not missed.
267+
norm expect=SimplifyCoalesceProject
268+
SELECT COALESCE(j, COALESCE(i, j)) FROM ij
269+
----
270+
project
271+
├── columns: coalesce:6
272+
├── scan ij
273+
│ └── columns: i:1!null j:2
274+
└── projections
275+
└── COALESCE(j:2, i:1) [as=coalesce:6, outer=(1,2)]
276+
277+
# COALESCE inside an IF expression: the inner nested Coalesce is simplified
278+
# via SimplifyCoalesceProject even though the outer Coalesce is not itself
279+
# directly simplifiable.
280+
norm expect=SimplifyCoalesceProject
281+
SELECT IF(j > 0, COALESCE(j, COALESCE(i, j)), j) FROM ij
282+
----
283+
project
284+
├── columns: if:6
285+
├── scan ij
286+
│ └── columns: i:1!null j:2
287+
└── projections
288+
└── CASE j:2 > 0 WHEN true THEN COALESCE(j:2, i:1) ELSE j:2 END [as=if:6, outer=(1,2)]
289+
290+
# SimplifyCoalesceProject does not fire when the first arg is a non-constant
291+
# nullable variable: CanSimplifyCoalesce exits early with false because i is
292+
# not a constant, and no leading nulls were stripped (i > 0 = false).
293+
norm expect-not=SimplifyCoalesceProject
294+
SELECT COALESCE(i, NULL, NULL) FROM a
295+
----
296+
project
297+
├── columns: coalesce:8
298+
├── scan a
299+
│ └── columns: i:2
300+
└── projections
301+
└── COALESCE(i:2, CAST(NULL AS INT8), CAST(NULL AS INT8)) [as=coalesce:8, outer=(2)]
302+
196303

197304
# --------------------------------------------------
198305
# EliminateCast

0 commit comments

Comments
 (0)