From 24e905f20a1808ed22261fa68acfefd3cc003828 Mon Sep 17 00:00:00 2001 From: Richard Guo 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 Diagnosed-by: Tom Lane Author: Richard Guo 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