From d24c408457f03b081b7db8f08df2fe2366137daa Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Wed, 9 Sep 2026 17:20:04 +0300
Subject: [PATCH v3] Use exact join size estimate for plain inner merge/hash
 joins

For a plain inner join, path->jpath.path.rows already equals the
number of tuples passing the merge/hash clauses, so
final_cost_mergejoin()/final_cost_hashjoin() can use it directly
instead of recomputing an approximation via approx_tuple_count().
For SEMI/LEFT/FULL joins this doesn't hold, so keep the substitution
JOIN_INNER-only.
---
 src/backend/optimizer/path/costsize.c | 20 ++++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7bbddb8bee4..55a1832080e 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -4057,8 +4057,18 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path,
 	/*
 	 * Get approx # tuples passing the mergequals.  We use approx_tuple_count
 	 * here because we need an estimate done with JOIN_INNER semantics.
+	 * However, for a plain inner join with no restriction clauses beyond the
+	 * mergeclauses, path->jpath.path.rows already gives an equally (or more)
+	 * accurate figure computed with JOIN_INNER semantics, so we reuse it and
+	 * skip the extra call.  For any other jointype, path->jpath.path.rows
+	 * reflects that jointype's own semantics (e.g. clamped to the outer/inner
+	 * size for LEFT/FULL joins), not JOIN_INNER, so it can't be substituted.
 	 */
-	mergejointuples = approx_tuple_count(root, &path->jpath, mergeclauses);
+	if (path->jpath.jointype == JOIN_INNER &&
+		list_length(path->jpath.joinrestrictinfo) == list_length(mergeclauses))
+		mergejointuples = path->jpath.path.rows;
+	else
+		mergejointuples = approx_tuple_count(root, &path->jpath, mergeclauses);
 
 	/*
 	 * When there are equal merge keys in the outer relation, the mergejoin
@@ -4699,7 +4709,10 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 	 * inner_unique joins that is the matched outer rows, and for ANTI the
 	 * unmatched ones, both available from outer_matched_rows computed above.
 	 * For plain joins, use approx_tuple_count(), which gives an estimate done
-	 * with JOIN_INNER semantics.
+	 * with JOIN_INNER semantics -- except for a plain inner join with no
+	 * restriction clauses beyond the hashclauses, where path->jpath.path.rows
+	 * already gives an equally (or more) accurate JOIN_INNER-semantics figure
+	 * for free, and calling approx_tuple_count() again would be redundant.
 	 */
 	if (path->jpath.jointype == JOIN_RIGHT_SEMI)
 		hashjointuples = clamp_row_est(inner_path_rows *
@@ -4711,6 +4724,9 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
 		hashjointuples = outer_path_rows - outer_matched_rows;
 	else if (path->jpath.jointype == JOIN_SEMI || extra->inner_unique)
 		hashjointuples = outer_matched_rows;
+	else if (path->jpath.jointype == JOIN_INNER &&
+			 list_length(path->jpath.joinrestrictinfo) == list_length(hashclauses))
+		hashjointuples = path->jpath.path.rows;
 	else
 		hashjointuples = approx_tuple_count(root, &path->jpath, hashclauses);
 
-- 
2.34.1

