Hi,
I kept poking at the patch releasing some transient sublists, trying to
make it release even more memory. Attached is a couple hacky patches
releasing memory in a couple more places:
0001 - Tom's patch releasing a couple transient lists
0002 - releases a couple more transient lists in nearby functions
0003 - release yet more transient lists in equivclass.c
0004 - release transient lists in estimate_num_groups
0005 - free lists used by mergejoin paths
0006 - free lists used by hahsjoin paths
I think 0002-0004 are a relatively straightforward extensions of the
0001 patch, releasing lists that are not used outside a function.
0005+0006 are a bit more invasive, but also save much more memory.
The places constructing mergejoin/hashjoin paths build lists for
inner/outer keys, merge/hash clauses etc. But then those are leaked,
because (a) it's not clear if a join using those lists was actually
created (or if add_path threw it away), and (b) add_path may keep it and
discard it sometime later. But that releases just the path, not the lists.
So 0005+0006 change the contract a bit:
(a) create_hashjoin_path/create_mergejoin_path always copy these list
(b) add_path frees these list when discarding a mergejoin/hashjoin path
(c) the places creating join paths free the lists too
This is a bit ugly, because sometimes the lists may be the same etc. But
it does work well enough for a hacky PoC (passes make check).
With these patches, the memory usage (per log_planner_stats) drops to
~500MB. That's a nice improvement, from the original 2.6GB. The larger
query that used ~17GB now needs ~3GB of memory.
I was wondering how this impacts planning time, so I ran the original
test with 2-11 tables, with 1000 joins for each join size. Attached are
two charts showing the average plan time and memory usage. The patched
build seems consistently faster - for smaller joins (up to ~8 tables)
the differences are small, not really visible in these charts. As the
joins grow it's getting much more visible, and the differences increase.
I redid the memory tracing for the "smaller" query (which now needs
~500MB), and it looks like this
allocation_group | pg_size_pretty
--------------------------------------+----------------
create_memoize_path | 92 MB
get_joinrel_parampathinfo | 65 MB
estimate_num_groups | 47 MB
add_paths_to_joinrel | 38 MB
generate_join_implied_equalities | 24 MB
| 15 MB
create_material_path | 15 MB
calc_nestloop_required_outer | 5224 kB
find_mergeclauses_for_outer_pathkeys | 0 bytes
make_inner_pathkeys_for_merge | 0 bytes
(10 rows)
It shouldn't be hard to get rid of estimate_num_groups entirely, by
freeing the GroupVarInfo entries (and not just the lists).
But after that, it's going to be harder. I'm not sure what to do about
the memoize/material paths - we're leaking these paths because various
places in joinpath.c try to "inject" these paths below a join. But this
way the logic in add_path may not free the path - we may not even get to
add_path, and even if we do, it operates on the join, not this new made
up memoize/material path. The callers have no idea if the path happens
to be used or not, so can't free it either.
I'm not sure what to do about this. I suppose it'd be good to have a
better idea if the path got used, in some way.
regards
--
Tomas Vondra
From 0f829fa3a3e7a0ea6b2c9155784d7976339ec963 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Tue, 9 Jun 2026 00:06:00 +0200
Subject: [PATCH v2 1/6] Tom's patch
---
src/backend/optimizer/path/equivclass.c | 3 ++-
src/backend/optimizer/path/pathkeys.c | 15 ++++++---------
2 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index e3697df51a2..1ee6398087e 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -1602,7 +1602,7 @@ generate_join_implied_equalities(PlannerInfo *root,
while ((i = bms_next_member(matching_ecs, i)) >= 0)
{
EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes, i);
- List *sublist = NIL;
+ List *sublist;
/* ECs containing consts do not need any further enforcement */
if (ec->ec_has_const)
@@ -1632,6 +1632,7 @@ generate_join_implied_equalities(PlannerInfo *root,
inner_rel);
result = list_concat(result, sublist);
+ list_free(sublist);
}
return result;
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 5eb71635d15..978ac1240d7 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -1560,7 +1560,7 @@ find_mergeclauses_for_outer_pathkeys(PlannerInfo *root,
{
PathKey *pathkey = (PathKey *) lfirst(i);
EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
- List *matched_restrictinfos = NIL;
+ bool found_clauses = false;
ListCell *j;
/*----------
@@ -1608,7 +1608,10 @@ find_mergeclauses_for_outer_pathkeys(PlannerInfo *root,
clause_ec = rinfo->outer_is_left ?
rinfo->left_ec : rinfo->right_ec;
if (clause_ec == pathkey_ec)
- matched_restrictinfos = lappend(matched_restrictinfos, rinfo);
+ {
+ mergeclauses = lappend(mergeclauses, rinfo);
+ found_clauses = true;
+ }
}
/*
@@ -1616,14 +1619,8 @@ find_mergeclauses_for_outer_pathkeys(PlannerInfo *root,
* sort-key positions in the pathkeys are useless. (But we can still
* mergejoin if we found at least one mergeclause.)
*/
- if (matched_restrictinfos == NIL)
+ if (!found_clauses)
break;
-
- /*
- * If we did find usable mergeclause(s) for this sort-key position,
- * add them to result list.
- */
- mergeclauses = list_concat(mergeclauses, matched_restrictinfos);
}
return mergeclauses;
--
2.54.0
From 9c47380c6da3ff78617ae80b1c8410a79b2ea396 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Tue, 9 Jun 2026 00:11:47 +0200
Subject: [PATCH v2 2/6] release more lists
---
src/backend/optimizer/path/equivclass.c | 1 +
src/backend/optimizer/util/relnode.c | 5 +++++
2 files changed, 6 insertions(+)
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 1ee6398087e..32e169a70e8 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -1709,6 +1709,7 @@ generate_join_implied_equalities_for_ecs(PlannerInfo *root,
inner_rel);
result = list_concat(result, sublist);
+ list_free(sublist);
}
return result;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 687e923c46c..88562b6602a 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -1910,6 +1910,8 @@ get_joinrel_parampathinfo(PlannerInfo *root, RelOptInfo *joinrel,
pclauses = lappend(pclauses, rinfo);
}
+ list_free(eclauses);
+
/*
* EquivalenceClasses are harder to deal with than we could wish, because
* of the fact that a given EC can generate different clauses depending on
@@ -1964,6 +1966,9 @@ get_joinrel_parampathinfo(PlannerInfo *root, RelOptInfo *joinrel,
outer_and_req))
pclauses = lappend(pclauses, rinfo);
}
+
+ list_free(dropped_ecs);
+ list_free(eclauses);
}
/*
--
2.54.0
From a11dec8c214b083884f5a79a4bd8ebb3a8b2a59a Mon Sep 17 00:00:00 2001
From: test <test>
Date: Tue, 9 Jun 2026 01:35:31 +0200
Subject: [PATCH v2 3/6] free lists in equivclasses
---
src/backend/optimizer/path/equivclass.c | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 32e169a70e8..7caa3defcc4 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -1823,6 +1823,10 @@ generate_join_implied_equalities_normal(PlannerInfo *root,
{
/* failed... */
ec->ec_broken = true;
+ list_free(result);
+ list_free(new_members);
+ list_free(inner_members);
+ list_free(outer_members);
return NIL;
}
@@ -1872,6 +1876,10 @@ generate_join_implied_equalities_normal(PlannerInfo *root,
{
/* failed... */
ec->ec_broken = true;
+ list_free(result);
+ list_free(new_members);
+ list_free(inner_members);
+ list_free(outer_members);
return NIL;
}
/* do NOT set parent_ec, this qual is not redundant! */
@@ -1885,6 +1893,10 @@ generate_join_implied_equalities_normal(PlannerInfo *root,
}
}
+ list_free(new_members);
+ list_free(inner_members);
+ list_free(outer_members);
+
return result;
}
--
2.54.0
From 18e9c7db5695bf5386d552f4c396110eeff2f568 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Tue, 9 Jun 2026 00:19:46 +0200
Subject: [PATCH v2 4/6] fix estimate_num_groups
---
src/backend/utils/adt/selfuncs.c | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d6efd07073a..5bd705c93ab 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -3915,7 +3915,10 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
if (varshere == NIL)
{
if (contain_volatile_functions(groupexpr))
+ {
+ list_free(varinfos);
return input_rows;
+ }
continue;
}
@@ -4033,6 +4036,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
}
/* we're done with this relation */
+ list_free(relvarinfos);
relvarinfos = NIL;
}
}
@@ -4119,6 +4123,9 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
numdistinct *= reldistinct;
}
+ list_free(varinfos);
+ list_free_deep(relvarinfos);
+
varinfos = newvarinfos;
} while (varinfos != NIL);
@@ -4134,6 +4141,8 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
if (numdistinct < 1.0)
numdistinct = 1.0;
+ list_free(varinfos);
+
return numdistinct;
}
--
2.54.0
From 012a966983d2316245ce847d2889db0ec2bec43f Mon Sep 17 00:00:00 2001
From: test <test>
Date: Tue, 9 Jun 2026 00:17:33 +0200
Subject: [PATCH v2 5/6] free mergejoin keys
---
src/backend/optimizer/path/joinpath.c | 18 ++++++++++-
src/backend/optimizer/util/pathnode.c | 44 ++++++++++++++++++++++++---
2 files changed, 57 insertions(+), 5 deletions(-)
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 713283a73aa..9396ce4c145 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -379,6 +379,8 @@ add_paths_to_joinrel(PlannerInfo *root,
if (set_join_pathlist_hook)
set_join_pathlist_hook(root, joinrel, outerrel, innerrel,
save_jointype, &extra);
+
+ list_free(extra.mergeclause_list);
}
/*
@@ -1519,7 +1521,7 @@ sort_inner_and_outer(PlannerInfo *root,
list_delete_nth_cell(list_copy(all_pathkeys),
foreach_current_index(l)));
else
- outerkeys = all_pathkeys; /* no work at first one... */
+ outerkeys = list_copy(all_pathkeys); /* no work at first one... */
/* Sort the mergeclauses into the corresponding ordering */
cur_mergeclauses =
@@ -1573,7 +1575,15 @@ sort_inner_and_outer(PlannerInfo *root,
innerkeys,
jointype,
extra);
+
+ list_free(cur_mergeclauses);
+ list_free(innerkeys);
+
+ if ((outerkeys != merge_pathkeys) && (innerkeys != merge_pathkeys))
+ list_free(outerkeys);
}
+
+ list_free(all_pathkeys);
}
/*
@@ -1634,7 +1644,10 @@ generate_mergejoin_paths(PlannerInfo *root,
}
if (useallclauses &&
list_length(mergeclauses) != list_length(extra->mergeclause_list))
+ {
+ list_free(mergeclauses);
return;
+ }
/* Compute the required ordering of the inner path */
innersortkeys = make_inner_pathkeys_for_merge(root,
@@ -1805,6 +1818,9 @@ generate_mergejoin_paths(PlannerInfo *root,
if (useallclauses)
break;
}
+
+ list_free(innersortkeys);
+ list_free(mergeclauses);
}
/*
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 73518c8f870..911f0de8c82 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -624,6 +624,14 @@ add_path(RelOptInfo *parent_rel, Path *new_path)
parent_rel->pathlist = foreach_delete_current(parent_rel->pathlist,
p1);
+ if (IsA(old_path, MergePath))
+ {
+ MergePath *mjpath = (MergePath *) old_path;
+ list_free(mjpath->path_mergeclauses);
+ list_free(mjpath->innersortkeys);
+ list_free(mjpath->outersortkeys);
+ list_free(mjpath->jpath.path.pathkeys);
+ }
/*
* Delete the data pointed-to by the deleted cell, if possible
*/
@@ -659,6 +667,15 @@ add_path(RelOptInfo *parent_rel, Path *new_path)
}
else
{
+ if (IsA(new_path, MergePath))
+ {
+ MergePath *mjpath = (MergePath *) new_path;
+ list_free(mjpath->path_mergeclauses);
+ list_free(mjpath->innersortkeys);
+ list_free(mjpath->outersortkeys);
+ list_free(mjpath->jpath.path.pathkeys);
+ }
+
/* Reject and recycle the new path */
if (!IsA(new_path, IndexPath))
pfree(new_path);
@@ -863,6 +880,16 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
{
parent_rel->partial_pathlist =
foreach_delete_current(parent_rel->partial_pathlist, p1);
+
+ if (IsA(old_path, MergePath))
+ {
+ MergePath *mjpath = (MergePath *) old_path;
+ list_free(mjpath->path_mergeclauses);
+ list_free(mjpath->innersortkeys);
+ list_free(mjpath->outersortkeys);
+ list_free(mjpath->jpath.path.pathkeys);
+ }
+
pfree(old_path);
}
else
@@ -895,6 +922,15 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
else
{
/* Reject and recycle the new path */
+ if (IsA(new_path, MergePath))
+ {
+ MergePath *mjpath = (MergePath *) new_path;
+ list_free(mjpath->path_mergeclauses);
+ list_free(mjpath->innersortkeys);
+ list_free(mjpath->outersortkeys);
+ list_free(mjpath->jpath.path.pathkeys);
+ }
+
pfree(new_path);
}
}
@@ -2483,15 +2519,15 @@ create_mergejoin_path(PlannerInfo *root,
outer_path->parallel_safe && inner_path->parallel_safe;
/* This is a foolish way to estimate parallel_workers, but for now... */
pathnode->jpath.path.parallel_workers = outer_path->parallel_workers;
- pathnode->jpath.path.pathkeys = pathkeys;
+ pathnode->jpath.path.pathkeys = list_copy(pathkeys);
pathnode->jpath.jointype = jointype;
pathnode->jpath.inner_unique = extra->inner_unique;
pathnode->jpath.outerjoinpath = outer_path;
pathnode->jpath.innerjoinpath = inner_path;
pathnode->jpath.joinrestrictinfo = restrict_clauses;
- pathnode->path_mergeclauses = mergeclauses;
- pathnode->outersortkeys = outersortkeys;
- pathnode->innersortkeys = innersortkeys;
+ pathnode->path_mergeclauses = list_copy(mergeclauses);
+ pathnode->outersortkeys = list_copy(outersortkeys);
+ pathnode->innersortkeys = list_copy(innersortkeys);
pathnode->outer_presorted_keys = outer_presorted_keys;
/* pathnode->skip_mark_restore will be set by final_cost_mergejoin */
/* pathnode->materialize_inner will be set by final_cost_mergejoin */
--
2.54.0
From 0f57a9d6c01548c746338023ff576cbd4499041c Mon Sep 17 00:00:00 2001
From: test <test>
Date: Tue, 9 Jun 2026 00:19:24 +0200
Subject: [PATCH v2 6/6] free hashjoin keys
---
src/backend/optimizer/path/joinpath.c | 5 +++++
src/backend/optimizer/util/pathnode.c | 22 +++++++++++++++++++++-
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 9396ce4c145..d6475985c04 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -2278,7 +2278,10 @@ hash_inner_and_outer(PlannerInfo *root,
*/
if (PATH_PARAM_BY_REL(cheapest_total_outer, innerrel) ||
PATH_PARAM_BY_REL(cheapest_total_inner, outerrel))
+ {
+ list_free(hashclauses);
return;
+ }
/*
* Consider the cheapest startup outer together with the cheapest
@@ -2400,6 +2403,8 @@ hash_inner_and_outer(PlannerInfo *root,
false /* parallel_hash */ );
}
}
+
+ list_free(hashclauses);
}
/*
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 911f0de8c82..1d5cd429516 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -632,6 +632,11 @@ add_path(RelOptInfo *parent_rel, Path *new_path)
list_free(mjpath->outersortkeys);
list_free(mjpath->jpath.path.pathkeys);
}
+ else if (IsA(old_path, HashPath))
+ {
+ HashPath *hjpath = (HashPath *) old_path;
+ list_free(hjpath->path_hashclauses);
+ }
/*
* Delete the data pointed-to by the deleted cell, if possible
*/
@@ -675,6 +680,11 @@ add_path(RelOptInfo *parent_rel, Path *new_path)
list_free(mjpath->outersortkeys);
list_free(mjpath->jpath.path.pathkeys);
}
+ else if (IsA(new_path, HashPath))
+ {
+ HashPath *hjpath = (HashPath *) new_path;
+ list_free(hjpath->path_hashclauses);
+ }
/* Reject and recycle the new path */
if (!IsA(new_path, IndexPath))
@@ -889,6 +899,11 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
list_free(mjpath->outersortkeys);
list_free(mjpath->jpath.path.pathkeys);
}
+ else if (IsA(old_path, HashPath))
+ {
+ HashPath *hjpath = (HashPath *) old_path;
+ list_free(hjpath->path_hashclauses);
+ }
pfree(old_path);
}
@@ -930,6 +945,11 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
list_free(mjpath->outersortkeys);
list_free(mjpath->jpath.path.pathkeys);
}
+ else if (IsA(new_path, HashPath))
+ {
+ HashPath *hjpath = (HashPath *) new_path;
+ list_free(hjpath->path_hashclauses);
+ }
pfree(new_path);
}
@@ -2603,7 +2623,7 @@ create_hashjoin_path(PlannerInfo *root,
pathnode->jpath.outerjoinpath = outer_path;
pathnode->jpath.innerjoinpath = inner_path;
pathnode->jpath.joinrestrictinfo = restrict_clauses;
- pathnode->path_hashclauses = hashclauses;
+ pathnode->path_hashclauses = list_copy(hashclauses);
/* final_cost_hashjoin will fill in pathnode->num_batches */
final_cost_hashjoin(root, pathnode, workspace, extra);
--
2.54.0