From b8bfc22f919f03d63c809cb15c22f9c78e2b9d75 Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Mon, 17 Aug 2026 14:10:45 +0300
Subject: [PATCH v1] Use extended statistics for join clauses during
 parameterized costing

Dependency-based extended statistics were only considered when every
clause in a clause list referenced a single relation. When costing a
parameterized inner path of a nested loop join, one side of an
equality clause is a Var belonging to the outer relation, so the
whole clause list was rejected by dependency_is_compatible_clause()
and the planner fell back to the default per-column independence
assumption - even though the parameterized relation's own extended
statistics could produce a much better estimate.

Teach dependency_is_compatible_clause() to also accept clauses of the
form "relid.a = otherrel.b": the other relation's Var is treated like
a pseudoconstant for compatibility-checking purposes, since its value
is fixed for the duration of one parameterized probe and the
degree-based estimate does not depend on it. The degree lookup and
combination logic itself is unchanged.
---
 src/backend/optimizer/path/clausesel.c | 10 ++++++++
 src/backend/statistics/dependencies.c  | 35 +++++++++++++++++++++++---
 2 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/src/backend/optimizer/path/clausesel.c b/src/backend/optimizer/path/clausesel.c
index 25c4d177ad9..57a370d5b14 100644
--- a/src/backend/optimizer/path/clausesel.c
+++ b/src/backend/optimizer/path/clausesel.c
@@ -155,6 +155,16 @@ clauselist_selectivity_ext(PlannerInfo *root,
 											&estimatedclauses, false);
 	}
 
+	if (use_extended_stats && rel == NULL && varRelid != 0)
+	{
+		RelOptInfo *paramrel = find_base_rel(root, varRelid);
+
+		if (paramrel->rtekind == RTE_RELATION && paramrel->statlist != NIL)
+			s1 *= dependencies_clauselist_selectivity(root, clauses, varRelid,
+													jointype, sjinfo, paramrel,
+													&estimatedclauses);
+	}
+
 	/*
 	 * Apply normal selectivity estimates for remaining clauses. We'll be
 	 * careful to skip any clauses which were already estimated above.
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index 81bcf76cc1c..211100b1547 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -734,8 +734,14 @@ dependency_is_compatible_clause(Node *clause, Index relid, AttrNumber *attnum)
 		if (rinfo->pseudoconstant)
 			return false;
 
-		/* Clauses referencing multiple, or no, varnos are incompatible */
-		if (bms_membership(rinfo->clause_relids) != BMS_SINGLETON)
+		/*
+		 * Clauses referencing more than two, or no, varnos are incompatible.
+		 * A second varno is allowed because we also accept "relid's Var =
+		 * other relation's Var" below, e.g. a parameterized index qual like
+		 * "t1.a = t2.b" while costing an index scan on t1.
+		 */
+		if (bms_num_members(rinfo->clause_relids) > 2 ||
+			bms_num_members(rinfo->clause_relids) == 0)
 			return false;
 
 		clause = (Node *) rinfo->clause;
@@ -756,7 +762,30 @@ dependency_is_compatible_clause(Node *clause, Index relid, AttrNumber *attnum)
 		else if (is_pseudo_constant_clause(linitial(expr->args)))
 			clause_expr = lsecond(expr->args);
 		else
-			return false;
+		{
+			/*
+			 * Neither argument is a pseudoconstant, but this can still be a
+			 * usable equality clause when costing a parameterized path: one
+			 * argument is a Var of "relid" and the other is a Var of some
+			 * other single relation, which is fixed for the duration of one
+			 * parameterized scan and can be treated like a constant.
+			 */
+			Bitmapset  *largrelids = pull_varnos(NULL, linitial(expr->args));
+			Bitmapset  *rargrelids = pull_varnos(NULL, lsecond(expr->args));
+
+			if (bms_membership(largrelids) == BMS_SINGLETON &&
+				bms_singleton_member(largrelids) == relid &&
+				bms_membership(rargrelids) == BMS_SINGLETON &&
+				bms_singleton_member(rargrelids) != relid)
+				clause_expr = linitial(expr->args);
+			else if (bms_membership(rargrelids) == BMS_SINGLETON &&
+					 bms_singleton_member(rargrelids) == relid &&
+					 bms_membership(largrelids) == BMS_SINGLETON &&
+					 bms_singleton_member(largrelids) != relid)
+				clause_expr = lsecond(expr->args);
+			else
+				return false;
+		}
 
 		/*
 		 * If it's not an "=" operator, just ignore the clause, as it's not
-- 
2.34.1

