From 76d19f644498dd3c32d26cad87ecc7d71d7eea66 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Thu, 30 Jul 2026 09:46:26 -0300 Subject: [PATCH] Prevent duplicate base restriction clauses from being counted twice The planner estimates the selectivity of a restriction list by treating each clause as independent and multiplying the individual selectivities together. When the same qual appears more than once on a relation, this squares its selectivity and underestimates the number of rows the scan will return. That can happen readily when a predicate is present in both a WHERE clause and an inner join's ON clause: both copies get pushed down onto the same base relation, and the resulting row underestimate can propagate up the join tree and lead to a worse plan being chosen. Since two structurally equal clauses have identical selectivity, a duplicate can be dropped without changing the result. Do so in add_base_clause_to_rel(), where the duplicate first becomes visible after qual pushdown, alongside the existing logic that discards always-true quals. The check is deliberately conservative: a clause is only removed when an equal() clause is already present with a matching security level, pushed-down status and pseudoconstant flag, and neither clause is an outer-join clone variant. --- src/backend/optimizer/plan/initsplan.c | 41 ++++++++++++++++++++++ src/test/regress/expected/create_index.out | 6 ++-- src/test/regress/expected/equivclass.out | 12 +++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index b38422c47a4..b1cd9e7f8a4 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -3406,6 +3406,40 @@ check_redundant_nullability_qual(PlannerInfo *root, Node *clause) return false; } +/* + * baserestrictinfo_contains_dup + * Return true if 'rel' already has a baserestrictinfo whose clause is + * equal() to restrictinfo. + * + * It only treat clauses as duplicates when their security levels, pushed-down + * status and pseudoconstant flags match, and neither is an outer-join clone + * variant (whose serials/relids need to stay in lockstep). + */ +static bool +baserestrictinfo_contains_dup(RelOptInfo *rel, RestrictInfo *restrictinfo) +{ + ListCell *lc; + + if (restrictinfo->has_clone || restrictinfo->is_clone) + return false; + + foreach(lc, rel->baserestrictinfo) + { + RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc); + + if (rinfo->has_clone || rinfo->is_clone) + continue; + + if (restrictinfo->security_level == rinfo->security_level && + restrictinfo->is_pushed_down == rinfo->is_pushed_down && + restrictinfo->pseudoconstant == rinfo->pseudoconstant && + equal(restrictinfo->clause, rinfo->clause)) + return true; + } + + return false; +} + /* * add_base_clause_to_rel * Add 'restrictinfo' as a baserestrictinfo to the base relation denoted @@ -3444,6 +3478,13 @@ add_base_clause_to_rel(PlannerInfo *root, Index relid, if (restriction_is_always_true(root, restrictinfo)) return; + /* + * Don't add the clause if an equal clause is already present, to + * avoid double-counting its selectivity. + */ + if (baserestrictinfo_contains_dup(rel, restrictinfo)) + return; + /* * Substitute the origin qual with constant-FALSE if it is provably * always false. diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index 7b2640f0e04..80c5e8bac9a 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -2441,10 +2441,10 @@ SELECT unique1 FROM tenk1 WHERE unique1 = ANY('{NULL,NULL,NULL}'); explain (costs off) SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL; - QUERY PLAN ---------------------------------------------------------- + QUERY PLAN +---------------------------------------------- Index Only Scan using tenk1_unique1 on tenk1 - Index Cond: ((unique1 IS NULL) AND (unique1 IS NULL)) + Index Cond: (unique1 IS NULL) (2 rows) SELECT unique1 FROM tenk1 WHERE unique1 IS NULL AND unique1 IS NULL; diff --git a/src/test/regress/expected/equivclass.out b/src/test/regress/expected/equivclass.out index ad8ab294ff6..95734cd346c 100644 --- a/src/test/regress/expected/equivclass.out +++ b/src/test/regress/expected/equivclass.out @@ -143,12 +143,12 @@ explain (costs off) explain (costs off) select * from ec1, ec2 where ff = x1 and ff = '42'::int8; - QUERY PLAN -------------------------------------------------------------------- + QUERY PLAN +----------------------------------------- Nested Loop Join Filter: (ec1.ff = ec2.x1) -> Index Scan using ec1_pkey on ec1 - Index Cond: ((ff = '42'::bigint) AND (ff = '42'::bigint)) + Index Cond: (ff = '42'::bigint) -> Seq Scan on ec2 (5 rows) @@ -233,12 +233,12 @@ explain (costs off) union all select ff + 4 as x from ec1) as ss1 where ss1.x = ec1.f1 and ec1.ff = 42::int8 and ec1.ff = ec1.f1; - QUERY PLAN -------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------- Nested Loop Join Filter: ((((ec1_1.ff + 2) + 1)) = ec1.f1) -> Index Scan using ec1_pkey on ec1 - Index Cond: ((ff = '42'::bigint) AND (ff = '42'::bigint)) + Index Cond: (ff = '42'::bigint) Filter: (ff = f1) -> Append -> Index Scan using ec1_expr2 on ec1 ec1_1 -- 2.50.1 (Apple Git-155)