]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Simplify COALESCE expressions using non-nullable arguments
authorRichard Guo <rguo@postgresql.org>
Wed, 24 Dec 2025 08:58:49 +0000 (17:58 +0900)
committerRichard Guo <rguo@postgresql.org>
Wed, 24 Dec 2025 08:58:49 +0000 (17:58 +0900)
The COALESCE function returns the first of its arguments that is not
null.  When an argument is proven non-null, if it is the first
non-null-constant argument, the entire COALESCE expression can be
replaced by that argument.  If it is a subsequent argument, all
following arguments can be dropped, since they will never be reached.

Currently, we perform this simplification only for Const arguments.
This patch extends the simplification to support any expression that
can be proven non-nullable.

This can help avoid the overhead of evaluating unreachable arguments.
It can also lead to better plans when the first argument is proven
non-nullable and replaces the expression, as the planner no longer has
to treat the expression as non-strict, and can also leverage index
scans on the resulting expression.

There is an ensuing plan change in generated_virtual.out, and we have
to modify the test to ensure that it continues to test what it is
intended to.

Author: Richard Guo <guofenglinux@gmail.com>
Reviewed-by: Tender Wang <tndrwang@gmail.com>
Reviewed-by: Dagfinn Ilmari Mannsåker <ilmari@ilmari.org>
Reviewed-by: David Rowley <dgrowleyml@gmail.com>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Discussion: https://postgr.es/m/CAMbWs49UhPBjm+NRpxerjaeuFKyUZJ_AjM3NBcSYK2JgZ6VTEQ@mail.gmail.com

src/backend/optimizer/util/clauses.c
src/test/regress/expected/generated_virtual.out
src/test/regress/expected/predicate.out
src/test/regress/sql/generated_virtual.sql
src/test/regress/sql/predicate.sql

index ddafc21c819a948f391461f7b586a06431327a59..ac21057ba51d222af2d4f3561f5064d3d12128c1 100644 (file)
@@ -3325,10 +3325,10 @@ eval_const_expressions_mutator(Node *node,
                                                                                                           context);
 
                                        /*
-                                        * We can remove null constants from the list. For a
-                                        * non-null constant, if it has not been preceded by any
-                                        * other non-null-constant expressions then it is the
-                                        * result. Otherwise, it's the next argument, but we can
+                                        * We can remove null constants from the list.  For a
+                                        * nonnullable expression, if it has not been preceded by
+                                        * any non-null-constant expressions then it is the
+                                        * result.  Otherwise, it's the next argument, but we can
                                         * drop following arguments since they will never be
                                         * reached.
                                         */
@@ -3341,6 +3341,14 @@ eval_const_expressions_mutator(Node *node,
                                                newargs = lappend(newargs, e);
                                                break;
                                        }
+                                       if (expr_is_nonnullable(context->root, (Expr *) e, false))
+                                       {
+                                               if (newargs == NIL)
+                                                       return e;       /* first expr */
+                                               newargs = lappend(newargs, e);
+                                               break;
+                                       }
+
                                        newargs = lappend(newargs, e);
                                }
 
@@ -4328,7 +4336,7 @@ var_is_nonnullable(PlannerInfo *root, Var *var, bool use_rel_info)
 bool
 expr_is_nonnullable(PlannerInfo *root, Expr *expr, bool use_rel_info)
 {
-       if (IsA(expr, Var))
+       if (IsA(expr, Var) && root)
                return var_is_nonnullable(root, (Var *) expr, use_rel_info);
        if (IsA(expr, Const))
                return !castNode(Const, expr)->constisnull;
index dde325e46c6401ad9dc0086c1d3e9925c9c8f631..249e68be654d79e5d635423a8798e330b1c2b9c9 100644 (file)
@@ -1509,10 +1509,11 @@ create table gtest32 (
   a int primary key,
   b int generated always as (a * 2),
   c int generated always as (10 + 10),
-  d int generated always as (coalesce(a, 100)),
-  e int
+  d int generated always as (coalesce(f, 100)),
+  e int,
+  f int
 );
-insert into gtest32 values (1), (2);
+insert into gtest32 (a, f) values (1, 1), (2, 2);
 analyze gtest32;
 -- Ensure that nullingrel bits are propagated into the generation expressions
 explain (costs off)
@@ -1591,46 +1592,47 @@ where coalesce(t2.b, 1) = 2 or t1.a is null;
 -- Ensure that the generation expressions are wrapped into PHVs if needed
 explain (verbose, costs off)
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
-                          QUERY PLAN                           
----------------------------------------------------------------
+                             QUERY PLAN                              
+---------------------------------------------------------------------
  Nested Loop Left Join
-   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.a, 100)), t2.e
+   Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.f, 100)), t2.e, t2.f
    Join Filter: false
    ->  Seq Scan on generated_virtual_tests.gtest32 t1
-         Output: t1.a, t1.b, t1.c, t1.d, t1.e
+         Output: t1.a, t1.b, t1.c, t1.d, t1.e, t1.f
    ->  Result
-         Output: t2.a, t2.e, 20, COALESCE(t2.a, 100)
+         Output: t2.a, t2.e, t2.f, 20, COALESCE(t2.f, 100)
          Replaces: Scan on t2
          One-Time Filter: false
 (9 rows)
 
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
- a | b | c | d | e 
----+---+---+---+---
-   |   |   |   |  
-   |   |   |   |  
+ a | b | c | d | e | f 
+---+---+---+---+---+---
+   |   |   |   |   |  
+   |   |   |   |   |  
 (2 rows)
 
 explain (verbose, costs off)
-select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20;
-                     QUERY PLAN                      
------------------------------------------------------
+select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20;
+                       QUERY PLAN                       
+--------------------------------------------------------
  HashAggregate
-   Output: a, ((a * 2)), (20), (COALESCE(a, 100)), e
+   Output: a, ((a * 2)), (20), (COALESCE(f, 100)), e, f
    Hash Key: t.a
    Hash Key: (t.a * 2)
    Hash Key: 20
-   Hash Key: COALESCE(t.a, 100)
+   Hash Key: COALESCE(t.f, 100)
    Hash Key: t.e
+   Hash Key: t.f
    Filter: ((20) = 20)
    ->  Seq Scan on generated_virtual_tests.gtest32 t
-         Output: a, (a * 2), 20, COALESCE(a, 100), e
-(10 rows)
+         Output: a, (a * 2), 20, COALESCE(f, 100), e, f
+(11 rows)
 
-select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20;
- a | b | c  | d | e 
----+---+----+---+---
-   |   | 20 |   |  
+select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20;
+ a | b | c  | d | e | f 
+---+---+----+---+---+---
+   |   | 20 |   |   |  
 (1 row)
 
 -- Ensure that the virtual generated columns in ALTER COLUMN TYPE USING expression are expanded
index 94c343fe0309ad66638fd5d42d11b290700e86a5..520b46cd32107b92fecab55de9b8f8b7655b7d83 100644 (file)
@@ -484,3 +484,36 @@ SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
 (2 rows)
 
 DROP TABLE pred_tab;
+--
+-- Test that COALESCE expressions in predicates are simplified using
+-- non-nullable arguments.
+--
+CREATE TABLE pred_tab (a int NOT NULL, b int);
+-- Ensure that constant NULL arguments are dropped
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1;
+           QUERY PLAN           
+--------------------------------
+ Seq Scan on pred_tab
+   Filter: (COALESCE(b, a) > 1)
+(2 rows)
+
+-- Ensure that argument "b*a" is dropped
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1;
+           QUERY PLAN           
+--------------------------------
+ Seq Scan on pred_tab
+   Filter: (COALESCE(b, a) > 1)
+(2 rows)
+
+-- Ensure that the entire COALESCE expression is replaced by "a"
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
+      QUERY PLAN      
+----------------------
+ Seq Scan on pred_tab
+   Filter: (a > 1)
+(2 rows)
+
+DROP TABLE pred_tab;
index 2911439776c01eae1b73c55506add2712b05bfb4..81152b39a7986993dea1177d597fc2c89cdda02e 100644 (file)
@@ -817,11 +817,12 @@ create table gtest32 (
   a int primary key,
   b int generated always as (a * 2),
   c int generated always as (10 + 10),
-  d int generated always as (coalesce(a, 100)),
-  e int
+  d int generated always as (coalesce(f, 100)),
+  e int,
+  f int
 );
 
-insert into gtest32 values (1), (2);
+insert into gtest32 (a, f) values (1, 1), (2, 2);
 analyze gtest32;
 
 -- Ensure that nullingrel bits are propagated into the generation expressions
@@ -859,8 +860,8 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false;
 select t2.* from gtest32 t1 left join gtest32 t2 on false;
 
 explain (verbose, costs off)
-select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20;
-select * from gtest32 t group by grouping sets (a, b, c, d, e) having c = 20;
+select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20;
+select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20;
 
 -- Ensure that the virtual generated columns in ALTER COLUMN TYPE USING expression are expanded
 alter table gtest32 alter column e type bigint using b;
index 7d4fda1bc183e2c81bc2c25f9ed7e51815ba4392..c3d1a81ada1587536be393e84b34dd37322c3266 100644 (file)
@@ -240,3 +240,23 @@ SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
 SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
 
 DROP TABLE pred_tab;
+
+--
+-- Test that COALESCE expressions in predicates are simplified using
+-- non-nullable arguments.
+--
+CREATE TABLE pred_tab (a int NOT NULL, b int);
+
+-- Ensure that constant NULL arguments are dropped
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1;
+
+-- Ensure that argument "b*a" is dropped
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1;
+
+-- Ensure that the entire COALESCE expression is replaced by "a"
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
+
+DROP TABLE pred_tab;