]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Teach planner to transform "x IS [NOT] DISTINCT FROM NULL" to a NullTest
authorRichard Guo <rguo@postgresql.org>
Tue, 10 Feb 2026 01:19:25 +0000 (10:19 +0900)
committerRichard Guo <rguo@postgresql.org>
Tue, 10 Feb 2026 01:19:25 +0000 (10:19 +0900)
In the spirit of 8d19d0e13, this patch teaches the planner about the
principle that NullTest with !argisrow is fully equivalent to SQL's IS
[NOT] DISTINCT FROM NULL.

The parser already performs this transformation for literal NULLs.
However, a DistinctExpr expression with one input evaluating to NULL
during planning (e.g., via const-folding of "1 + NULL" or parameter
substitution in custom plans) currently remains as a DistinctExpr
node.

This patch closes the gap for const-folded NULLs.  It specifically
targets the case where one input is a constant NULL and the other is a
nullable non-constant expression.  (If the other input were otherwise,
the DistinctExpr node would have already been simplified to a constant
TRUE or FALSE.)

This transformation can be beneficial because NullTest is much more
amenable to optimization than DistinctExpr, since the planner knows a
good deal about the former and next to nothing about the latter.

Author: Richard Guo <guofenglinux@gmail.com>
Reviewed-by: Tender Wang <tndrwang@gmail.com>
Discussion: https://postgr.es/m/CAMbWs49BMAOWvkdSHxpUDnniqJcEcGq3_8dd_5wTR4xrQY8urA@mail.gmail.com

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

index f73d6a68495fd2212b7e37db2ac2fce58c399809..504a30d8836858a4482709a4f519c0ee283bb816 100644 (file)
@@ -2837,6 +2837,30 @@ eval_const_expressions_mutator(Node *node,
                                        return eval_const_expressions_mutator(negate_clause((Node *) eqexpr),
                                                                                                                  context);
                                }
+                               else if (has_null_input)
+                               {
+                                       /*
+                                        * One input is a nullable non-constant expression, and
+                                        * the other is an explicit NULL constant.  We can
+                                        * transform this to a NullTest with !argisrow, which is
+                                        * much more amenable to optimization.
+                                        */
+
+                                       NullTest   *nt = makeNode(NullTest);
+
+                                       nt->arg = (Expr *) (IsA(linitial(args), Const) ?
+                                                                               lsecond(args) : linitial(args));
+                                       nt->nulltesttype = IS_NOT_NULL;
+
+                                       /*
+                                        * argisrow = false is correct whether or not arg is
+                                        * composite
+                                        */
+                                       nt->argisrow = false;
+                                       nt->location = expr->location;
+
+                                       return eval_const_expressions_mutator((Node *) nt, context);
+                               }
 
                                /*
                                 * The expression cannot be simplified any further, so build
index 75590edf1bc9cb723aaabc4a9cde21d91d0b0374..feae77cb84097d0a2ed0df86dc90ef9f42a26558 100644 (file)
@@ -633,7 +633,7 @@ SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
 
 DROP TABLE pred_tab;
 --
--- Test optimization of IS [NOT] DISTINCT FROM on non-nullable inputs
+-- Test optimization of IS [NOT] DISTINCT FROM
 --
 CREATE TYPE dist_row_t AS (a int, b int);
 CREATE TABLE dist_tab (id int, val_nn int NOT NULL, val_null int, row_nn dist_row_t NOT NULL);
@@ -766,6 +766,73 @@ SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.
 (3 rows)
 
 RESET enable_nestloop;
+-- Ensure that the predicate is converted to IS NOT NULL
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT;
+            QUERY PLAN            
+----------------------------------
+ Seq Scan on dist_tab
+   Filter: (val_null IS NOT NULL)
+(2 rows)
+
+SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT;
+ id 
+----
+  1
+  3
+(2 rows)
+
+-- Ensure that the predicate is converted to IS NULL
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT;
+          QUERY PLAN          
+------------------------------
+ Seq Scan on dist_tab
+   Filter: (val_null IS NULL)
+(2 rows)
+
+SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT;
+ id 
+----
+  2
+(1 row)
+
+-- Safety check for rowtypes
+-- The predicate is converted to IS NOT NULL, and get_rule_expr prints it as IS
+-- DISTINCT FROM because argisrow is false, indicating that we're applying a
+-- scalar test
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Seq Scan on dist_tab
+   Filter: (ROW(val_null, val_null) IS DISTINCT FROM NULL)
+(2 rows)
+
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD;
+ id 
+----
+  1
+  2
+  3
+(3 rows)
+
+-- The predicate is converted to IS NULL, and get_rule_expr prints it as IS NOT
+-- DISTINCT FROM because argisrow is false, indicating that we're applying a
+-- scalar test
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Seq Scan on dist_tab
+   Filter: (ROW(val_null, val_null) IS NOT DISTINCT FROM NULL)
+(2 rows)
+
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD;
+ id 
+----
+(0 rows)
+
 DROP TABLE dist_tab;
 DROP TYPE dist_row_t;
 --
index b2811f7f3f0050b5f5347fd16062ce181ed8d8d4..0f92bb5243508616ac03b64be1d29331f9d8c4f9 100644 (file)
@@ -310,7 +310,7 @@ SELECT * FROM pred_tab WHERE (a::oid) IS NULL;
 DROP TABLE pred_tab;
 
 --
--- Test optimization of IS [NOT] DISTINCT FROM on non-nullable inputs
+-- Test optimization of IS [NOT] DISTINCT FROM
 --
 
 CREATE TYPE dist_row_t AS (a int, b int);
@@ -367,6 +367,31 @@ SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.
 SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.val_nn;
 RESET enable_nestloop;
 
+-- Ensure that the predicate is converted to IS NOT NULL
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT;
+SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT;
+
+-- Ensure that the predicate is converted to IS NULL
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT;
+SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT;
+
+-- Safety check for rowtypes
+-- The predicate is converted to IS NOT NULL, and get_rule_expr prints it as IS
+-- DISTINCT FROM because argisrow is false, indicating that we're applying a
+-- scalar test
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD;
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD;
+
+-- The predicate is converted to IS NULL, and get_rule_expr prints it as IS NOT
+-- DISTINCT FROM because argisrow is false, indicating that we're applying a
+-- scalar test
+EXPLAIN (COSTS OFF)
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD;
+SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD;
+
 DROP TABLE dist_tab;
 DROP TYPE dist_row_t;