From bd1faff01cb6a669a38a8bb9440ba93cd82de262 Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Wed, 19 Mar 2025 17:30:54 +0900
Subject: [PATCH v2 3/3] amit delta

---
 src/backend/nodes/outfuncs.c              |   6 +-
 src/backend/optimizer/path/equivclass.c   | 372 ++++++++++++----------
 src/backend/optimizer/plan/analyzejoins.c |   4 +-
 src/include/nodes/pathnodes.h             |  31 +-
 src/include/optimizer/paths.h             |   2 +-
 5 files changed, 224 insertions(+), 191 deletions(-)

diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 642ead7e629..557f06e344f 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -467,12 +467,8 @@ _outEquivalenceClass(StringInfo str, const EquivalenceClass *node)
 	WRITE_OID_FIELD(ec_collation);
 	WRITE_NODE_FIELD(ec_members);
 	WRITE_NODE_FIELD(ec_sources);
+	/* Only ec_derives_list is written; hash is not serialized. */
 	WRITE_NODE_FIELD(ec_derives_list);
-
-	/*
-	 * ec_derives_list and ec_derives_hash contain the same set of
-	 * RestrictInfos, hence no need to write contents of ec_derives_hash.
-	 */
 	WRITE_BITMAPSET_FIELD(ec_relids);
 	WRITE_BOOL_FIELD(ec_has_const);
 	WRITE_BOOL_FIELD(ec_has_volatile);
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 0d21461dc7c..4d926f3b582 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -73,22 +73,23 @@ static Bitmapset *get_eclass_indexes_for_relids(PlannerInfo *root,
 												Relids relids);
 static Bitmapset *get_common_eclass_indexes(PlannerInfo *root, Relids relids1,
 											Relids relids2);
-static void add_derived_clauses(EquivalenceClass *ec, List *clauses);
-static void add_derived_clause(EquivalenceClass *ec, RestrictInfo *clause);
-static RestrictInfo *search_clause_for_ems(PlannerInfo *root, EquivalenceClass *ec,
-										   EquivalenceMember *leftem,
-										   EquivalenceMember *rightem,
-										   EquivalenceClass *parent_ec);
-static RestrictInfo *search_derived_clause_for_ems(PlannerInfo *root,
-												   EquivalenceClass *ec,
-												   EquivalenceMember *leftem,
-												   EquivalenceMember *rightem,
-												   EquivalenceClass *parent_ec);
-static void add_clause_to_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo);
 static void build_ec_derives_hash(PlannerInfo *root, EquivalenceClass *ec);
+static void ec_add_derived_clauses(EquivalenceClass *ec, List *clauses);
+static void ec_add_derived_clause(EquivalenceClass *ec, RestrictInfo *clause);
+static void add_clause_to_ec_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo);
+static RestrictInfo *ec_search_clause_for_ems(PlannerInfo *root, EquivalenceClass *ec,
+											  EquivalenceMember *leftem,
+											  EquivalenceMember *rightem,
+											  EquivalenceClass *parent_ec);
+static RestrictInfo *ec_search_derived_clause_for_ems(PlannerInfo *root,
+													  EquivalenceClass *ec,
+													  EquivalenceMember *leftem,
+													  EquivalenceMember *rightem,
+													  EquivalenceClass *parent_ec);
 
-
-/* Hash key for derived clause lookup by EquivalenceMembers. */
+/*
+ * Hash key identifying a derived clause by EquivalenceMembers and parent EC.
+ */
 typedef struct
 {
 	EquivalenceMember *em1;
@@ -96,7 +97,7 @@ typedef struct
 	EquivalenceClass *parent_ec;
 } ECDerivesKey;
 
-/* Hash table entry in ec_derives_hash. */
+/* Hash table entry used in ec_derives_hash. */
 typedef struct
 {
 	uint32		status;
@@ -108,8 +109,10 @@ typedef struct
 #define SH_ELEMENT_TYPE			ECDerivesEntry
 #define SH_KEY_TYPE             ECDerivesKey
 #define SH_KEY                  key
-#define SH_HASH_KEY(tb, key)    hash_bytes((const unsigned char *) &(key), sizeof(ECDerivesKey))
-#define SH_EQUAL(tb, a, b)		((a).em1 == (b).em1 && (a).em2 == (b).em2 && (a).parent_ec == (b).parent_ec)
+#define SH_HASH_KEY(tb, key)	\
+	hash_bytes((const unsigned char *) &(key), sizeof(ECDerivesKey))
+#define SH_EQUAL(tb, a, b)	\
+	((a).em1 == (b).em1 && (a).em2 == (b).em2 && (a).parent_ec == (b).parent_ec)
 #define SH_SCOPE                static inline
 #define SH_DECLARE
 #define SH_DEFINE
@@ -383,7 +386,8 @@ process_equivalence(PlannerInfo *root,
 		 */
 		ec1->ec_members = list_concat(ec1->ec_members, ec2->ec_members);
 		ec1->ec_sources = list_concat(ec1->ec_sources, ec2->ec_sources);
-		add_derived_clauses(ec1, ec2->ec_derives_list);
+		/* Updates ec1's ec_derives_list and ec_derives_hash if present. */
+		ec_add_derived_clauses(ec1, ec2->ec_derives_list);
 		ec1->ec_relids = bms_join(ec1->ec_relids, ec2->ec_relids);
 		ec1->ec_has_const |= ec2->ec_has_const;
 		/* can't need to set has_volatile */
@@ -396,7 +400,7 @@ process_equivalence(PlannerInfo *root,
 		/* just to avoid debugging confusion w/ dangling pointers: */
 		ec2->ec_members = NIL;
 		ec2->ec_sources = NIL;
-		clear_ec_derived_clauses(ec2);
+		ec_clear_derived_clauses(ec2);
 		ec2->ec_relids = NULL;
 		ec1->ec_sources = lappend(ec1->ec_sources, restrictinfo);
 		ec1->ec_min_security = Min(ec1->ec_min_security,
@@ -1069,8 +1073,8 @@ relation_can_be_sorted_early(PlannerInfo *root, RelOptInfo *rel,
  * scanning of the quals and before Path construction begins.
  *
  * We make no attempt to avoid generating duplicate RestrictInfos here: we
- * don't search source or derived clauses for matches.  It doesn't really
- * seem worth the trouble to do so.
+ * don't search existing source or derived clauses in the EC for matches.  It
+ * doesn't really seem worth the trouble to do so.
  */
 void
 generate_base_implied_equalities(PlannerInfo *root)
@@ -1243,7 +1247,7 @@ generate_base_implied_equalities_const(PlannerInfo *root,
 			rinfo->left_ec = rinfo->right_ec = ec;
 			rinfo->left_em = cur_em;
 			rinfo->right_em = const_em;
-			add_derived_clause(ec, rinfo);
+			ec_add_derived_clause(ec, rinfo);
 		}
 	}
 }
@@ -1308,10 +1312,10 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
 
 			/*
 			 * If the clause didn't degenerate to a constant, fill in the
-			 * correct markings for a mergejoinable clause.  We don't save it
-			 * as a derived clause however; we don't currently need to re-find
-			 * such clauses, and we don't want to clutter that set with
-			 * non-join clauses.
+			 * correct markings for a mergejoinable clause.  We don't record it
+			 * as a derived clause, since we don't currently need to re-find
+			 * such clauses, and don't want to clutter the derived-clause set
+			 * with non-join clauses.
 			 */
 			if (rinfo && rinfo->mergeopfamilies)
 			{
@@ -1797,7 +1801,7 @@ generate_join_implied_equalities_broken(PlannerInfo *root,
 	/*
 	 * If we have to translate, just brute-force apply adjust_appendrel_attrs
 	 * to all the RestrictInfos at once.  This will result in returning
-	 * RestrictInfos that are not part of EC derived clauses , but there
+	 * RestrictInfos that are not included in EC's derived clauses, but there
 	 * shouldn't be any duplication, and it's a sufficiently narrow corner
 	 * case that we shouldn't sweat too much over it anyway.
 	 *
@@ -1845,97 +1849,6 @@ select_equality_operator(EquivalenceClass *ec, Oid lefttype, Oid righttype)
 	return InvalidOid;
 }
 
-/*
- * search_clause_for_ems
- *	  Return an already built RestrictInfo for the given pair of
- *	  EquivalenceMembers, if exists.
- *
- * We can use either original source clauses or previously-derived clauses, and
- * a commutator clause is acceptable.
- *
- * We used to verify that opno matches, but that seems redundant: even if it's
- * not identical, it'd better have the same effects, or the operator families
- * we're using are broken.
- *
- * Returns NULL if the required RestrictInfo is not already built.
- */
-static RestrictInfo *
-search_clause_for_ems(PlannerInfo *root, EquivalenceClass *ec, EquivalenceMember *leftem, EquivalenceMember *rightem, EquivalenceClass *parent_ec)
-{
-	foreach_node(RestrictInfo, rinfo, ec->ec_sources)
-	{
-		if (rinfo->left_em == leftem &&
-			rinfo->right_em == rightem &&
-			rinfo->parent_ec == parent_ec)
-			return rinfo;
-		if (rinfo->left_em == rightem &&
-			rinfo->right_em == leftem &&
-			rinfo->parent_ec == parent_ec)
-			return rinfo;
-	}
-
-	return search_derived_clause_for_ems(root, ec, leftem, rightem, parent_ec);
-}
-
-/*
- * search_derived_clause_for_ems
- *	  Similar to search_clause_for_ems() but looks up derived clauses.
- */
-static RestrictInfo *
-search_derived_clause_for_ems(PlannerInfo *root, EquivalenceClass *ec, EquivalenceMember *leftem, EquivalenceMember *rightem, EquivalenceClass *parent_ec)
-{
-	/*
-	 * Switch to using hash lookup when list grows "too long". The threshold
-	 * is arbitrary and is known only here.
-	 */
-	if (!ec->ec_derives_hash && list_length(ec->ec_derives_list) >= 32)
-		build_ec_derives_hash(root, ec);
-
-	/* Perform hash table lookup or linear search as appropriate. */
-	if (ec->ec_derives_hash)
-	{
-		ECDerivesKey key;
-		RestrictInfo *rinfo;
-		ECDerivesEntry *entry;
-
-		/*
-		 * add_clause_to_derives_hash() explains why we don't perform a second
-		 * search by swapping EquivalenceMembers.
-		 */
-		key.em1 = leftem;
-		key.em2 = rightem;
-		key.parent_ec = parent_ec;
-		entry = derives_lookup(ec->ec_derives_hash, key);
-		if (entry)
-		{
-			rinfo = entry->rinfo;
-			Assert(rinfo);
-			return rinfo;
-		}
-	}
-	else
-	{
-		foreach_node(RestrictInfo, rinfo, ec->ec_derives_list)
-		{
-			if (!rightem &&
-				rinfo->left_em == leftem)
-			{
-				Assert(rinfo->right_em->em_is_const);
-				return rinfo;
-			}
-			if (rinfo->left_em == leftem &&
-				rinfo->right_em == rightem &&
-				rinfo->parent_ec == parent_ec)
-				return rinfo;
-			if (rinfo->left_em == rightem &&
-				rinfo->right_em == leftem &&
-				rinfo->parent_ec == parent_ec)
-				return rinfo;
-		}
-	}
-
-	return NULL;
-}
 
 /*
  * create_join_clause
@@ -1959,7 +1872,7 @@ create_join_clause(PlannerInfo *root,
 	RestrictInfo *parent_rinfo = NULL;
 	MemoryContext oldcontext;
 
-	rinfo = search_clause_for_ems(root, ec, leftem, rightem, parent_ec);
+	rinfo = ec_search_clause_for_ems(root, ec, leftem, rightem, parent_ec);
 	if (rinfo)
 		return rinfo;
 
@@ -2025,7 +1938,7 @@ create_join_clause(PlannerInfo *root,
 	rinfo->left_em = leftem;
 	rinfo->right_em = rightem;
 	/* and save it for possible re-use */
-	add_derived_clause(ec, rinfo);
+	ec_add_derived_clause(ec, rinfo);
 
 	MemoryContextSwitchTo(oldcontext);
 
@@ -2757,7 +2670,7 @@ find_derived_clause_for_ec_member(PlannerInfo *root,
 	Assert(ec->ec_has_const);
 	Assert(!em->em_is_const);
 
-	return search_derived_clause_for_ems(root, ec, em, NULL, NULL);
+	return ec_search_derived_clause_for_ems(root, ec, em, NULL, NULL);
 }
 
 
@@ -3532,81 +3445,84 @@ get_common_eclass_indexes(PlannerInfo *root, Relids relids1, Relids relids2)
 }
 
 /*
- * add_derived_clauses
- *		Add given clause to the set of clauses derived from the given EquivalenceClass; adding to the list and hash table if needed.
+ * build_ec_derives_hash
+ *	  Construct the auxiliary hash table for derived clauses.
  */
 static void
-add_derived_clause(EquivalenceClass *ec, RestrictInfo *clause)
+build_ec_derives_hash(PlannerInfo *root, EquivalenceClass *ec)
+{
+	Assert(!ec->ec_derives_hash);
+
+	/* Create the hash table */
+	ec->ec_derives_hash = derives_create(root->planner_cxt, 256L, NULL);
+
+	foreach_node(RestrictInfo, rinfo, ec->ec_derives_list)
+		add_clause_to_ec_derives_hash(ec, rinfo);
+}
+
+/*
+ * ec_add_derived_clause
+ *		Add a clause to the set of derived clauses for the given
+ *		EquivalenceClass. Always appends to ec_derives_list; also adds
+ *		to ec_derives_hash if it exists.
+ */
+static void
+ec_add_derived_clause(EquivalenceClass *ec, RestrictInfo *clause)
 {
 	ec->ec_derives_list = lappend(ec->ec_derives_list, clause);
 	if (ec->ec_derives_hash)
-		add_clause_to_derives_hash(ec, clause);
+		add_clause_to_ec_derives_hash(ec, clause);
 }
 
 /*
- * add_derived_clauses
- *		Add a list of clauses to the set of clauses derived from the given EquivalenceClass; adding to the list and hash table if needed.
+ * ec_add_derived_clauses
+ *		Add a list of clauses to the set of clauses derived from the given
+ *		EquivalenceClass; adding to the list and hash table if needed.
  *
  * This function is similar to above function but optimized for adding multiple
  * clauses at a time to the ec_derives_list.
  */
 static void
-add_derived_clauses(EquivalenceClass *ec, List *clauses)
+ec_add_derived_clauses(EquivalenceClass *ec, List *clauses)
 {
 	ec->ec_derives_list = list_concat(ec->ec_derives_list, clauses);
 	if (ec->ec_derives_hash)
 		foreach_node(RestrictInfo, rinfo, clauses)
-			add_clause_to_derives_hash(ec, rinfo);
+			add_clause_to_ec_derives_hash(ec, rinfo);
 }
 
 /*
- * build_ec_derives_hash
- *	  Construct the auxiliary hash table for derived clauses.
+ * add_clause_to_ec_derives_hash
+ *      Add a derived clause to the ec_derives_hash of the given
+ *      EquivalenceClass.
  */
 static void
-build_ec_derives_hash(PlannerInfo *root, EquivalenceClass *ec)
-{
-	Assert(!ec->ec_derives_hash);
-
-	/* Create the hash table */
-	ec->ec_derives_hash = derives_create(root->planner_cxt, 256L, NULL);
-
-	foreach_node(RestrictInfo, rinfo, ec->ec_derives_list)
-		add_clause_to_derives_hash(ec, rinfo);
-}
-
-/*
- * add_clause_to_derives_hash
- *	  Add the given clause to the hash table of derived clauses in the given EquivalenceClass.
- */
-static void
-add_clause_to_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo)
+add_clause_to_ec_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo)
 {
 	ECDerivesKey key;
 	ECDerivesEntry *entry;
 	bool		found;
 
 	/*
-	 * Per generate_base_implied_equalities_const(), a constant always appears
-	 * on the RHS of equality.
+	 * Constants are always placed on the RHS; see
+	 * generate_base_implied_equalities_const().
 	 */
 	Assert(!rinfo->left_em->em_is_const);
 
 	if (rinfo->right_em->em_is_const)
 	{
 		/*
-		 * generate_base_implied_equalities_const() doesn't set parent_ec
-		 * since clauses involving constants are not redundant.
+		 * Clauses containing a constant never considered redundant, so
+		 * parent_ec is not set.
 		 */
 		Assert(!rinfo->parent_ec);
 
 		/*
-		 * find_derived_clause_for_ec_member() looks up a clause involving a
-		 * constant by the non-constant EquivalenceMember. The actual constant
-		 * EquivalenceMember is irrelevant for such lookup and is not
-		 * available when looking up. Further there is only one constant
-		 * EquivalenceMember per EquivalenceClass. For ease of lookup, we set
-		 * right_em to NULL in the key.
+		 * find_derived_clause_for_ec_member() performs lookup of a clause
+		 * involving a constant using only the non-constant EM and NULL for
+		 * the RHS. Since there's only one constant EM per EC, we don't need
+		 * to store or match it during lookup.  We set key.em2 = NULL to
+		 * reflect this.
 		 */
 		key.em1 = rinfo->left_em;
 		key.em2 = NULL;
@@ -3625,8 +3541,9 @@ add_clause_to_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo)
 		entry->rinfo = rinfo;
 
 		/*
-		 * Add another entry to match the clause even if left and right EMs
-		 * have swapped.
+		 * Insert the clause under the given EM pair key, and also under the
+		 * reverse order. This ensures we can find the clause regardless of
+		 * the order in which EMs are passed to the lookup function.
 		 */
 		key.em1 = rinfo->right_em;
 		key.em2 = rinfo->left_em;
@@ -3635,21 +3552,21 @@ add_clause_to_derives_hash(EquivalenceClass *ec, RestrictInfo *rinfo)
 		Assert(!found);
 		entry->rinfo = rinfo;
 	}
-
 }
 
 /*
  * ec_clear_derived_clauses
- *	  Reset the ec_derives list and the hash table.
+ *      Reset ec_derives_list and ec_derives_hash.
  *
- * We destroy the hash table since it consumes more space than the list. But we
- * don't bother to free the list.
+ * We destroy the hash table explicitly, since it may consume significant
+ * space. The list is simply cleared by setting it to NIL; we do not
+ * explicitly free it.
  *
- * XXX: when there are thousands of partitions involved, the list can grow
- * sizable and thus freeing it might be desirable.
+ * XXX: When thousands of partitions are involved, the list can become
+ * sizable. It might be worth freeing it explicitly in such cases.
  */
 void
-clear_ec_derived_clauses(EquivalenceClass *ec)
+ec_clear_derived_clauses(EquivalenceClass *ec)
 {
 	ec->ec_derives_list = NIL;
 	if (ec->ec_derives_hash)
@@ -3658,3 +3575,124 @@ clear_ec_derived_clauses(EquivalenceClass *ec)
 		ec->ec_derives_hash = NULL;
 	}
 }
+
+/*
+ * ec_search_clause_for_ems
+ *		Search for an existing RestrictInfo that equates the given pair
+ *		of EquivalenceMembers, either from ec_sources or ec_derives.
+ *
+ * We accept clauses in either operand order, so commutators are matched. We
+ * used to require matching operator OIDs, but dropped that since any
+ * semantically different operator here would indicate a broken operator
+ * family.
+ *
+ * Returns NULL if no matching clause is found.
+ */
+
+static RestrictInfo *
+ec_search_clause_for_ems(PlannerInfo *root, EquivalenceClass *ec,
+						 EquivalenceMember *leftem, EquivalenceMember *rightem,
+						 EquivalenceClass *parent_ec)
+{
+	/* Check original source clauses */
+	foreach_node(RestrictInfo, rinfo, ec->ec_sources)
+	{
+		if (rinfo->left_em == leftem &&
+			rinfo->right_em == rightem &&
+			rinfo->parent_ec == parent_ec)
+			return rinfo;
+		if (rinfo->left_em == rightem &&
+			rinfo->right_em == leftem &&
+			rinfo->parent_ec == parent_ec)
+			return rinfo;
+	}
+
+	/* Not found in ec_sources; search derived clauses */
+	return ec_search_derived_clause_for_ems(root, ec, leftem, rightem,
+											parent_ec);
+}
+
+/*
+ * ec_search_derived_clause_for_ems
+ *		Search for an existing derived clause between two EquivalenceMembers.
+ *
+ * If the number of derived clauses exceeds a threshold, switch to hash table
+ * lookup; otherwise, scan ec_derives_list linearly.
+ *
+ * Clauses involving constants are looked up using only the non-const EM
+ * (leftem) and a NULL rightem. In that case, we expect to find a clause with
+ * a constant on the RHS.
+ *
+ * We do not attempt a second lookup with EMs swapped when using the hash
+ * table; such clauses are inserted under both orderings at the time of
+ * insertion.
+ */
+static RestrictInfo *
+ec_search_derived_clause_for_ems(PlannerInfo *root, EquivalenceClass *ec,
+								 EquivalenceMember *leftem,
+								 EquivalenceMember *rightem,
+								 EquivalenceClass *parent_ec)
+{
+	/*
+	 * Switch to using hash lookup when list grows "too long". The threshold
+	 * is arbitrary and is known only here.
+	 */
+	if (!ec->ec_derives_hash && list_length(ec->ec_derives_list) >= 32)
+		build_ec_derives_hash(root, ec);
+
+	/* Perform hash table lookup if available */
+	if (ec->ec_derives_hash)
+	{
+		ECDerivesKey key;
+		RestrictInfo *rinfo;
+		ECDerivesEntry *entry;
+
+		/*
+		 * See add_clause_to_ec_derives_hash() for rationale: derived clauses
+		 * are inserted into the hash table under both (em1, em2) and
+		 * (em2, em1), so a single lookup with the original order is
+		 * sufficient.
+		 */
+		key.em1 = leftem;
+		key.em2 = rightem;
+		key.parent_ec = parent_ec;
+		entry = derives_lookup(ec->ec_derives_hash, key);
+		if (entry)
+		{
+			rinfo = entry->rinfo;
+			Assert(rinfo);
+
+			/*
+			 * If this is a lookup in a const-containing EC, the RHS must be a
+			 * constant. The caller signals this by passing NULL for rightem.
+			 */
+			Assert(rightem || rinfo->right_em->em_is_const);
+			return rinfo;
+		}
+	}
+	else
+	{
+		/* Fallback to linear search over ec_derives_list */
+		foreach_node(RestrictInfo, rinfo, ec->ec_derives_list)
+		{
+			/* Handle special case: lookup by non-const EM alone */
+			if (!rightem &&
+				rinfo->left_em == leftem)
+			{
+				/* See the comment above in hash path for rationale. */
+				Assert(rinfo->right_em->em_is_const);
+				return rinfo;
+			}
+			if (rinfo->left_em == leftem &&
+				rinfo->right_em == rightem &&
+				rinfo->parent_ec == parent_ec)
+				return rinfo;
+			if (rinfo->left_em == rightem &&
+				rinfo->right_em == leftem &&
+				rinfo->parent_ec == parent_ec)
+				return rinfo;
+		}
+	}
+
+	return NULL;
+}
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 84489cdc4b4..ae20691ca91 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -749,7 +749,7 @@ remove_rel_from_eclass(EquivalenceClass *ec, SpecialJoinInfo *sjinfo,
 	 * drop them.  (At this point, any such clauses would be base restriction
 	 * clauses, which we'd not need anymore anyway.)
 	 */
-	clear_ec_derived_clauses(ec);
+	ec_clear_derived_clauses(ec);
 }
 
 /*
@@ -1544,7 +1544,7 @@ update_eclasses(EquivalenceClass *ec, int from, int to)
 	list_free(ec->ec_members);
 	ec->ec_members = new_members;
 
-	clear_ec_derived_clauses(ec);
+	ec_clear_derived_clauses(ec);
 
 	/* Update EC source expressions */
 	foreach_node(RestrictInfo, rinfo, ec->ec_sources)
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index d4af0df010e..ff5c69c84aa 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1403,6 +1403,17 @@ typedef struct JoinDomain
  * entry: consider SELECT random() AS a, random() AS b ... ORDER BY b,a.
  * So we record the SortGroupRef of the originating sort clause.
  *
+ * Derived equality clauses between EC members are stored in ec_derives_list.
+ * For small queries, this list is scanned directly during lookup. For larger
+ * queries -- e.g., with many partitions or joins -- a hash table
+ * (ec_derives_hash) is built for faster lookup.  Both structures contain the
+ * same RestrictInfos and are maintained in parallel. We retain the list even
+ * when the hash is used to simplify serialization (e.g., in
+ * _outEquivalenceClass()) and support EquivalenceClass merging.
+ *
+ * In contrast, ec_sources holds equality clauses that appear directly in the
+ * query. These are typically few and do not require a hash table for lookup.
+ *
  * NB: if ec_merged isn't NULL, this class has been merged into another, and
  * should be ignored in favor of using the pointed-to class.
  *
@@ -1422,22 +1433,10 @@ typedef struct EquivalenceClass
 	Oid			ec_collation;	/* collation, if datatypes are collatable */
 	List	   *ec_members;		/* list of EquivalenceMembers */
 	List	   *ec_sources;		/* list of generating RestrictInfos */
-
-	/*
-	 * ec_derives_list is a list of derived RestrictInfos. For small problems
-	 * we just scan the list to lookup RestrictInfos with given
-	 * EquivalenceMembers, but when there are many derived clauses, because of
-	 * many partitions or many tables being joined or both, we build hash
-	 * table for faster lookups. The hash table is present and valid when
-	 * ec_derives_hash is not NULL. Note that we still maintain the list even
-	 * when using the hash table for lookups; this simplifies life for
-	 * _outEquivalenceClass() and when merging two EquivalenceClasses.
-	 *
-	 * Note that ec_sources contains the clauses, mentioned in the query,
-	 * which are fewer. It does not need a hash table for lookups.
-	 */
-	List	   *ec_derives_list;
-	struct derives_hash *ec_derives_hash;
+	List	   *ec_derives_list;	/* list of derived RestrictInfos */
+	struct derives_hash *ec_derives_hash;	/* optional hash table for fast
+											 * lookup; contains same
+											 * RestrictInfos as list */
 	Relids		ec_relids;		/* all relids appearing in ec_members, except
 								 * for child members (see below) */
 	bool		ec_has_const;	/* any pseudoconstants in ec_members? */
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index f80fcf606d6..16dc4d5ee82 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -198,7 +198,7 @@ extern bool eclass_useful_for_merging(PlannerInfo *root,
 extern bool is_redundant_derived_clause(RestrictInfo *rinfo, List *clauselist);
 extern bool is_redundant_with_indexclauses(RestrictInfo *rinfo,
 										   List *indexclauses);
-extern void clear_ec_derived_clauses(EquivalenceClass *ec);
+extern void ec_clear_derived_clauses(EquivalenceClass *ec);
 
 /*
  * pathkeys.c
-- 
2.43.0

