v1-0001-Do-not-reduce-a-clone-clause-to-a-constant-true.patch

application/octet-stream

Filename: v1-0001-Do-not-reduce-a-clone-clause-to-a-constant-true.patch
Type: application/octet-stream
Part: 0
Message: Re: Query result differences between PostgreSQL 17 vs 16

Patch

Format: format-patch
Series: patch v1-0001
Subject: Do not reduce a clone clause to a constant true
File+
src/backend/optimizer/plan/initsplan.c 11 0
src/test/regress/expected/predicate.out 75 30
src/test/regress/sql/predicate.sql 32 6
From 24e905f20a1808ed22261fa68acfefd3cc003828 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Wed, 26 Feb 2025 10:39:14 +0900
Subject: [PATCH v1] Do not reduce a clone clause to a constant true

If an outer join can commute with any other ones per outer-join
identity 3, and it is the one providing the join clause with flexible
semantics, then we have to generate variants of the join clause with
different nullingrels labeling.  These variants are referred to as
clone clauses.  One fundamental assumption is that each clone clause
will always have corresponding counterparts.

However, with b262ad440, an IS NOT NULL qual on a NOT NULL column
might be treated as always true and removed.  This occurs after we
have generated multiple clones of the same qual condition.  As a
result, this breaks that assumption and may lead to a situation where
a clone clause has no corresponding counterparts.  This can cause
faulty plans, as we may fail to find any clone of the qual condition
to apply.

To fix, don't reduce a clause marked has_clone or is_clone to a
constant true.

Back-patch to v17 where this bug crept in.

Reported-by: Ronald Cruz <cruz@rentec.com>
Diagnosed-by: Tom Lane <tgl@sss.pgh.pa.us>
Author: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/f5320d3d-77af-4ce8-b9c3-4715ff33f213@rentec.com
Backpatch-through: 17
---
 src/backend/optimizer/plan/initsplan.c  |  11 +++
 src/test/regress/expected/predicate.out | 105 +++++++++++++++++-------
 src/test/regress/sql/predicate.sql      |  38 +++++++--
 3 files changed, 118 insertions(+), 36 deletions(-)

diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 2cb0ae6d659..827141df2a1 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -3091,6 +3091,17 @@ bool
 restriction_is_always_true(PlannerInfo *root,
 						   RestrictInfo *restrictinfo)
 {
+	/*
+	 * For a clone clause, we don't have a reliable way to determine if the
+	 * input expression of a NullTest is non-nullable, due to the
+	 * commuted-left-join cases.  Additionally, removing a clone clause will
+	 * break the assumption that each clone clause will always have
+	 * corresponding counterparts, which can lead to faulty plans, as we may
+	 * fail to find any clone of the qual condition to apply.
+	 */
+	if (restrictinfo->has_clone || restrictinfo->is_clone)
+		return false;
+
 	/* Check for NullTest qual */
 	if (IsA(restrictinfo->clause, NullTest))
 	{
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
index 965a3a76161..2248e0b588c 100644
--- a/src/test/regress/expected/predicate.out
+++ b/src/test/regress/expected/predicate.out
@@ -97,37 +97,36 @@ SELECT * FROM pred_tab t WHERE t.b IS NULL OR t.c IS NULL;
 -- and b) its Var is not nullable by any outer joins
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON TRUE
-    LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
-                   QUERY PLAN                    
--------------------------------------------------
+    LEFT JOIN pred_tab t2 ON t1.a IS NOT NULL;
+             QUERY PLAN              
+-------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on pred_tab t1
    ->  Materialize
-         ->  Nested Loop Left Join
-               ->  Seq Scan on pred_tab t2
-               ->  Materialize
-                     ->  Seq Scan on pred_tab t3
-(7 rows)
+         ->  Seq Scan on pred_tab t2
+(4 rows)
 
 -- Ensure the IS_NOT_NULL qual is not ignored when columns are made nullable
 -- by an outer join
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON t1.a = 1
+    FULL JOIN pred_tab t2 ON t1.a = t2.a
     LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
                 QUERY PLAN                 
 -------------------------------------------
  Nested Loop Left Join
    Join Filter: (t2.a IS NOT NULL)
-   ->  Nested Loop Left Join
-         Join Filter: (t1.a = 1)
-         ->  Seq Scan on pred_tab t1
-         ->  Materialize
+   ->  Merge Full Join
+         Merge Cond: (t1.a = t2.a)
+         ->  Sort
+               Sort Key: t1.a
+               ->  Seq Scan on pred_tab t1
+         ->  Sort
+               Sort Key: t2.a
                ->  Seq Scan on pred_tab t2
    ->  Materialize
          ->  Seq Scan on pred_tab t3
-(9 rows)
+(12 rows)
 
 -- Ensure the IS_NULL qual is reduced to constant-FALSE, since a) it's on a NOT
 -- NULL column, and b) its Var is not nullable by any outer joins
@@ -172,37 +171,36 @@ SELECT * FROM pred_tab t1
 -- Ensure the OR clause is ignored when an OR branch is provably always true
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON TRUE
-    LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
-                   QUERY PLAN                    
--------------------------------------------------
+    LEFT JOIN pred_tab t2 ON t1.a IS NOT NULL OR t2.b = 1;
+             QUERY PLAN              
+-------------------------------------
  Nested Loop Left Join
    ->  Seq Scan on pred_tab t1
    ->  Materialize
-         ->  Nested Loop Left Join
-               ->  Seq Scan on pred_tab t2
-               ->  Materialize
-                     ->  Seq Scan on pred_tab t3
-(7 rows)
+         ->  Seq Scan on pred_tab t2
+(4 rows)
 
 -- Ensure the NullTest is not ignored when the column is nullable by an outer
 -- join
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON t1.a = 1
+    FULL JOIN pred_tab t2 ON t1.a = t2.a
     LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
                     QUERY PLAN                     
 ---------------------------------------------------
  Nested Loop Left Join
    Join Filter: ((t2.a IS NOT NULL) OR (t2.b = 1))
-   ->  Nested Loop Left Join
-         Join Filter: (t1.a = 1)
-         ->  Seq Scan on pred_tab t1
-         ->  Materialize
+   ->  Merge Full Join
+         Merge Cond: (t1.a = t2.a)
+         ->  Sort
+               Sort Key: t1.a
+               ->  Seq Scan on pred_tab t1
+         ->  Sort
+               Sort Key: t2.a
                ->  Seq Scan on pred_tab t2
    ->  Materialize
          ->  Seq Scan on pred_tab t3
-(9 rows)
+(12 rows)
 
 -- Ensure the OR clause is reduced to constant-FALSE when all OR branches are
 -- provably false
@@ -318,3 +316,50 @@ SELECT 1 FROM pred_tab t1
 (12 rows)
 
 DROP TABLE pred_tab;
+-- Validate we do not reduce a clone clause to a constant true
+CREATE TABLE pred_tab (a int, b int);
+CREATE TABLE pred_tab_notnull (a int, b int NOT NULL);
+INSERT INTO pred_tab VALUES (1, 1);
+INSERT INTO pred_tab VALUES (2, 2);
+INSERT INTO pred_tab_notnull VALUES (2, 2);
+INSERT INTO pred_tab_notnull VALUES (3, 3);
+ANALYZE pred_tab;
+ANALYZE pred_tab_notnull;
+-- Ensure the IS_NOT_NULL qual is not reduced to constant true and removed
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1
+    LEFT JOIN pred_tab t2 ON TRUE
+    LEFT JOIN pred_tab_notnull t3 ON t2.a = t3.a
+    LEFT JOIN pred_tab t4 ON t3.b IS NOT NULL;
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Nested Loop Left Join
+   ->  Seq Scan on pred_tab t1
+   ->  Materialize
+         ->  Nested Loop Left Join
+               Join Filter: (t3.b IS NOT NULL)
+               ->  Nested Loop Left Join
+                     Join Filter: (t2.a = t3.a)
+                     ->  Seq Scan on pred_tab t2
+                     ->  Materialize
+                           ->  Seq Scan on pred_tab_notnull t3
+               ->  Materialize
+                     ->  Seq Scan on pred_tab t4
+(12 rows)
+
+SELECT * FROM pred_tab t1
+    LEFT JOIN pred_tab t2 ON TRUE
+    LEFT JOIN pred_tab_notnull t3 ON t2.a = t3.a
+    LEFT JOIN pred_tab t4 ON t3.b IS NOT NULL;
+ a | b | a | b | a | b | a | b 
+---+---+---+---+---+---+---+---
+ 1 | 1 | 1 | 1 |   |   |   |  
+ 1 | 1 | 2 | 2 | 2 | 2 | 1 | 1
+ 1 | 1 | 2 | 2 | 2 | 2 | 2 | 2
+ 2 | 2 | 1 | 1 |   |   |   |  
+ 2 | 2 | 2 | 2 | 2 | 2 | 1 | 1
+ 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2
+(6 rows)
+
+DROP TABLE pred_tab;
+DROP TABLE pred_tab_notnull;
diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql
index 661013ff7e4..03547a2b33a 100644
--- a/src/test/regress/sql/predicate.sql
+++ b/src/test/regress/sql/predicate.sql
@@ -64,14 +64,13 @@ SELECT * FROM pred_tab t WHERE t.b IS NULL OR t.c IS NULL;
 -- and b) its Var is not nullable by any outer joins
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON TRUE
-    LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
+    LEFT JOIN pred_tab t2 ON t1.a IS NOT NULL;
 
 -- Ensure the IS_NOT_NULL qual is not ignored when columns are made nullable
 -- by an outer join
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON t1.a = 1
+    FULL JOIN pred_tab t2 ON t1.a = t2.a
     LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
 
 -- Ensure the IS_NULL qual is reduced to constant-FALSE, since a) it's on a NOT
@@ -95,14 +94,13 @@ SELECT * FROM pred_tab t1
 -- Ensure the OR clause is ignored when an OR branch is provably always true
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON TRUE
-    LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
+    LEFT JOIN pred_tab t2 ON t1.a IS NOT NULL OR t2.b = 1;
 
 -- Ensure the NullTest is not ignored when the column is nullable by an outer
 -- join
 EXPLAIN (COSTS OFF)
 SELECT * FROM pred_tab t1
-    LEFT JOIN pred_tab t2 ON t1.a = 1
+    FULL JOIN pred_tab t2 ON t1.a = t2.a
     LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
 
 -- Ensure the OR clause is reduced to constant-FALSE when all OR branches are
@@ -162,3 +160,31 @@ SELECT 1 FROM pred_tab t1
     RIGHT JOIN pred_tab t5 ON t1.b = t5.b;
 
 DROP TABLE pred_tab;
+
+-- Validate we do not reduce a clone clause to a constant true
+CREATE TABLE pred_tab (a int, b int);
+CREATE TABLE pred_tab_notnull (a int, b int NOT NULL);
+
+INSERT INTO pred_tab VALUES (1, 1);
+INSERT INTO pred_tab VALUES (2, 2);
+
+INSERT INTO pred_tab_notnull VALUES (2, 2);
+INSERT INTO pred_tab_notnull VALUES (3, 3);
+
+ANALYZE pred_tab;
+ANALYZE pred_tab_notnull;
+
+-- Ensure the IS_NOT_NULL qual is not reduced to constant true and removed
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1
+    LEFT JOIN pred_tab t2 ON TRUE
+    LEFT JOIN pred_tab_notnull t3 ON t2.a = t3.a
+    LEFT JOIN pred_tab t4 ON t3.b IS NOT NULL;
+
+SELECT * FROM pred_tab t1
+    LEFT JOIN pred_tab t2 ON TRUE
+    LEFT JOIN pred_tab_notnull t3 ON t2.a = t3.a
+    LEFT JOIN pred_tab t4 ON t3.b IS NOT NULL;
+
+DROP TABLE pred_tab;
+DROP TABLE pred_tab_notnull;
-- 
2.43.0