Hi hackers,

While testing ANALYZE with large statistics targets, I found that building ndistinct extended statistics leaks memory proportional to the sample size for every combination of columns.

Example:
postgresql.conf settings
```
default_statistics_target = 1000
```
Commands
```
SET client_min_messages = log;
SET log_statement_stats = on;
DROP TABLE IF EXISTS ndist_leak;
CREATE TABLE ndist_leak AS
SELECT (random() * 3000)::int AS a,
              (random() * 3000)::int AS b,
              (random() * 50)::int   AS c,
              (random() * 20)::int   AS d,
              (random() * 10)::int   AS e,
              (random() * 5)::int    AS f
FROM generate_series(1, 1000000);

CREATE STATISTICS ndist_leak_nd (ndistinct) ON a, b, c, d, e, f FROM ndist_leak;

ANALYZE ndist_leak;
```

*Peak memory of the backend:*
Before patch - 971076 kB max resident size
After    patch - 112780 kB max resident size

ndistinct_for_combination() allocates the items, values and isnull arrays for the whole sample, plus the MultiSortSupport, and never frees them. statext_ndistinct_build() calls it once per combination of columns, so these allocations accumulate until the whole ndistinct object is built.

The attached patch changes statext_ndistinct_build() to call ndistinct_for_combination() in a separate memory context and reset it after each combination, the same way as is already done for dependency_degree() in statext_dependencies_build().

Similar memory issues in extended statistics were fixed in [0]. The original report there actually used ndistinct objects, but the fixes addressed memory accumulated across statistics objects and in dependency_degree(); the per-combination allocations in ndistinct_for_combination() were not discussed there.

Any suggestions?

[0]: https://www.postgresql.org/message-id/20210915200928.GP831%40telsasoft.com

--
Best regards,
Ilia Evdokimov,
Tantor Labs LLC,
https://tantorlabs.com/
From 70dac855e18ce19aad4d0c9b98028da1a6d9e470 Mon Sep 17 00:00:00 2001
From: Evdokimov Ilia <[email protected]>
Date: Fri, 9 Oct 2026 00:02:22 +0500
Subject: [PATCH v1] Release memory allocated by ndistinct_for_combination

Building ndistinct extended statistics calls ndistinct_for_combination()
once for every combination of columns.  Each call allocates arrays for
the whole sample (SortItems, Datums and null flags), plus the
MultiSortSupport, and never frees them, so the memory accumulates until
the whole statistics object is built.  The sample has 300 * target rows,
so with large statistics targets this can add up to gigabytes;

Fixed by calling ndistinct_for_combination() in a dedicated memory
context and resetting it after each combination, as is already done
for dependency_degree() in statext_dependencies_build().  We only need
the estimated number of distinct values, so we don't need to copy
anything out of the context.
---
 src/backend/statistics/mvdistinct.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 601f108a2b4..0add6455f6a 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
 #include "statistics/extended_stats_internal.h"
+#include "utils/memutils.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 #include "varatt.h"
@@ -89,6 +90,7 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data)
 	uint32		itemcnt;
 	int			numattrs = data->nattnums;
 	int			numcombs = num_combinations(numattrs);
+	MemoryContext cxt;
 
 	result = palloc(offsetof(MVNDistinct, items) +
 					numcombs * sizeof(MVNDistinctItem));
@@ -96,6 +98,10 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data)
 	result->type = STATS_NDISTINCT_TYPE_BASIC;
 	result->nitems = numcombs;
 
+	cxt = AllocSetContextCreate(CurrentMemoryContext,
+								"ndistinct_for_combination cxt",
+								ALLOCSET_DEFAULT_SIZES);
+
 	itemcnt = 0;
 	for (k = 2; k <= numattrs; k++)
 	{
@@ -109,6 +115,7 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data)
 		{
 			MVNDistinctItem *item = &result->items[itemcnt];
 			int			j;
+			MemoryContext oldcxt;
 
 			item->attributes = palloc_array(AttrNumber, k);
 			item->nattributes = k;
@@ -121,8 +128,12 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data)
 				Assert(AttributeNumberIsValid(item->attributes[j]));
 			}
 
+			/* release memory used by ndistinct calculation */
+			oldcxt = MemoryContextSwitchTo(cxt);
 			item->ndistinct =
 				ndistinct_for_combination(totalrows, data, k, combination);
+			MemoryContextSwitchTo(oldcxt);
+			MemoryContextReset(cxt);
 
 			itemcnt++;
 			Assert(itemcnt <= result->nitems);
@@ -131,6 +142,8 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data)
 		generator_free(generator);
 	}
 
+	MemoryContextDelete(cxt);
+
 	/* must consume exactly the whole output array */
 	Assert(itemcnt == result->nitems);
 
-- 
2.53.0

Reply via email to