]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Teach expr_is_nonnullable() to handle more expression types
authorRichard Guo <rguo@postgresql.org>
Wed, 24 Dec 2025 09:00:44 +0000 (18:00 +0900)
committerRichard Guo <rguo@postgresql.org>
Wed, 24 Dec 2025 09:00:44 +0000 (18:00 +0900)
Currently, the function expr_is_nonnullable() checks only Const and
Var expressions to determine if an expression is non-nullable.  This
patch extends the detection logic to handle more expression types.

This can enable several downstream optimizations, such as reducing
NullTest quals to constant truth values (e.g., "COALESCE(var, 1) IS
NULL" becomes FALSE) and converting "COUNT(expr)" to the more
efficient "COUNT(*)" when the expression is proven non-nullable.

This breaks a test case in test_predtest.sql, since we now simplify
"ARRAY[] IS NULL" to constant FALSE, preventing it from weakly
refuting a strict ScalarArrayOpExpr ("x = any(ARRAY[])").  To ensure
the refutation logic is still exercised as intended, wrap the array
argument in opaque_array().

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/modules/test_predtest/expected/test_predtest.out
src/test/modules/test_predtest/sql/test_predtest.sql
src/test/regress/expected/predicate.out
src/test/regress/sql/predicate.sql

index eaeadcbcc516d760cbe61c71a6805b1b7141a22d..67b7de16fc5c307c4e2f2a1489f2d808635064de 100644 (file)
@@ -4341,16 +4341,127 @@ var_is_nonnullable(PlannerInfo *root, Var *var, bool use_rel_info)
  * nullability information before RelOptInfos are generated.  These should
  * pass 'use_rel_info' as false.
  *
- * For now, we only support Var and Const.  Support for other node types may
- * be possible.
+ * For now, we support only a limited set of expression types.  Support for
+ * additional node types can be added in the future.
  */
 bool
 expr_is_nonnullable(PlannerInfo *root, Expr *expr, bool use_rel_info)
 {
-       if (IsA(expr, Var) && root)
-               return var_is_nonnullable(root, (Var *) expr, use_rel_info);
-       if (IsA(expr, Const))
-               return !castNode(Const, expr)->constisnull;
+       /* since this function recurses, it could be driven to stack overflow */
+       check_stack_depth();
+
+       switch (nodeTag(expr))
+       {
+               case T_Var:
+                       {
+                               if (root)
+                                       return var_is_nonnullable(root, (Var *) expr, use_rel_info);
+                       }
+                       break;
+               case T_Const:
+                       return !((Const *) expr)->constisnull;
+               case T_CoalesceExpr:
+                       {
+                               /*
+                                * A CoalesceExpr returns NULL if and only if all its
+                                * arguments are NULL.  Therefore, we can determine that a
+                                * CoalesceExpr cannot be NULL if at least one of its
+                                * arguments can be proven non-nullable.
+                                */
+                               CoalesceExpr *coalesceexpr = (CoalesceExpr *) expr;
+
+                               foreach_ptr(Expr, arg, coalesceexpr->args)
+                               {
+                                       if (expr_is_nonnullable(root, arg, use_rel_info))
+                                               return true;
+                               }
+                       }
+                       break;
+               case T_MinMaxExpr:
+                       {
+                               /*
+                                * Like CoalesceExpr, a MinMaxExpr returns NULL only if all
+                                * its arguments evaluate to NULL.
+                                */
+                               MinMaxExpr *minmaxexpr = (MinMaxExpr *) expr;
+
+                               foreach_ptr(Expr, arg, minmaxexpr->args)
+                               {
+                                       if (expr_is_nonnullable(root, arg, use_rel_info))
+                                               return true;
+                               }
+                       }
+                       break;
+               case T_CaseExpr:
+                       {
+                               /*
+                                * A CASE expression is non-nullable if all branch results are
+                                * non-nullable.  We must also verify that the default result
+                                * (ELSE) exists and is non-nullable.
+                                */
+                               CaseExpr   *caseexpr = (CaseExpr *) expr;
+
+                               /* The default result must be present and non-nullable */
+                               if (caseexpr->defresult == NULL ||
+                                       !expr_is_nonnullable(root, caseexpr->defresult, use_rel_info))
+                                       return false;
+
+                               /* All branch results must be non-nullable */
+                               foreach_ptr(CaseWhen, casewhen, caseexpr->args)
+                               {
+                                       if (!expr_is_nonnullable(root, casewhen->result, use_rel_info))
+                                               return false;
+                               }
+
+                               return true;
+                       }
+                       break;
+               case T_ArrayExpr:
+                       {
+                               /*
+                                * An ARRAY[] expression always returns a valid Array object,
+                                * even if it is empty (ARRAY[]) or contains NULLs
+                                * (ARRAY[NULL]).  It never evaluates to a SQL NULL.
+                                */
+                               return true;
+                       }
+               case T_NullTest:
+                       {
+                               /*
+                                * An IS NULL / IS NOT NULL expression always returns a
+                                * boolean value.  It never returns SQL NULL.
+                                */
+                               return true;
+                       }
+               case T_BooleanTest:
+                       {
+                               /*
+                                * A BooleanTest expression always evaluates to a boolean
+                                * value.  It never returns SQL NULL.
+                                */
+                               return true;
+                       }
+               case T_DistinctExpr:
+                       {
+                               /*
+                                * IS DISTINCT FROM never returns NULL, effectively acting as
+                                * though NULL were a normal data value.
+                                */
+                               return true;
+                       }
+               case T_RelabelType:
+                       {
+                               /*
+                                * RelabelType does not change the nullability of the data.
+                                * The result is non-nullable if and only if the argument is
+                                * non-nullable.
+                                */
+                               return expr_is_nonnullable(root, ((RelabelType *) expr)->arg,
+                                                                                  use_rel_info);
+                       }
+               default:
+                       break;
+       }
 
        return false;
 }
index 6d21bcd603ee69acbe486a4992f40e02bdb5de95..ad82b4f8f91cdc3c2bc4a2ad502118a8b81beace 100644 (file)
@@ -1066,7 +1066,7 @@ w_r_holds         | t
 
 -- as does nullness of the array
 select * from test_predtest($$
-select x = any(opaque_array(array[y])), array[y] is null
+select x = any(opaque_array(array[y])), opaque_array(array[y]) is null
 from integers
 $$);
 -[ RECORD 1 ]-----+--
index 072eb5b0d50222094554b3b99aa1853b7261bf8c..dc59f0c22f0795159c09ffbb6fa580a76d614838 100644 (file)
@@ -431,7 +431,7 @@ $$);
 
 -- as does nullness of the array
 select * from test_predtest($$
-select x = any(opaque_array(array[y])), array[y] is null
+select x = any(opaque_array(array[y])), opaque_array(array[y]) is null
 from integers
 $$);
 
index 520b46cd32107b92fecab55de9b8f8b7655b7d83..8ff1172008ee0727022ed26157b5c3e09c8226d9 100644 (file)
@@ -488,7 +488,7 @@ 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);
+CREATE TABLE pred_tab (a int NOT NULL, b int, c int);
 -- Ensure that constant NULL arguments are dropped
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1;
@@ -516,4 +516,119 @@ SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
    Filter: (a > 1)
 (2 rows)
 
+--
+-- Test detection of non-nullable expressions in predicates
+--
+-- CoalesceExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL;
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on pred_tab
+   Filter: (COALESCE(b, c) IS NULL)
+(2 rows)
+
+-- MinMaxExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL;
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on pred_tab
+   Filter: (GREATEST(b, c) IS NULL)
+(2 rows)
+
+-- CaseExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Seq Scan on pred_tab
+   Filter: (CASE WHEN (c > 0) THEN b ELSE a END IS NULL)
+(2 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Seq Scan on pred_tab
+   Filter: (CASE WHEN (c > 0) THEN a ELSE NULL::integer END IS NULL)
+(2 rows)
+
+-- ArrayExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- NullTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- BooleanTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- DistinctExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
+-- RelabelType
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
+          QUERY PLAN          
+------------------------------
+ Result
+   Replaces: Scan on pred_tab
+   One-Time Filter: false
+(3 rows)
+
 DROP TABLE pred_tab;
index c3d1a81ada1587536be393e84b34dd37322c3266..db72b11bb22856af95b89c8abadb4505c5bc2e25 100644 (file)
@@ -245,7 +245,7 @@ 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);
+CREATE TABLE pred_tab (a int NOT NULL, b int, c int);
 
 -- Ensure that constant NULL arguments are dropped
 EXPLAIN (COSTS OFF)
@@ -259,4 +259,52 @@ SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1;
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1;
 
+--
+-- Test detection of non-nullable expressions in predicates
+--
+
+-- CoalesceExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL;
+
+-- MinMaxExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL;
+
+-- CaseExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL;
+
+-- ArrayExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL;
+
+-- NullTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL;
+
+-- BooleanTest
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL;
+
+-- DistinctExpr
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL;
+
+-- RelabelType
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
+
 DROP TABLE pred_tab;