explain HashAggregate to report bucket and memory stats

Started by Justin Pryzbyabout 6 years ago24 messages
#1Justin Pryzby
pryzby@telsasoft.com
2 attachment(s)

On Sun, Feb 17, 2019 at 11:29:56AM -0500, Jeff Janes wrote:
/messages/by-id/CAMkU=1zBJNVo2DGYBgLJqpu8fyjCE_ys+msr6pOEoiwA7y5jrA@mail.gmail.com

What would I find very useful is [...] if the HashAggregate node under
"explain analyze" would report memory and bucket stats; and if the Aggregate
node would report...anything.

Find attached my WIP attempt to implement this.

Jeff: can you suggest what details Aggregate should show ?

Justin

Attachments:

v1-0001-refactor-show_hinstrument-and-avoid-showing-memor.patchtext/x-diff; charset=us-asciiDownload
From 5d0afe5d92649f575d9b09ae19b31d2bfd5bfd12 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 1 Jan 2020 13:09:33 -0600
Subject: [PATCH v1 1/2] refactor: show_hinstrument and avoid showing memory
 use if not verbose..

This changes explain analyze at least for Hash(join), but doesn't break
affect regression tests, since they all run explain without analyze, so
nbatch=0, and no stats are shown.

But for future patch to show stats for HashAgg (for which nbatch=1, always), we
want to show buckets in explain analyze, but don't want to show memory, which
is machine-specific.
---
 src/backend/commands/explain.c | 73 ++++++++++++++++++++++++++----------------
 1 file changed, 45 insertions(+), 28 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 497a3bd..d5eaf15 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -101,6 +101,7 @@ static void show_sortorder_options(StringInfo buf, Node *sortexpr,
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
+static void show_hinstrument(ExplainState *es, HashInstrumentation *h);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
@@ -2702,43 +2703,59 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 		}
 	}
 
-	if (hinstrument.nbatch > 0)
-	{
-		long		spacePeakKb = (hinstrument.space_peak + 1023) / 1024;
+	show_hinstrument(es, &hinstrument);
+}
 
-		if (es->format != EXPLAIN_FORMAT_TEXT)
-		{
-			ExplainPropertyInteger("Hash Buckets", NULL,
-								   hinstrument.nbuckets, es);
-			ExplainPropertyInteger("Original Hash Buckets", NULL,
-								   hinstrument.nbuckets_original, es);
-			ExplainPropertyInteger("Hash Batches", NULL,
-								   hinstrument.nbatch, es);
-			ExplainPropertyInteger("Original Hash Batches", NULL,
-								   hinstrument.nbatch_original, es);
-			ExplainPropertyInteger("Peak Memory Usage", "kB",
-								   spacePeakKb, es);
-		}
-		else if (hinstrument.nbatch_original != hinstrument.nbatch ||
-				 hinstrument.nbuckets_original != hinstrument.nbuckets)
-		{
+/*
+ * Show hash bucket stats and (optionally) memory.
+ */
+static void
+show_hinstrument(ExplainState *es, HashInstrumentation *h)
+{
+	long		spacePeakKb = (h->space_peak + 1023) / 1024;
+
+	if (h->nbatch <= 0)
+		return;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   h->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   h->nbuckets_original, es);
+		ExplainPropertyInteger("Hash Batches", NULL,
+							   h->nbatch, es);
+		ExplainPropertyInteger("Original Hash Batches", NULL,
+							   h->nbatch_original, es);
+		ExplainPropertyInteger("Peak Memory Usage", "kB",
+							   spacePeakKb, es);
+	}
+	else
+	{
+		if (h->nbatch_original != h->nbatch ||
+			 h->nbuckets_original != h->nbuckets) {
 			appendStringInfoSpaces(es->str, es->indent * 2);
 			appendStringInfo(es->str,
-							 "Buckets: %d (originally %d)  Batches: %d (originally %d)  Memory Usage: %ldkB\n",
-							 hinstrument.nbuckets,
-							 hinstrument.nbuckets_original,
-							 hinstrument.nbatch,
-							 hinstrument.nbatch_original,
-							 spacePeakKb);
+						"Buckets: %d (originally %d)   Batches: %d (originally %d)",
+						h->nbuckets,
+						h->nbuckets_original,
+						h->nbatch,
+						h->nbatch_original);
 		}
 		else
 		{
 			appendStringInfoSpaces(es->str, es->indent * 2);
 			appendStringInfo(es->str,
-							 "Buckets: %d  Batches: %d  Memory Usage: %ldkB\n",
-							 hinstrument.nbuckets, hinstrument.nbatch,
-							 spacePeakKb);
+						"Buckets: %d  Batches: %d",
+						h->nbuckets,
+						h->nbatch);
 		}
+
+		if (es->verbose && es->analyze)
+			appendStringInfo(es->str,
+					"  Memory Usage: %ldkB",
+					spacePeakKb);
+		appendStringInfoChar(es->str, '\n');
 	}
 }
 
-- 
2.7.4

v1-0002-explain-analyze-to-show-stats-from-hash-aggregate.patchtext/x-diff; charset=us-asciiDownload
From d691e492b619e5cc6a1fcd4134728c1c0852d589 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v1 2/2] explain analyze to show stats from (hash) aggregate..

..as suggested by Jeff Janes
---
 src/backend/commands/explain.c      | 50 +++++++++++++++++++++++++++++++------
 src/backend/executor/execGrouping.c | 10 ++++++++
 src/include/executor/nodeAgg.h      |  1 +
 src/include/nodes/execnodes.h       | 27 ++++++++++----------
 4 files changed, 67 insertions(+), 21 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d5eaf15..22d6087 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -82,6 +83,7 @@ static void show_sort_keys(SortState *sortstate, List *ancestors,
 						   ExplainState *es);
 static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
+static void show_agg_info(AggState *astate, ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
 static void show_grouping_sets(PlanState *planstate, Agg *agg,
@@ -101,7 +103,7 @@ static void show_sortorder_options(StringInfo buf, Node *sortexpr,
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
-static void show_hinstrument(ExplainState *es, HashInstrumentation *h);
+static void show_hinstrument(ExplainState *es, HashInstrumentation *h, bool showbatch);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
@@ -1848,6 +1850,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			show_agg_info(castNode(AggState, planstate), es);
+
 			break;
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
@@ -2057,6 +2061,23 @@ ExplainNode(PlanState *planstate, List *ancestors,
 }
 
 /*
+ * Show instrumentation info for an Agg node.
+ */
+static void
+show_agg_info(AggState *astate, ExplainState *es)
+{
+	// perhash->aggnode->numGroups; memctx; AggState->
+
+	for (int i=0; i<astate->num_hashes; ++i) {
+		HashInstrumentation *hinstrument = &astate->perhash->hashtable->hinstrument;
+// fprintf(stderr, "memallocated %lu\n", astate->hashcontext->ecxt_per_query_memory->mem_allocated);
+		show_hinstrument(es, hinstrument, false);
+	}
+
+	// TODO
+}
+
+/*
  * Show the targetlist of a plan node
  */
 static void
@@ -2703,19 +2724,26 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 		}
 	}
 
-	show_hinstrument(es, &hinstrument);
+	show_hinstrument(es, &hinstrument, true);
 }
 
 /*
  * Show hash bucket stats and (optionally) memory.
  */
 static void
-show_hinstrument(ExplainState *es, HashInstrumentation *h)
+show_hinstrument(ExplainState *es, HashInstrumentation *h, bool showbatch)
 {
 	long		spacePeakKb = (h->space_peak + 1023) / 1024;
 
+	// Currently, this isn't shown for explain of hash(join) since nbatch=0 without analyze
+	// But, it's shown for hashAgg since nbatch=1, always.
+	// Need to 1) avoid showing memory use if !analyze; and, 2) avoid memory use if not verbose
+
 	if (h->nbatch <= 0)
 		return;
+	/* This avoids showing anything if it's explain without analyze; should we just check that, instead ? */
+	if (!es->analyze)
+		return;
 
 	if (es->format != EXPLAIN_FORMAT_TEXT)
 	{
@@ -2736,9 +2764,12 @@ show_hinstrument(ExplainState *es, HashInstrumentation *h)
 			 h->nbuckets_original != h->nbuckets) {
 			appendStringInfoSpaces(es->str, es->indent * 2);
 			appendStringInfo(es->str,
-						"Buckets: %d (originally %d)   Batches: %d (originally %d)",
+						"Buckets: %d (originally %d)",
 						h->nbuckets,
-						h->nbuckets_original,
+						h->nbuckets_original);
+			if (showbatch)
+				appendStringInfo(es->str,
+						"  Batches: %d (originally %d)",
 						h->nbatch,
 						h->nbatch_original);
 		}
@@ -2746,9 +2777,12 @@ show_hinstrument(ExplainState *es, HashInstrumentation *h)
 		{
 			appendStringInfoSpaces(es->str, es->indent * 2);
 			appendStringInfo(es->str,
-						"Buckets: %d  Batches: %d",
-						h->nbuckets,
-						h->nbatch);
+						"Buckets: %d",
+						h->nbuckets);
+				if (showbatch)
+					appendStringInfo(es->str,
+							"  Batches: %d",
+							h->nbatch);
 		}
 
 		if (es->verbose && es->analyze)
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 3603c58..cf0fe3c 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -203,6 +203,11 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	hashtable->hinstrument.nbuckets_original = nbuckets;
+	hashtable->hinstrument.nbuckets = nbuckets;
+	hashtable->hinstrument.space_peak = entrysize * hashtable->hashtab->size;
+	hashtable->hinstrument.nbatch_original = 1; /* Unused */
+	hashtable->hinstrument.nbatch = 1; /* Unused */
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -328,6 +333,11 @@ LookupTupleHashEntry(TupleHashTable hashtable, TupleTableSlot *slot,
 		{
 			/* created new entry */
 			*isnew = true;
+			/* maybe grew ? XXX: need to call max() here ? */
+			hashtable->hinstrument.nbuckets = hashtable->hashtab->size;
+			hashtable->hinstrument.space_peak = sizeof(TupleHashEntryData) * hashtable->hashtab->size +
+				sizeof(MinimalTuple) * hashtable->hashtab->size; // members ?
+
 			/* zero caller data */
 			entry->additional = NULL;
 			MemoryContextSwitchTo(hashtable->tablecxt);
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 2fe82da..b2ab74c 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -302,6 +302,7 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	// XXX struct HashInstrumentation hinstrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index eaea1f3..ef83406 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -671,6 +671,19 @@ typedef struct ExecAuxRowMark
 typedef struct TupleHashEntryData *TupleHashEntry;
 typedef struct TupleHashTableData *TupleHashTable;
 
+/* ----------------
+ *	 Values displayed by EXPLAIN ANALYZE
+ * ----------------
+ */
+typedef struct HashInstrumentation
+{
+	int			nbuckets;		/* number of buckets at end of execution */
+	int			nbuckets_original;	/* planned number of buckets */
+	int			nbatch;			/* number of batches at end of execution */
+	int			nbatch_original;	/* planned number of batches */
+	size_t		space_peak;		/* peak memory usage in bytes */
+} HashInstrumentation;
+
 typedef struct TupleHashEntryData
 {
 	MinimalTuple firstTuple;	/* copy of first tuple in this group */
@@ -705,6 +718,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	struct HashInstrumentation hinstrument;	/* XXX: NOT A POINTER this worker's entry */
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -2235,19 +2249,6 @@ typedef struct GatherMergeState
 } GatherMergeState;
 
 /* ----------------
- *	 Values displayed by EXPLAIN ANALYZE
- * ----------------
- */
-typedef struct HashInstrumentation
-{
-	int			nbuckets;		/* number of buckets at end of execution */
-	int			nbuckets_original;	/* planned number of buckets */
-	int			nbatch;			/* number of batches at end of execution */
-	int			nbatch_original;	/* planned number of batches */
-	size_t		space_peak;		/* peak memory usage in bytes */
-} HashInstrumentation;
-
-/* ----------------
  *	 Shared memory container for per-worker hash information
  * ----------------
  */
-- 
2.7.4

#2Justin Pryzby
pryzby@telsasoft.com
In reply to: Justin Pryzby (#1)
Re: explain HashAggregate to report bucket and memory stats

On Fri, Jan 03, 2020 at 10:19:25AM -0600, Justin Pryzby wrote:

On Sun, Feb 17, 2019 at 11:29:56AM -0500, Jeff Janes wrote:
/messages/by-id/CAMkU=1zBJNVo2DGYBgLJqpu8fyjCE_ys+msr6pOEoiwA7y5jrA@mail.gmail.com

What would I find very useful is [...] if the HashAggregate node under
"explain analyze" would report memory and bucket stats; and if the Aggregate
node would report...anything.

Find attached my WIP attempt to implement this.

Jeff: can you suggest what details Aggregate should show ?

Rebased on top of 10013684970453a0ddc86050bba813c611114321
And added https://commitfest.postgresql.org/27/2428/

#3Justin Pryzby
pryzby@telsasoft.com
In reply to: Justin Pryzby (#2)
2 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

On Sun, Jan 26, 2020 at 08:14:25AM -0600, Justin Pryzby wrote:

On Fri, Jan 03, 2020 at 10:19:25AM -0600, Justin Pryzby wrote:

On Sun, Feb 17, 2019 at 11:29:56AM -0500, Jeff Janes wrote:
/messages/by-id/CAMkU=1zBJNVo2DGYBgLJqpu8fyjCE_ys+msr6pOEoiwA7y5jrA@mail.gmail.com

What would I find very useful is [...] if the HashAggregate node under
"explain analyze" would report memory and bucket stats; and if the Aggregate
node would report...anything.

Find attached my WIP attempt to implement this.

Jeff: can you suggest what details Aggregate should show ?

Rebased on top of 10013684970453a0ddc86050bba813c611114321
And added https://commitfest.postgresql.org/27/2428/

Attached for real.

Attachments:

v1-0001-refactor-show_hinstrument-and-avoid-showing-memor.patchtext/x-diff; charset=us-asciiDownload
From 42634353f44ab44a06ffe4ae36a0dcbb848408ca Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 1 Jan 2020 13:09:33 -0600
Subject: [PATCH v1 1/2] refactor: show_hinstrument and avoid showing memory
 use if not verbose..

This changes explain analyze at least for Hash(join), but doesn't affect
regression tests, since all the HashJoin tests seem to run explain without
analyze, so nbatch=0, and no stats are shown.

But for future patch to show stats for HashAgg (for which nbatch=1, always), we
want to show buckets in explain analyze, but don't want to show memory, since
it's machine-specific.
---
 src/backend/commands/explain.c | 79 +++++++++++++++++++++++++++---------------
 1 file changed, 52 insertions(+), 27 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index f523adb..11b5857 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -103,6 +103,7 @@ static void show_sortorder_options(StringInfo buf, Node *sortexpr,
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
+static void show_hinstrument(ExplainState *es, HashInstrumentation *h);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
@@ -2713,43 +2714,67 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 		}
 	}
 
-	if (hinstrument.nbatch > 0)
+	show_hinstrument(es, &hinstrument);
+}
+
+/*
+ * Show hash bucket stats and (optionally) memory.
+ */
+static void
+show_hinstrument(ExplainState *es, HashInstrumentation *h)
+{
+	long		spacePeakKb = (h->space_peak + 1023) / 1024;
+
+	// Currently, this isn't shown for explain of hash(join) since nbatch=0 without analyze
+	// But, it's shown for hashAgg since nbatch=1, always.
+	// Need to 1) avoid showing memory use if !analyze; and, 2) avoid memory use if not verbose
+
+	if (h->nbatch <= 0)
+		return;
+	/* This avoids showing anything if it's explain without analyze; should we just check that, instead ? */
+	if (!es->analyze)
+		return;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   h->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   h->nbuckets_original, es);
+		ExplainPropertyInteger("Hash Batches", NULL,
+							   h->nbatch, es);
+		ExplainPropertyInteger("Original Hash Batches", NULL,
+							   h->nbatch_original, es);
+		ExplainPropertyInteger("Peak Memory Usage", "kB",
+							   spacePeakKb, es);
+	}
+	else
 	{
-		long		spacePeakKb = (hinstrument.space_peak + 1023) / 1024;
 
-		if (es->format != EXPLAIN_FORMAT_TEXT)
-		{
-			ExplainPropertyInteger("Hash Buckets", NULL,
-								   hinstrument.nbuckets, es);
-			ExplainPropertyInteger("Original Hash Buckets", NULL,
-								   hinstrument.nbuckets_original, es);
-			ExplainPropertyInteger("Hash Batches", NULL,
-								   hinstrument.nbatch, es);
-			ExplainPropertyInteger("Original Hash Batches", NULL,
-								   hinstrument.nbatch_original, es);
-			ExplainPropertyInteger("Peak Memory Usage", "kB",
-								   spacePeakKb, es);
-		}
-		else if (hinstrument.nbatch_original != hinstrument.nbatch ||
-				 hinstrument.nbuckets_original != hinstrument.nbuckets)
-		{
+		if (h->nbatch_original != h->nbatch ||
+			 h->nbuckets_original != h->nbuckets) {
 			ExplainIndentText(es);
 			appendStringInfo(es->str,
-							 "Buckets: %d (originally %d)  Batches: %d (originally %d)  Memory Usage: %ldkB\n",
-							 hinstrument.nbuckets,
-							 hinstrument.nbuckets_original,
-							 hinstrument.nbatch,
-							 hinstrument.nbatch_original,
-							 spacePeakKb);
+						"Buckets: %d (originally %d)   Batches: %d (originally %d)",
+						h->nbuckets,
+						h->nbuckets_original,
+						h->nbatch,
+						h->nbatch_original);
 		}
 		else
 		{
 			ExplainIndentText(es);
 			appendStringInfo(es->str,
-							 "Buckets: %d  Batches: %d  Memory Usage: %ldkB\n",
-							 hinstrument.nbuckets, hinstrument.nbatch,
-							 spacePeakKb);
+						"Buckets: %d  Batches: %d",
+						h->nbuckets,
+						h->nbatch);
 		}
+
+		if (es->verbose && es->analyze)
+			appendStringInfo(es->str,
+					"  Memory Usage: %ldkB",
+					spacePeakKb);
+		appendStringInfoChar(es->str, '\n');
 	}
 }
 
-- 
2.7.4

v1-0002-explain-analyze-to-show-stats-from-hash-aggregate.patchtext/x-diff; charset=us-asciiDownload
From 4b61e5ae514f5e5bc430bcf1132597ba4ecd202b Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v1 2/2] explain analyze to show stats from (hash) aggregate..

..as suggested by Jeff Janes
---
 src/backend/commands/explain.c      | 41 +++++++++++++++++++++++++++++--------
 src/backend/executor/execGrouping.c | 10 +++++++++
 src/include/nodes/execnodes.h       | 27 ++++++++++++------------
 3 files changed, 57 insertions(+), 21 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 11b5857..9493e7d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -84,6 +85,7 @@ static void show_sort_keys(SortState *sortstate, List *ancestors,
 						   ExplainState *es);
 static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
+static void show_agg_info(AggState *astate, ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
 static void show_grouping_sets(PlanState *planstate, Agg *agg,
@@ -103,7 +105,7 @@ static void show_sortorder_options(StringInfo buf, Node *sortexpr,
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
-static void show_hinstrument(ExplainState *es, HashInstrumentation *h);
+static void show_hinstrument(ExplainState *es, HashInstrumentation *h, bool showbatch);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
@@ -1889,6 +1891,8 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
+			show_agg_info(castNode(AggState, planstate), es);
+
 			break;
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
@@ -2072,6 +2076,21 @@ ExplainNode(PlanState *planstate, List *ancestors,
 }
 
 /*
+ * Show instrumentation info for an Agg node.
+ */
+static void
+show_agg_info(AggState *astate, ExplainState *es)
+{
+	// perhash->aggnode->numGroups; memctx; AggState->
+
+	for (int i=0; i<astate->num_hashes; ++i) {
+		HashInstrumentation *hinstrument = &astate->perhash->hashtable->hinstrument;
+// fprintf(stderr, "memallocated %lu\n", astate->hashcontext->ecxt_per_query_memory->mem_allocated);
+		show_hinstrument(es, hinstrument, false);
+	}
+}
+
+/*
  * Show the targetlist of a plan node
  */
 static void
@@ -2714,14 +2733,14 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 		}
 	}
 
-	show_hinstrument(es, &hinstrument);
+	show_hinstrument(es, &hinstrument, true);
 }
 
 /*
  * Show hash bucket stats and (optionally) memory.
  */
 static void
-show_hinstrument(ExplainState *es, HashInstrumentation *h)
+show_hinstrument(ExplainState *es, HashInstrumentation *h, bool showbatch)
 {
 	long		spacePeakKb = (h->space_peak + 1023) / 1024;
 
@@ -2755,9 +2774,12 @@ show_hinstrument(ExplainState *es, HashInstrumentation *h)
 			 h->nbuckets_original != h->nbuckets) {
 			ExplainIndentText(es);
 			appendStringInfo(es->str,
-						"Buckets: %d (originally %d)   Batches: %d (originally %d)",
+						"Buckets: %d (originally %d)",
 						h->nbuckets,
-						h->nbuckets_original,
+						h->nbuckets_original);
+			if (showbatch)
+				appendStringInfo(es->str,
+						"  Batches: %d (originally %d)",
 						h->nbatch,
 						h->nbatch_original);
 		}
@@ -2765,9 +2787,12 @@ show_hinstrument(ExplainState *es, HashInstrumentation *h)
 		{
 			ExplainIndentText(es);
 			appendStringInfo(es->str,
-						"Buckets: %d  Batches: %d",
-						h->nbuckets,
-						h->nbatch);
+						"Buckets: %d",
+						h->nbuckets);
+				if (showbatch)
+					appendStringInfo(es->str,
+							"  Batches: %d",
+							h->nbatch);
 		}
 
 		if (es->verbose && es->analyze)
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 3603c58..cf0fe3c 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -203,6 +203,11 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	hashtable->hinstrument.nbuckets_original = nbuckets;
+	hashtable->hinstrument.nbuckets = nbuckets;
+	hashtable->hinstrument.space_peak = entrysize * hashtable->hashtab->size;
+	hashtable->hinstrument.nbatch_original = 1; /* Unused */
+	hashtable->hinstrument.nbatch = 1; /* Unused */
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -328,6 +333,11 @@ LookupTupleHashEntry(TupleHashTable hashtable, TupleTableSlot *slot,
 		{
 			/* created new entry */
 			*isnew = true;
+			/* maybe grew ? XXX: need to call max() here ? */
+			hashtable->hinstrument.nbuckets = hashtable->hashtab->size;
+			hashtable->hinstrument.space_peak = sizeof(TupleHashEntryData) * hashtable->hashtab->size +
+				sizeof(MinimalTuple) * hashtable->hashtab->size; // members ?
+
 			/* zero caller data */
 			entry->additional = NULL;
 			MemoryContextSwitchTo(hashtable->tablecxt);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 1f6f5bb..913416f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -671,6 +671,19 @@ typedef struct ExecAuxRowMark
 typedef struct TupleHashEntryData *TupleHashEntry;
 typedef struct TupleHashTableData *TupleHashTable;
 
+/* ----------------
+ *	 Values displayed by EXPLAIN ANALYZE
+ * ----------------
+ */
+typedef struct HashInstrumentation
+{
+	int			nbuckets;		/* number of buckets at end of execution */
+	int			nbuckets_original;	/* planned number of buckets */
+	int			nbatch;			/* number of batches at end of execution */
+	int			nbatch_original;	/* planned number of batches */
+	size_t		space_peak;		/* peak memory usage in bytes */
+} HashInstrumentation;
+
 typedef struct TupleHashEntryData
 {
 	MinimalTuple firstTuple;	/* copy of first tuple in this group */
@@ -705,6 +718,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	HashInstrumentation hinstrument;	/* instrumentation for EXPLAIN */
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -2243,19 +2257,6 @@ typedef struct GatherMergeState
 } GatherMergeState;
 
 /* ----------------
- *	 Values displayed by EXPLAIN ANALYZE
- * ----------------
- */
-typedef struct HashInstrumentation
-{
-	int			nbuckets;		/* number of buckets at end of execution */
-	int			nbuckets_original;	/* planned number of buckets */
-	int			nbatch;			/* number of batches at end of execution */
-	int			nbatch_original;	/* planned number of batches */
-	size_t		space_peak;		/* peak memory usage in bytes */
-} HashInstrumentation;
-
-/* ----------------
  *	 Shared memory container for per-worker hash information
  * ----------------
  */
-- 
2.7.4

#4Andres Freund
andres@anarazel.de
In reply to: Justin Pryzby (#1)
Re: explain HashAggregate to report bucket and memory stats

Hi,

On 2020-01-03 10:19:26 -0600, Justin Pryzby wrote:

On Sun, Feb 17, 2019 at 11:29:56AM -0500, Jeff Janes wrote:
/messages/by-id/CAMkU=1zBJNVo2DGYBgLJqpu8fyjCE_ys+msr6pOEoiwA7y5jrA@mail.gmail.com

What would I find very useful is [...] if the HashAggregate node under
"explain analyze" would report memory and bucket stats; and if the Aggregate
node would report...anything.

Yea, that'd be amazing. It probably should be something every
execGrouping.c using node can opt into.

Justin: As far as I can tell, you're trying to share one instrumentation
state between hashagg and hashjoins. I'm doubtful that's a good
idea. The cases are different enough that that's probably just going to
be complicated, without actually simplifying anything.

Jeff: can you suggest what details Aggregate should show ?

Memory usage most importantly. Probably makes sense to differentiate
between the memory for the hashtable itself, and the tuples in it (since
they're allocated separately, and just having a overly large hashtable
doesn't hurt that much if it's not filled).

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 3603c58..cf0fe3c 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -203,6 +203,11 @@ BuildTupleHashTableExt(PlanState *parent,
hashtable->hash_iv = 0;
hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	hashtable->hinstrument.nbuckets_original = nbuckets;
+	hashtable->hinstrument.nbuckets = nbuckets;
+	hashtable->hinstrument.space_peak = entrysize * hashtable->hashtab->size;

That's not actually an accurate accounting of memory, because for filled
entries a lot of memory is used to store actual tuples:

static TupleHashEntryData *
lookup_hash_entry(AggState *aggstate)
...
/* find or create the hashtable entry using the filtered tuple */
entry = LookupTupleHashEntry(perhash->hashtable, hashslot, &isnew);

if (isnew)
{
AggStatePerGroup pergroup;
int transno;

pergroup = (AggStatePerGroup)
MemoryContextAlloc(perhash->hashtable->tablecxt,
sizeof(AggStatePerGroupData) * aggstate->numtrans);
entry->additional = pergroup;

since the memory doesn't actually shrink unless the hashtable is
destroyed or reset, it'd probably be sensible to compute the memory
usage either at reset, or at the end of the query.

Greetings,

Andres Freund

#5Justin Pryzby
pryzby@telsasoft.com
In reply to: Andres Freund (#4)
7 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

On Mon, Feb 03, 2020 at 06:53:01AM -0800, Andres Freund wrote:

On 2020-01-03 10:19:26 -0600, Justin Pryzby wrote:

On Sun, Feb 17, 2019 at 11:29:56AM -0500, Jeff Janes wrote:
/messages/by-id/CAMkU=1zBJNVo2DGYBgLJqpu8fyjCE_ys+msr6pOEoiwA7y5jrA@mail.gmail.com

What would I find very useful is [...] if the HashAggregate node under
"explain analyze" would report memory and bucket stats; and if the Aggregate
node would report...anything.

Yea, that'd be amazing. It probably should be something every
execGrouping.c using node can opt into.

Do you think it should be implemented in execGrouping/TupleHashTableData (as I
did) ? I also did an experiment moving into the higher level nodes, but I
guess that's not actually desirable. There's currently different output from
tests between the implementation using execGrouping.c and the one outside it,
so there's at least an issue with grouping sets.

+	hashtable->hinstrument.nbuckets_original = nbuckets;
+	hashtable->hinstrument.nbuckets = nbuckets;
+	hashtable->hinstrument.space_peak = entrysize * hashtable->hashtab->size;

That's not actually an accurate accounting of memory, because for filled
entries a lot of memory is used to store actual tuples:

Thanks - I think I finally understood this.

I updated some existing tests to show the new output. I imagine that's a
throwaway commit, and should eventually add new tests for each of these node
types under explain analyze.

I've been testing the various nodes like:

--heapscan:
DROP TABLE t; CREATE TABLE t (i int unique) WITH(autovacuum_enabled=off); INSERT INTO t SELECT generate_series(1,99999); SET enable_seqscan=off; SET parallel_tuple_cost=0; SET parallel_setup_cost=0; SET enable_indexonlyscan=off; explain analyze verbose SELECT * FROM t WHERE i BETWEEN 999 and 99999999;

--setop:
explain( analyze,verbose) SELECT * FROM generate_series(1,999) EXCEPT (SELECT NULL UNION ALL SELECT * FROM generate_series(1,99999));
Buckets: 2048 (originally 256) Memory Usage: hashtable: 48kB, tuples: 8Kb

--recursive union:
explain analyze verbose WITH RECURSIVE t(n) AS ( SELECT 'foo' UNION SELECT n || ' bar' FROM t WHERE length(n) < 9999) SELECT n, n IS OF (text) AS is_text FROM t;

--subplan
explain analyze verbose SELECT i FROM generate_series(1,999)i WHERE (i,i) NOT IN (SELECT 1,1 UNION ALL SELECT j,j FROM generate_series(1,99999)j);
Buckets: 262144 (originally 131072) Memory Usage: hashtable: 6144kB, tuples: 782Kb
explain analyze verbose select i FROM generate_series(1,999)i WHERE(1,i) NOT in (select i,null::int from t) ;

--Agg:
explain (analyze,verbose) SELECT A,COUNT(1) FROM generate_series(1,99999)a GROUP BY 1;
Buckets: 262144 (originally 256) Memory Usage: hashtable: 6144kB, tuples: 782Kb

explain (analyze, verbose) select i FROM generate_series(1,999)i WHERE(1,1) not in (select a,null from (SELECT generate_series(1,99999) a)x) ;

explain analyze verbose select * from (SELECT a FROM generate_series(1,99)a)v left join lateral (select v.a, four, ten, count(*) from (SELECT b four, 2 ten, b FROM generate_series(1,999)b)x group by cube(four,ten)) s on true order by v.a,four,ten;

--Grouping sets:
explain analyze verbose select unique1,
count(two), count(four), count(ten),
count(hundred), count(thousand), count(twothousand),
count(*)
from tenk1 group by grouping sets (unique1,twothousand,thousand,hundred,ten,four,two);

--
Justin

Attachments:

v2-0001-Run-some-existing-tests-with-explain-ANALYZE.patchtext/x-diff; charset=us-asciiDownload
From dff7109e4d82fd498ae8493caa0e4c84b0f04c74 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 12:03:11 -0600
Subject: [PATCH v2 1/7] Run some existing tests with explain (ANALYZE)..

..in a separate, earlier patch, to better show what bits are added by later
patches for hashtable instrumentation.
---
 src/test/regress/expected/aggregates.out      |  20 +-
 src/test/regress/expected/groupingsets.out    | 298 ++++++++++++++------------
 src/test/regress/expected/select_parallel.out |  20 +-
 src/test/regress/expected/subselect.out       |  69 ++++++
 src/test/regress/expected/union.out           |  71 +++---
 src/test/regress/sql/aggregates.sql           |   2 +-
 src/test/regress/sql/groupingsets.sql         |  44 ++--
 src/test/regress/sql/select_parallel.sql      |   4 +-
 src/test/regress/sql/subselect.sql            |  25 +++
 src/test/regress/sql/union.sql                |   6 +-
 10 files changed, 341 insertions(+), 218 deletions(-)

diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b..b3dcbaa 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -2342,18 +2342,20 @@ select v||'a', case when v||'a' = 'aa' then 1 else 0 end, count(*)
 -- Make sure that generation of HashAggregate for uniqification purposes
 -- does not lead to array overflow due to unexpected duplicate hash keys
 -- see CAFeeJoKKu0u+A_A9R9316djW-YW3-+Gtgvy3ju655qRHR3jtdA@mail.gmail.com
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select 1 from tenk1
    where (hundred, thousand) in (select twothousand, twothousand from onek);
-                         QUERY PLAN                          
--------------------------------------------------------------
- Hash Join
+                          QUERY PLAN                           
+---------------------------------------------------------------
+ Hash Join (actual rows=1000 loops=1)
    Hash Cond: (tenk1.hundred = onek.twothousand)
-   ->  Seq Scan on tenk1
+   ->  Seq Scan on tenk1 (actual rows=1000 loops=1)
          Filter: (hundred = thousand)
-   ->  Hash
-         ->  HashAggregate
+         Rows Removed by Filter: 9000
+   ->  Hash (actual rows=200 loops=1)
+         Buckets: 1024  Batches: 1  Memory Usage: 16kB
+         ->  HashAggregate (actual rows=200 loops=1)
                Group Key: onek.twothousand, onek.twothousand
-               ->  Seq Scan on onek
-(8 rows)
+               ->  Seq Scan on onek (actual rows=1000 loops=1)
+(10 rows)
 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c1f802c..7348f39 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -362,19 +362,20 @@ select a, d, grouping(a,b,c)
 
 -- check that distinct grouping columns are kept separate
 -- even if they are equal()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select g as alias1, g as alias2
   from generate_series(1,3) g
  group by alias1, rollup(alias2);
-                   QUERY PLAN                   
-------------------------------------------------
- GroupAggregate
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ GroupAggregate (actual rows=6 loops=1)
    Group Key: g, g
    Group Key: g
-   ->  Sort
+   ->  Sort (actual rows=3 loops=1)
          Sort Key: g
-         ->  Function Scan on generate_series g
-(6 rows)
+         Sort Method: quicksort  Memory: 25kB
+         ->  Function Scan on generate_series g (actual rows=3 loops=1)
+(7 rows)
 
 select g as alias1, g as alias2
   from generate_series(1,3) g
@@ -458,16 +459,17 @@ ERROR:  aggregate functions are not allowed in FROM clause of their own query le
 LINE 3:        lateral (select a, b, sum(v.x) from gstest_data(v.x) ...
                                      ^
 -- min max optimization should still work with GROUP BY ()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select min(unique1) from tenk1 GROUP BY ();
-                         QUERY PLAN                         
-------------------------------------------------------------
- Result
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ Result (actual rows=1 loops=1)
    InitPlan 1 (returns $0)
-     ->  Limit
-           ->  Index Only Scan using tenk1_unique1 on tenk1
+     ->  Limit (actual rows=1 loops=1)
+           ->  Index Only Scan using tenk1_unique1 on tenk1 (actual rows=1 loops=1)
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+                 Heap Fetches: 0
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
@@ -638,17 +640,18 @@ select a, b, sum(v.x)
 (12 rows)
 
 -- Test reordering of grouping sets
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select * from gstest1 group by grouping sets((a,b,v),(v)) order by v,b,a;
                                   QUERY PLAN                                  
 ------------------------------------------------------------------------------
- GroupAggregate
+ GroupAggregate (actual rows=20 loops=1)
    Group Key: "*VALUES*".column3, "*VALUES*".column2, "*VALUES*".column1
    Group Key: "*VALUES*".column3
-   ->  Sort
+   ->  Sort (actual rows=10 loops=1)
          Sort Key: "*VALUES*".column3, "*VALUES*".column2, "*VALUES*".column1
-         ->  Values Scan on "*VALUES*"
-(6 rows)
+         Sort Method: quicksort  Memory: 25kB
+         ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
+(7 rows)
 
 -- Agg level check. This query should error out.
 select (select grouping(a,b) from gstest2) from gstest2 group by a,b;
@@ -718,18 +721,20 @@ select a,count(*) from gstest2 group by rollup(a) having a is distinct from 1 or
    |     9
 (2 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a,count(*) from gstest2 group by rollup(a) having a is distinct from 1 order by a;
-            QUERY PLAN            
-----------------------------------
- GroupAggregate
+                       QUERY PLAN                        
+---------------------------------------------------------
+ GroupAggregate (actual rows=2 loops=1)
    Group Key: a
    Group Key: ()
    Filter: (a IS DISTINCT FROM 1)
-   ->  Sort
+   Rows Removed by Filter: 1
+   ->  Sort (actual rows=9 loops=1)
          Sort Key: a
-         ->  Seq Scan on gstest2
-(7 rows)
+         Sort Method: quicksort  Memory: 25kB
+         ->  Seq Scan on gstest2 (actual rows=9 loops=1)
+(9 rows)
 
 select v.c, (select count(*) from gstest2 group by () having v.c)
   from (values (false),(true)) v(c) order by v.c;
@@ -739,22 +744,24 @@ select v.c, (select count(*) from gstest2 group by () having v.c)
  t |     9
 (2 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select v.c, (select count(*) from gstest2 group by () having v.c)
     from (values (false),(true)) v(c) order by v.c;
-                        QUERY PLAN                         
------------------------------------------------------------
- Sort
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Sort (actual rows=2 loops=1)
    Sort Key: "*VALUES*".column1
-   ->  Values Scan on "*VALUES*"
+   Sort Method: quicksort  Memory: 25kB
+   ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
          SubPlan 1
-           ->  Aggregate
+           ->  Aggregate (actual rows=0 loops=2)
                  Group Key: ()
                  Filter: "*VALUES*".column1
-                 ->  Result
+                 Rows Removed by Filter: 0
+                 ->  Result (actual rows=4 loops=2)
                        One-Time Filter: "*VALUES*".column1
-                       ->  Seq Scan on gstest2
-(10 rows)
+                       ->  Seq Scan on gstest2 (actual rows=9 loops=1)
+(12 rows)
 
 -- HAVING with GROUPING queries
 select ten, grouping(ten) from onek
@@ -966,17 +973,18 @@ select a, b, grouping(a,b), sum(v), count(*), max(v)
    | 4 |        2 |  17 |     1 |  17
 (8 rows)
 
-explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
+explain (costs off, timing off, summary off, analyze) select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by grouping sets ((a),(b)) order by 3,1,2;
                                                QUERY PLAN                                               
 --------------------------------------------------------------------------------------------------------
- Sort
+ Sort (actual rows=8 loops=1)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
-   ->  HashAggregate
+   Sort Method: quicksort  Memory: 25kB
+   ->  HashAggregate (actual rows=8 loops=1)
          Hash Key: "*VALUES*".column1
          Hash Key: "*VALUES*".column2
-         ->  Values Scan on "*VALUES*"
-(6 rows)
+         ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
+(7 rows)
 
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
@@ -1000,36 +1008,38 @@ select a, b, grouping(a,b), sum(v), count(*), max(v)
    |   |        3 | 145 |    10 |  19
 (16 rows)
 
-explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
+explain (costs off, timing off, summary off, analyze) select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
                                                QUERY PLAN                                               
 --------------------------------------------------------------------------------------------------------
- Sort
+ Sort (actual rows=16 loops=1)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
-   ->  MixedAggregate
+   Sort Method: quicksort  Memory: 26kB
+   ->  MixedAggregate (actual rows=16 loops=1)
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
          Hash Key: "*VALUES*".column1
          Hash Key: "*VALUES*".column2
          Group Key: ()
-         ->  Values Scan on "*VALUES*"
-(8 rows)
+         ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
+(9 rows)
 
 -- shouldn't try and hash
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, grouping(a,b), array_agg(v order by v)
     from gstest1 group by cube(a,b);
-                        QUERY PLAN                        
-----------------------------------------------------------
- GroupAggregate
+                           QUERY PLAN                           
+----------------------------------------------------------------
+ GroupAggregate (actual rows=16 loops=1)
    Group Key: "*VALUES*".column1, "*VALUES*".column2
    Group Key: "*VALUES*".column1
    Group Key: ()
    Sort Key: "*VALUES*".column2
      Group Key: "*VALUES*".column2
-   ->  Sort
+   ->  Sort (actual rows=10 loops=1)
          Sort Key: "*VALUES*".column1, "*VALUES*".column2
-         ->  Values Scan on "*VALUES*"
-(9 rows)
+         Sort Method: quicksort  Memory: 25kB
+         ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
+(10 rows)
 
 -- unsortable cases
 select unsortable_col, count(*)
@@ -1059,7 +1069,7 @@ select unhashable_col, unsortable_col,
                 |              1 |        2 |     4 | 195
 (6 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unhashable_col, unsortable_col,
          grouping(unhashable_col, unsortable_col),
          count(*), sum(v)
@@ -1067,15 +1077,17 @@ explain (costs off)
    order by 3,5;
                             QUERY PLAN                            
 ------------------------------------------------------------------
- Sort
+ Sort (actual rows=6 loops=1)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
-   ->  MixedAggregate
+   Sort Method: quicksort  Memory: 25kB
+   ->  MixedAggregate (actual rows=6 loops=1)
          Hash Key: unsortable_col
          Group Key: unhashable_col
-         ->  Sort
+         ->  Sort (actual rows=8 loops=1)
                Sort Key: unhashable_col
-               ->  Seq Scan on gstest4
-(8 rows)
+               Sort Method: quicksort  Memory: 25kB
+               ->  Seq Scan on gstest4 (actual rows=8 loops=1)
+(10 rows)
 
 select unhashable_col, unsortable_col,
        grouping(unhashable_col, unsortable_col),
@@ -1102,7 +1114,7 @@ select unhashable_col, unsortable_col,
                 |              1 |        2 |     1 | 128
 (16 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unhashable_col, unsortable_col,
          grouping(unhashable_col, unsortable_col),
          count(*), sum(v)
@@ -1110,15 +1122,17 @@ explain (costs off)
    order by 3,5;
                             QUERY PLAN                            
 ------------------------------------------------------------------
- Sort
+ Sort (actual rows=16 loops=1)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
-   ->  MixedAggregate
+   Sort Method: quicksort  Memory: 26kB
+   ->  MixedAggregate (actual rows=16 loops=1)
          Hash Key: v, unsortable_col
          Group Key: v, unhashable_col
-         ->  Sort
+         ->  Sort (actual rows=8 loops=1)
                Sort Key: v, unhashable_col
-               ->  Seq Scan on gstest4
-(8 rows)
+               Sort Method: quicksort  Memory: 25kB
+               ->  Seq Scan on gstest4 (actual rows=8 loops=1)
+(10 rows)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
@@ -1126,14 +1140,14 @@ select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a)
 ---+---+-----+-------
 (0 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
-           QUERY PLAN           
---------------------------------
- HashAggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ HashAggregate (actual rows=0 loops=1)
    Hash Key: a, b
    Hash Key: a
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (4 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
@@ -1150,16 +1164,16 @@ select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),()
    |   |     |     0
 (3 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
-           QUERY PLAN           
---------------------------------
- MixedAggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ MixedAggregate (actual rows=3 loops=1)
    Hash Key: a, b
    Group Key: ()
    Group Key: ()
    Group Key: ()
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (6 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
@@ -1170,15 +1184,15 @@ select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
      |     0
 (3 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
-           QUERY PLAN           
---------------------------------
- Aggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Aggregate (actual rows=3 loops=1)
    Group Key: ()
    Group Key: ()
    Group Key: ()
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (5 rows)
 
 -- check that functionally dependent cols are not nulled
@@ -1193,16 +1207,16 @@ select a, d, grouping(a,b,c)
  2 | 2 |        2
 (4 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, d, grouping(a,b,c)
     from gstest3
    group by grouping sets ((a,b), (a,c));
-        QUERY PLAN         
----------------------------
- HashAggregate
+                    QUERY PLAN                     
+---------------------------------------------------
+ HashAggregate (actual rows=4 loops=1)
    Hash Key: a, b
    Hash Key: a, c
-   ->  Seq Scan on gstest3
+   ->  Seq Scan on gstest3 (actual rows=2 loops=1)
 (4 rows)
 
 -- simple rescan tests
@@ -1219,22 +1233,23 @@ select a, b, sum(v.x)
    | 3 |   3
 (5 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v.x)
     from (values (1),(2)) v(x), gstest_data(v.x)
    group by grouping sets (a,b)
    order by 3, 1, 2;
-                             QUERY PLAN                              
----------------------------------------------------------------------
- Sort
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort (actual rows=5 loops=1)
    Sort Key: (sum("*VALUES*".column1)), gstest_data.a, gstest_data.b
-   ->  HashAggregate
+   Sort Method: quicksort  Memory: 25kB
+   ->  HashAggregate (actual rows=5 loops=1)
          Hash Key: gstest_data.a
          Hash Key: gstest_data.b
-         ->  Nested Loop
-               ->  Values Scan on "*VALUES*"
-               ->  Function Scan on gstest_data
-(8 rows)
+         ->  Nested Loop (actual rows=6 loops=1)
+               ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+               ->  Function Scan on gstest_data (actual rows=3 loops=2)
+(9 rows)
 
 select *
   from (values (1),(2)) v(x),
@@ -1242,7 +1257,7 @@ select *
 ERROR:  aggregate functions are not allowed in FROM clause of their own query level
 LINE 3:        lateral (select a, b, sum(v.x) from gstest_data(v.x) ...
                                      ^
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select *
     from (values (1),(2)) v(x),
          lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
@@ -1277,19 +1292,20 @@ select a, b, grouping(a,b), sum(v), count(*), max(v)
    |   |        3 |  37 |     2 |  19
 (21 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, grouping(a,b), sum(v), count(*), max(v)
     from gstest1 group by grouping sets ((a,b),(a+1,b+1),(a+2,b+2)) order by 3,6;
                                         QUERY PLAN                                         
 -------------------------------------------------------------------------------------------
- Sort
+ Sort (actual rows=21 loops=1)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), (max("*VALUES*".column3))
-   ->  HashAggregate
+   Sort Method: quicksort  Memory: 26kB
+   ->  HashAggregate (actual rows=21 loops=1)
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
          Hash Key: ("*VALUES*".column1 + 1), ("*VALUES*".column2 + 1)
          Hash Key: ("*VALUES*".column1 + 2), ("*VALUES*".column2 + 2)
-         ->  Values Scan on "*VALUES*"
-(7 rows)
+         ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
+(8 rows)
 
 select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
   from gstest2 group by cube (a,b) order by rsum, a, b;
@@ -1305,23 +1321,25 @@ select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
    |   |  12 |   48
 (8 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
     from gstest2 group by cube (a,b) order by rsum, a, b;
-                 QUERY PLAN                  
----------------------------------------------
- Sort
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Sort (actual rows=8 loops=1)
    Sort Key: (sum((sum(c))) OVER (?)), a, b
-   ->  WindowAgg
-         ->  Sort
+   Sort Method: quicksort  Memory: 25kB
+   ->  WindowAgg (actual rows=8 loops=1)
+         ->  Sort (actual rows=8 loops=1)
                Sort Key: a, b
-               ->  MixedAggregate
+               Sort Method: quicksort  Memory: 25kB
+               ->  MixedAggregate (actual rows=8 loops=1)
                      Hash Key: a, b
                      Hash Key: a
                      Hash Key: b
                      Group Key: ()
-                     ->  Seq Scan on gstest2
-(11 rows)
+                     ->  Seq Scan on gstest2 (actual rows=9 loops=1)
+(13 rows)
 
 select a, b, sum(v.x)
   from (values (1),(2)) v(x), gstest_data(v.x)
@@ -1342,23 +1360,24 @@ select a, b, sum(v.x)
    |   |   9
 (12 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v.x)
     from (values (1),(2)) v(x), gstest_data(v.x)
    group by cube (a,b) order by a,b;
-                   QUERY PLAN                   
-------------------------------------------------
- Sort
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort (actual rows=12 loops=1)
    Sort Key: gstest_data.a, gstest_data.b
-   ->  MixedAggregate
+   Sort Method: quicksort  Memory: 25kB
+   ->  MixedAggregate (actual rows=12 loops=1)
          Hash Key: gstest_data.a, gstest_data.b
          Hash Key: gstest_data.a
          Hash Key: gstest_data.b
          Group Key: ()
-         ->  Nested Loop
-               ->  Values Scan on "*VALUES*"
-               ->  Function Scan on gstest_data
-(10 rows)
+         ->  Nested Loop (actual rows=6 loops=1)
+               ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+               ->  Function Scan on gstest_data (actual rows=3 loops=2)
+(11 rows)
 
 -- Verify that we correctly handle the child node returning a
 -- non-minimal slot, which happens if the input is pre-sorted,
@@ -1543,15 +1562,15 @@ select array(select row(v.a,s1.*) from (select two,four, count(*) from onek grou
 -- test the knapsack
 set enable_indexscan = false;
 set work_mem = '64kB';
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
          count(*)
     from tenk1 group by grouping sets (unique1,twothousand,thousand,hundred,ten,four,two);
-          QUERY PLAN           
--------------------------------
- MixedAggregate
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ MixedAggregate (actual rows=13116 loops=1)
    Hash Key: two
    Hash Key: four
    Hash Key: ten
@@ -1561,40 +1580,42 @@ explain (costs off)
      Group Key: twothousand
    Sort Key: thousand
      Group Key: thousand
-   ->  Sort
+   ->  Sort (actual rows=10000 loops=1)
          Sort Key: unique1
-         ->  Seq Scan on tenk1
-(13 rows)
+         Sort Method: external merge  Disk: 392kB
+         ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+(14 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
          count(*)
     from tenk1 group by grouping sets (unique1,hundred,ten,four,two);
-          QUERY PLAN           
--------------------------------
- MixedAggregate
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ MixedAggregate (actual rows=10116 loops=1)
    Hash Key: two
    Hash Key: four
    Hash Key: ten
    Hash Key: hundred
    Group Key: unique1
-   ->  Sort
+   ->  Sort (actual rows=10000 loops=1)
          Sort Key: unique1
-         ->  Seq Scan on tenk1
-(9 rows)
+         Sort Method: external merge  Disk: 392kB
+         ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+(10 rows)
 
 set work_mem = '384kB';
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
          count(*)
     from tenk1 group by grouping sets (unique1,twothousand,thousand,hundred,ten,four,two);
-          QUERY PLAN           
--------------------------------
- MixedAggregate
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ MixedAggregate (actual rows=13116 loops=1)
    Hash Key: two
    Hash Key: four
    Hash Key: ten
@@ -1603,10 +1624,11 @@ explain (costs off)
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
-   ->  Sort
+   ->  Sort (actual rows=10000 loops=1)
          Sort Key: unique1
-         ->  Seq Scan on tenk1
-(12 rows)
+         Sort Method: external merge  Disk: 392kB
+         ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+(13 rows)
 
 -- check collation-sensitive matching between grouping expressions
 -- (similar to a check for aggregates, but there are additional code
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 96dfb7c..94cf969 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -290,21 +290,23 @@ execute tenk1_count(1);
 deallocate tenk1_count;
 -- test parallel plans for queries containing un-correlated subplans.
 alter table tenk2 set (parallel_workers = 0);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
-                      QUERY PLAN                      
-------------------------------------------------------
- Finalize Aggregate
-   ->  Gather
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Finalize Aggregate (actual rows=1 loops=1)
+   ->  Gather (actual rows=5 loops=1)
          Workers Planned: 4
-         ->  Partial Aggregate
-               ->  Parallel Seq Scan on tenk1
+         Workers Launched: 4
+         ->  Partial Aggregate (actual rows=1 loops=5)
+               ->  Parallel Seq Scan on tenk1 (actual rows=2000 loops=5)
                      Filter: (NOT (hashed SubPlan 1))
                      SubPlan 1
-                       ->  Seq Scan on tenk2
+                       ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
-(9 rows)
+                             Rows Removed by Filter: 1010
+(11 rows)
 
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 71a677b..55991c8 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -782,6 +782,17 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
                  Output: 'bar'::name
 (8 rows)
 
+explain (analyze, timing off, summary off, costs off)
+select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
+                  QUERY PLAN                  
+----------------------------------------------
+ Result (actual rows=1 loops=1)
+   SubPlan 1
+     ->  Append (actual rows=2 loops=1)
+           ->  Result (actual rows=1 loops=1)
+           ->  Result (actual rows=1 loops=1)
+(5 rows)
+
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
  ?column? 
 ----------
@@ -974,6 +985,22 @@ select * from int4_tbl where
            Output: a.unique1
 (10 rows)
 
+explain (analyze, timing off, summary off, costs off)
+select * from int4_tbl where
+  (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
+  (select ten from tenk1 b);
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Nested Loop Semi Join (actual rows=1 loops=1)
+   Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
+   Rows Removed by Join Filter: 40000
+   ->  Seq Scan on int4_tbl (actual rows=5 loops=1)
+   ->  Seq Scan on tenk1 b (actual rows=8000 loops=5)
+   SubPlan 1
+     ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
+           Heap Fetches: 0
+(8 rows)
+
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
@@ -1377,6 +1404,29 @@ select * from x;
                        Output: z1.a
 (16 rows)
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ CTE Scan on x (actual rows=22 loops=1)
+   CTE x
+     ->  Recursive Union (actual rows=22 loops=1)
+           ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+           ->  Nested Loop (actual rows=7 loops=3)
+                 Join Filter: (length((z.a || z1.a)) < 5)
+                 Rows Removed by Join Filter: 85
+                 CTE z
+                   ->  WorkTable Scan on x x_1 (actual rows=7 loops=3)
+                 ->  CTE Scan on z (actual rows=7 loops=3)
+                 ->  CTE Scan on z z1 (actual rows=13 loops=22)
+(11 rows)
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
@@ -1431,6 +1481,25 @@ select * from x;
                  Filter: (length((x_1.a || x_1.a)) < 5)
 (9 rows)
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ CTE Scan on x (actual rows=6 loops=1)
+   CTE x
+     ->  Recursive Union (actual rows=6 loops=1)
+           ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+           ->  WorkTable Scan on x x_1 (actual rows=1 loops=3)
+                 Filter: (length((a || a)) < 5)
+                 Rows Removed by Filter: 1
+(7 rows)
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92..dcd51a7 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -347,20 +347,21 @@ ERROR:  FOR NO KEY UPDATE is not allowed with UNION/INTERSECT/EXCEPT
 
 -- exercise both hashed and sorted implementations of INTERSECT/EXCEPT
 set enable_hashagg to on;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
-                                     QUERY PLAN                                     
-------------------------------------------------------------------------------------
- Aggregate
-   ->  Subquery Scan on ss
-         ->  HashSetOp Intersect
-               ->  Append
-                     ->  Subquery Scan on "*SELECT* 2"
-                           ->  Seq Scan on tenk1
-                     ->  Subquery Scan on "*SELECT* 1"
-                           ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(8 rows)
+                                                   QUERY PLAN                                                   
+----------------------------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=1)
+   ->  Subquery Scan on ss (actual rows=5000 loops=1)
+         ->  HashSetOp Intersect (actual rows=5000 loops=1)
+               ->  Append (actual rows=20000 loops=1)
+                     ->  Subquery Scan on "*SELECT* 2" (actual rows=10000 loops=1)
+                           ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+                     ->  Subquery Scan on "*SELECT* 1" (actual rows=10000 loops=1)
+                           ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1 (actual rows=10000 loops=1)
+                                 Heap Fetches: 0
+(9 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -389,22 +390,24 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
 (1 row)
 
 set enable_hashagg to off;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
-                                        QUERY PLAN                                        
-------------------------------------------------------------------------------------------
- Aggregate
-   ->  Subquery Scan on ss
-         ->  SetOp Intersect
-               ->  Sort
+                                                      QUERY PLAN                                                      
+----------------------------------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=1)
+   ->  Subquery Scan on ss (actual rows=5000 loops=1)
+         ->  SetOp Intersect (actual rows=5000 loops=1)
+               ->  Sort (actual rows=20000 loops=1)
                      Sort Key: "*SELECT* 2".fivethous
-                     ->  Append
-                           ->  Subquery Scan on "*SELECT* 2"
-                                 ->  Seq Scan on tenk1
-                           ->  Subquery Scan on "*SELECT* 1"
-                                 ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(10 rows)
+                     Sort Method: quicksort  Memory: 1862kB
+                     ->  Append (actual rows=20000 loops=1)
+                           ->  Subquery Scan on "*SELECT* 2" (actual rows=10000 loops=1)
+                                 ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+                           ->  Subquery Scan on "*SELECT* 1" (actual rows=10000 loops=1)
+                                 ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1 (actual rows=10000 loops=1)
+                                       Heap Fetches: 0
+(12 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -580,16 +583,16 @@ select from generate_series(1,5) union select from generate_series(1,3);
          ->  Function Scan on generate_series generate_series_1
 (4 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select from generate_series(1,5) intersect select from generate_series(1,3);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- HashSetOp Intersect
-   ->  Append
-         ->  Subquery Scan on "*SELECT* 1"
-               ->  Function Scan on generate_series
-         ->  Subquery Scan on "*SELECT* 2"
-               ->  Function Scan on generate_series generate_series_1
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ HashSetOp Intersect (actual rows=1 loops=1)
+   ->  Append (actual rows=8 loops=1)
+         ->  Subquery Scan on "*SELECT* 1" (actual rows=5 loops=1)
+               ->  Function Scan on generate_series (actual rows=5 loops=1)
+         ->  Subquery Scan on "*SELECT* 2" (actual rows=3 loops=1)
+               ->  Function Scan on generate_series generate_series_1 (actual rows=3 loops=1)
 (6 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql
index 3e593f2..3d37754 100644
--- a/src/test/regress/sql/aggregates.sql
+++ b/src/test/regress/sql/aggregates.sql
@@ -1029,6 +1029,6 @@ select v||'a', case when v||'a' = 'aa' then 1 else 0 end, count(*)
 -- Make sure that generation of HashAggregate for uniqification purposes
 -- does not lead to array overflow due to unexpected duplicate hash keys
 -- see CAFeeJoKKu0u+A_A9R9316djW-YW3-+Gtgvy3ju655qRHR3jtdA@mail.gmail.com
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select 1 from tenk1
    where (hundred, thousand) in (select twothousand, twothousand from onek);
diff --git a/src/test/regress/sql/groupingsets.sql b/src/test/regress/sql/groupingsets.sql
index 95ac3fb..1f18365 100644
--- a/src/test/regress/sql/groupingsets.sql
+++ b/src/test/regress/sql/groupingsets.sql
@@ -143,7 +143,7 @@ select a, d, grouping(a,b,c)
 
 -- check that distinct grouping columns are kept separate
 -- even if they are equal()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select g as alias1, g as alias2
   from generate_series(1,3) g
  group by alias1, rollup(alias2);
@@ -183,7 +183,7 @@ select *
        lateral (select a, b, sum(v.x) from gstest_data(v.x) group by rollup (a,b)) s;
 
 -- min max optimization should still work with GROUP BY ()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select min(unique1) from tenk1 GROUP BY ();
 
 -- Views with GROUPING SET queries
@@ -214,7 +214,7 @@ select a, b, sum(v.x)
  group by cube (a,b) order by a,b;
 
 -- Test reordering of grouping sets
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select * from gstest1 group by grouping sets((a,b,v),(v)) order by v,b,a;
 
 -- Agg level check. This query should error out.
@@ -231,12 +231,12 @@ having exists (select 1 from onek b where sum(distinct a.four) = b.four);
 -- Tests around pushdown of HAVING clauses, partially testing against previous bugs
 select a,count(*) from gstest2 group by rollup(a) order by a;
 select a,count(*) from gstest2 group by rollup(a) having a is distinct from 1 order by a;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a,count(*) from gstest2 group by rollup(a) having a is distinct from 1 order by a;
 
 select v.c, (select count(*) from gstest2 group by () having v.c)
   from (values (false),(true)) v(c) order by v.c;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select v.c, (select count(*) from gstest2 group by () having v.c)
     from (values (false),(true)) v(c) order by v.c;
 
@@ -282,16 +282,16 @@ select array_agg(v order by v) from gstest4 group by grouping sets ((id,unsortab
 
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by grouping sets ((a),(b)) order by 3,1,2;
-explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
+explain (costs off, timing off, summary off, analyze) select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by grouping sets ((a),(b)) order by 3,1,2;
 
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
-explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
+explain (costs off, timing off, summary off, analyze) select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
 
 -- shouldn't try and hash
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, grouping(a,b), array_agg(v order by v)
     from gstest1 group by cube(a,b);
 
@@ -306,7 +306,7 @@ select unhashable_col, unsortable_col,
        count(*), sum(v)
   from gstest4 group by grouping sets ((unhashable_col),(unsortable_col))
  order by 3, 5;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unhashable_col, unsortable_col,
          grouping(unhashable_col, unsortable_col),
          count(*), sum(v)
@@ -318,7 +318,7 @@ select unhashable_col, unsortable_col,
        count(*), sum(v)
   from gstest4 group by grouping sets ((v,unhashable_col),(v,unsortable_col))
  order by 3,5;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unhashable_col, unsortable_col,
          grouping(unhashable_col, unsortable_col),
          count(*), sum(v)
@@ -327,21 +327,21 @@ explain (costs off)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
 
 -- check that functionally dependent cols are not nulled
 select a, d, grouping(a,b,c)
   from gstest3
  group by grouping sets ((a,b), (a,c));
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, d, grouping(a,b,c)
     from gstest3
    group by grouping sets ((a,b), (a,c));
@@ -352,7 +352,7 @@ select a, b, sum(v.x)
   from (values (1),(2)) v(x), gstest_data(v.x)
  group by grouping sets (a,b)
  order by 1, 2, 3;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v.x)
     from (values (1),(2)) v(x), gstest_data(v.x)
    group by grouping sets (a,b)
@@ -360,7 +360,7 @@ explain (costs off)
 select *
   from (values (1),(2)) v(x),
        lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select *
     from (values (1),(2)) v(x),
          lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
@@ -368,18 +368,18 @@ explain (costs off)
 -- Tests for chained aggregates
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by grouping sets ((a,b),(a+1,b+1),(a+2,b+2)) order by 3,6;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, grouping(a,b), sum(v), count(*), max(v)
     from gstest1 group by grouping sets ((a,b),(a+1,b+1),(a+2,b+2)) order by 3,6;
 select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
   from gstest2 group by cube (a,b) order by rsum, a, b;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
     from gstest2 group by cube (a,b) order by rsum, a, b;
 select a, b, sum(v.x)
   from (values (1),(2)) v(x), gstest_data(v.x)
  group by cube (a,b) order by a,b;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v.x)
     from (values (1),(2)) v(x), gstest_data(v.x)
    group by cube (a,b) order by a,b;
@@ -409,13 +409,13 @@ select array(select row(v.a,s1.*) from (select two,four, count(*) from onek grou
 
 set enable_indexscan = false;
 set work_mem = '64kB';
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
          count(*)
     from tenk1 group by grouping sets (unique1,twothousand,thousand,hundred,ten,four,two);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
@@ -423,7 +423,7 @@ explain (costs off)
     from tenk1 group by grouping sets (unique1,hundred,ten,four,two);
 
 set work_mem = '384kB';
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 11e7735..49d44e2 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -108,13 +108,13 @@ deallocate tenk1_count;
 
 -- test parallel plans for queries containing un-correlated subplans.
 alter table tenk2 set (parallel_workers = 0);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 -- this is not parallel-safe due to use of random() within SubLink's testexpr:
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select * from tenk1 where (unique1 + random())::integer not in
 	(select ten from tenk2);
 alter table tenk2 reset (parallel_workers);
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index bd8d2f6..b7e7734 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -460,6 +460,9 @@ select * from outer_text where (f1, f2) not in (select * from inner_text);
 explain (verbose, costs off)
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 
+explain (analyze, timing off, summary off, costs off)
+select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
+
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 
 --
@@ -539,6 +542,10 @@ explain (verbose, costs off)
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
+explain (analyze, timing off, summary off, costs off)
+select * from int4_tbl where
+  (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
+  (select ten from tenk1 b);
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
@@ -722,6 +729,15 @@ with recursive x(a) as
     where length(z.a || z1.a) < 5))
 select * from x;
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
@@ -739,6 +755,15 @@ with recursive x(a) as
     where length(z.a || z.a) < 5))
 select * from x;
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql
index 5f4881d..7fbe801 100644
--- a/src/test/regress/sql/union.sql
+++ b/src/test/regress/sql/union.sql
@@ -122,7 +122,7 @@ SELECT q1 FROM int8_tbl EXCEPT ALL SELECT q1 FROM int8_tbl FOR NO KEY UPDATE;
 
 set enable_hashagg to on;
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
 select count(*) from
@@ -134,7 +134,7 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
 
 set enable_hashagg to off;
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
 select count(*) from
@@ -204,7 +204,7 @@ set enable_sort = false;
 
 explain (costs off)
 select from generate_series(1,5) union select from generate_series(1,3);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select from generate_series(1,5) intersect select from generate_series(1,3);
 
 select from generate_series(1,5) union select from generate_series(1,3);
-- 
2.7.4

v2-0002-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From f35f10afc8349eb642ea300782105de097afd5c9 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v2 2/7] explain to show tuplehash bucket and memory stats..

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 src/backend/commands/explain.c                | 131 +++++++++++++++++++++++---
 src/backend/executor/execGrouping.c           |  27 ++++++
 src/backend/executor/nodeAgg.c                |   2 +
 src/backend/executor/nodeRecursiveunion.c     |   3 +
 src/backend/executor/nodeSetOp.c              |   1 +
 src/backend/executor/nodeSubplan.c            |   3 +
 src/include/executor/executor.h               |   1 +
 src/include/nodes/execnodes.h                 |  10 ++
 src/test/regress/expected/aggregates.out      |   3 +-
 src/test/regress/expected/groupingsets.out    |  64 ++++++++++---
 src/test/regress/expected/select_parallel.out |   4 +-
 src/test/regress/expected/subselect.out       |   8 +-
 src/test/regress/expected/union.out           |   6 +-
 src/test/regress/sql/select_parallel.sql      |   2 +-
 14 files changed, 232 insertions(+), 33 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d901dc4..e262108 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -86,12 +87,13 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *planstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_keys(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors, ExplainState *es,
+								   hash_instrumentation *inst);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -104,6 +106,7 @@ static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
+static void show_tuplehash_info(hash_instrumentation *inst, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1489,6 +1492,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1886,6 +1890,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, es);
+				break;
+			}
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2262,21 +2280,26 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
-		else
+			show_grouping_sets(astate, plan, ancestors, es);
+		else {
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes<=1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
@@ -2289,27 +2312,43 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	// this will show things twice??
+	show_grouping_set_keys(aggstate, agg, NULL,
+						   context, useprefix, ancestors, es,
+						   aggstate->num_hashes ? &aggstate->perhash[0].hashtable->instrument : NULL);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		hash_instrumentation *inst;
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED) {
+			int	nth = list_cell_number(agg->chain, lc);
+			Assert(nth < aggstate->num_hashes);
+			inst = &aggstate->perhash[nth].hashtable->instrument;
+		}
+		else
+			inst = NULL;
+
+		show_grouping_set_keys(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors, es,
+							   inst);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_keys(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, ExplainState *es,
+					   hash_instrumentation *inst)
+
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2369,6 +2408,10 @@ show_grouping_set_keys(PlanState *planstate,
 			ExplainPropertyText(keyname, "()", es);
 		else
 			ExplainPropertyListNested(keyname, result, es);
+
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+			show_tuplehash_info(inst, es);
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
@@ -2770,6 +2813,59 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 }
 
 /*
+ * Show hash bucket stats and (optionally) memory.
+ */
+
+// fprintf(stderr, "memallocated %lu\n", astate->hashcontext->ecxt_per_query_memory->mem_allocated);
+// perhash->aggnode->numGroups; memctx; AggState->
+static void
+show_tuplehash_info(hash_instrumentation *inst, ExplainState *es)
+{
+	long	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
+
+	if (!es->analyze)
+		return;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets) {
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld",
+						inst->nbuckets);
+		}
+
+		if (es->verbose)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %ldkB, tuples: %ldkB",
+					spacePeakKb_hash, spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+	}
+}
+
+/*
  * If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
  */
 static void
@@ -3436,6 +3532,17 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+			show_tuplehash_info(&sps->hashtable->instrument, es);
+		if (sps->hashnulls) {
+			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT) {
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, es);
+			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..5641b3f 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,6 +191,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -206,6 +207,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -284,9 +286,34 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial) {
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size * sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+#define maxself(a,b) a=Max(a,b)
+		/* hashtable->entrysize includes additionalsize */
+		maxself(hashtable->instrument.space_peak_hash,
+				hashtable->hashtab->size * sizeof(TupleHashEntryData) +
+				hashtable->hashtab->members * (hashtable->entrysize - sizeof(TupleHashEntryData)));
+
+		maxself(hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * sizeof(MinimalTuple));
+#undef maxself
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index b7f49ce..170dfc7 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1666,6 +1666,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
+				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1937,6 +1938,7 @@ agg_fill_hash_table(AggState *aggstate)
 	}
 
 	aggstate->table_filled = true;
+	UpdateTupleHashTableStats(aggstate->perhash[aggstate->current_set].hashtable, false);
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
 	ResetTupleHashIterator(aggstate->perhash[0].hashtable,
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a..93272c2 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a..9c0e0ab 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index ff95317..eec849c 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 81fdfa4..34199b5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5d5b38b..f929585 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -688,6 +688,15 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+/* XXX: not to be confused with struct HashInstrumentation... */
+typedef struct hash_instrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;	/* peak memory usage in bytes */
+	size_t	space_peak_tuples;	/* peak memory usage in bytes */
+} hash_instrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -706,6 +715,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	hash_instrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index b3dcbaa..7f1c1e1 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -2356,6 +2356,7 @@ explain (costs off, timing off, summary off, analyze)
          Buckets: 1024  Batches: 1  Memory Usage: 16kB
          ->  HashAggregate (actual rows=200 loops=1)
                Group Key: onek.twothousand, onek.twothousand
+               Buckets: 256
                ->  Seq Scan on onek (actual rows=1000 loops=1)
-(10 rows)
+(11 rows)
 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 7348f39..092225b 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -982,9 +982,11 @@ explain (costs off, timing off, summary off, analyze) select a, b, grouping(a,b)
    Sort Method: quicksort  Memory: 25kB
    ->  HashAggregate (actual rows=8 loops=1)
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
-(7 rows)
+(9 rows)
 
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
@@ -1017,11 +1019,14 @@ explain (costs off, timing off, summary off, analyze) select a, b, grouping(a,b)
    Sort Method: quicksort  Memory: 26kB
    ->  MixedAggregate (actual rows=16 loops=1)
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          Group Key: ()
          ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
-(9 rows)
+(12 rows)
 
 -- shouldn't try and hash
 explain (costs off, timing off, summary off, analyze)
@@ -1082,12 +1087,13 @@ explain (costs off, timing off, summary off, analyze)
    Sort Method: quicksort  Memory: 25kB
    ->  MixedAggregate (actual rows=6 loops=1)
          Hash Key: unsortable_col
+         Buckets: 256
          Group Key: unhashable_col
          ->  Sort (actual rows=8 loops=1)
                Sort Key: unhashable_col
                Sort Method: quicksort  Memory: 25kB
                ->  Seq Scan on gstest4 (actual rows=8 loops=1)
-(10 rows)
+(11 rows)
 
 select unhashable_col, unsortable_col,
        grouping(unhashable_col, unsortable_col),
@@ -1127,12 +1133,13 @@ explain (costs off, timing off, summary off, analyze)
    Sort Method: quicksort  Memory: 26kB
    ->  MixedAggregate (actual rows=16 loops=1)
          Hash Key: v, unsortable_col
+         Buckets: 256
          Group Key: v, unhashable_col
          ->  Sort (actual rows=8 loops=1)
                Sort Key: v, unhashable_col
                Sort Method: quicksort  Memory: 25kB
                ->  Seq Scan on gstest4 (actual rows=8 loops=1)
-(10 rows)
+(11 rows)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
@@ -1146,9 +1153,11 @@ explain (costs off, timing off, summary off, analyze)
 --------------------------------------------------------
  HashAggregate (actual rows=0 loops=1)
    Hash Key: a, b
+   Buckets: 256
    Hash Key: a
+   Buckets: 256
    ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
-(4 rows)
+(6 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
  a | b | sum | count 
@@ -1170,11 +1179,12 @@ explain (costs off, timing off, summary off, analyze)
 --------------------------------------------------------
  MixedAggregate (actual rows=3 loops=1)
    Hash Key: a, b
+   Buckets: 256
    Group Key: ()
    Group Key: ()
    Group Key: ()
    ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
-(6 rows)
+(7 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
  sum | count 
@@ -1215,9 +1225,11 @@ explain (costs off, timing off, summary off, analyze)
 ---------------------------------------------------
  HashAggregate (actual rows=4 loops=1)
    Hash Key: a, b
+   Buckets: 4 (originally 2)
    Hash Key: a, c
+   Buckets: 4 (originally 2)
    ->  Seq Scan on gstest3 (actual rows=2 loops=1)
-(4 rows)
+(6 rows)
 
 -- simple rescan tests
 select a, b, sum(v.x)
@@ -1245,11 +1257,13 @@ explain (costs off, timing off, summary off, analyze)
    Sort Method: quicksort  Memory: 25kB
    ->  HashAggregate (actual rows=5 loops=1)
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          ->  Nested Loop (actual rows=6 loops=1)
                ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
                ->  Function Scan on gstest_data (actual rows=3 loops=2)
-(9 rows)
+(11 rows)
 
 select *
   from (values (1),(2)) v(x),
@@ -1302,10 +1316,13 @@ explain (costs off, timing off, summary off, analyze)
    Sort Method: quicksort  Memory: 26kB
    ->  HashAggregate (actual rows=21 loops=1)
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 1), ("*VALUES*".column2 + 1)
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 2), ("*VALUES*".column2 + 2)
+         Buckets: 16
          ->  Values Scan on "*VALUES*" (actual rows=10 loops=1)
-(8 rows)
+(11 rows)
 
 select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
   from gstest2 group by cube (a,b) order by rsum, a, b;
@@ -1335,11 +1352,14 @@ explain (costs off, timing off, summary off, analyze)
                Sort Method: quicksort  Memory: 25kB
                ->  MixedAggregate (actual rows=8 loops=1)
                      Hash Key: a, b
+                     Buckets: 256
                      Hash Key: a
+                     Buckets: 256
                      Hash Key: b
+                     Buckets: 256
                      Group Key: ()
                      ->  Seq Scan on gstest2 (actual rows=9 loops=1)
-(13 rows)
+(16 rows)
 
 select a, b, sum(v.x)
   from (values (1),(2)) v(x), gstest_data(v.x)
@@ -1371,13 +1391,16 @@ explain (costs off, timing off, summary off, analyze)
    Sort Method: quicksort  Memory: 25kB
    ->  MixedAggregate (actual rows=12 loops=1)
          Hash Key: gstest_data.a, gstest_data.b
+         Buckets: 256
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          Group Key: ()
          ->  Nested Loop (actual rows=6 loops=1)
                ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
                ->  Function Scan on gstest_data (actual rows=3 loops=2)
-(11 rows)
+(14 rows)
 
 -- Verify that we correctly handle the child node returning a
 -- non-minimal slot, which happens if the input is pre-sorted,
@@ -1572,9 +1595,13 @@ explain (costs off, timing off, summary off, analyze)
 -----------------------------------------------------------
  MixedAggregate (actual rows=13116 loops=1)
    Hash Key: two
+   Buckets: 4 (originally 2)
    Hash Key: four
+   Buckets: 4 (originally 2)
    Hash Key: ten
+   Buckets: 4
    Hash Key: hundred
+   Buckets: 16
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
@@ -1584,7 +1611,7 @@ explain (costs off, timing off, summary off, analyze)
          Sort Key: unique1
          Sort Method: external merge  Disk: 392kB
          ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
-(14 rows)
+(18 rows)
 
 explain (costs off, timing off, summary off, analyze)
   select unique1,
@@ -1596,15 +1623,19 @@ explain (costs off, timing off, summary off, analyze)
 -----------------------------------------------------------
  MixedAggregate (actual rows=10116 loops=1)
    Hash Key: two
+   Buckets: 4 (originally 2)
    Hash Key: four
+   Buckets: 4 (originally 2)
    Hash Key: ten
+   Buckets: 4
    Hash Key: hundred
+   Buckets: 16
    Group Key: unique1
    ->  Sort (actual rows=10000 loops=1)
          Sort Key: unique1
          Sort Method: external merge  Disk: 392kB
          ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
-(10 rows)
+(14 rows)
 
 set work_mem = '384kB';
 explain (costs off, timing off, summary off, analyze)
@@ -1617,10 +1648,15 @@ explain (costs off, timing off, summary off, analyze)
 -----------------------------------------------------------
  MixedAggregate (actual rows=13116 loops=1)
    Hash Key: two
+   Buckets: 4 (originally 2)
    Hash Key: four
+   Buckets: 4 (originally 2)
    Hash Key: ten
+   Buckets: 4
    Hash Key: hundred
+   Buckets: 16
    Hash Key: thousand
+   Buckets: 128
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
@@ -1628,7 +1664,7 @@ explain (costs off, timing off, summary off, analyze)
          Sort Key: unique1
          Sort Method: external merge  Disk: 392kB
          ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
-(13 rows)
+(18 rows)
 
 -- check collation-sensitive matching between grouping expressions
 -- (similar to a check for aggregates, but there are additional code
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 94cf969..783c1da 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -306,7 +306,9 @@ explain (costs off, timing off, summary off, analyze)
                        ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
                              Rows Removed by Filter: 1010
-(11 rows)
+                     Buckets: 16384
+                     Null hashtable: Buckets: 1024
+(13 rows)
 
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 55991c8..410daa0 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -791,7 +791,9 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
      ->  Append (actual rows=2 loops=1)
            ->  Result (actual rows=1 loops=1)
            ->  Result (actual rows=1 loops=1)
-(5 rows)
+   Buckets: 4 (originally 2)
+   Null hashtable: Buckets: 2
+(7 rows)
 
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
  ?column? 
@@ -999,7 +1001,9 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
            Heap Fetches: 0
-(8 rows)
+   Buckets: 16384
+   Null hashtable: Buckets: 2
+(10 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index dcd51a7..d36981a 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -355,13 +355,14 @@ select count(*) from
  Aggregate (actual rows=1 loops=1)
    ->  Subquery Scan on ss (actual rows=5000 loops=1)
          ->  HashSetOp Intersect (actual rows=5000 loops=1)
+               Buckets: 8192
                ->  Append (actual rows=20000 loops=1)
                      ->  Subquery Scan on "*SELECT* 2" (actual rows=10000 loops=1)
                            ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
                      ->  Subquery Scan on "*SELECT* 1" (actual rows=10000 loops=1)
                            ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1 (actual rows=10000 loops=1)
                                  Heap Fetches: 0
-(9 rows)
+(10 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -588,12 +589,13 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
                                           QUERY PLAN                                          
 ----------------------------------------------------------------------------------------------
  HashSetOp Intersect (actual rows=1 loops=1)
+   Buckets: 4 (originally 2)
    ->  Append (actual rows=8 loops=1)
          ->  Subquery Scan on "*SELECT* 1" (actual rows=5 loops=1)
                ->  Function Scan on generate_series (actual rows=5 loops=1)
          ->  Subquery Scan on "*SELECT* 2" (actual rows=3 loops=1)
                ->  Function Scan on generate_series generate_series_1 (actual rows=3 loops=1)
-(6 rows)
+(7 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 49d44e2..b8b1c8c 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -114,7 +114,7 @@ explain (costs off, timing off, summary off, analyze)
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 -- this is not parallel-safe due to use of random() within SubLink's testexpr:
-explain (costs off, timing off, summary off, analyze)
+explain (costs off)
 	select * from tenk1 where (unique1 + random())::integer not in
 	(select ten from tenk2);
 alter table tenk2 reset (parallel_workers);
-- 
2.7.4

v2-0003-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From 9aef7906903285cedb3daae9085b97eb18fb0a22 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v2 3/7] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c                | 46 +++++++++++++++------------
 src/test/regress/expected/select_parallel.out |  4 +--
 src/test/regress/expected/subselect.out       |  8 ++---
 3 files changed, 32 insertions(+), 26 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e262108..67a9840 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						ExplainState *es, SubPlanState *subplanstate);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -718,7 +718,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, es, NULL);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1080,7 +1080,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			ExplainState *es, SubPlanState *subplanstate)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1337,6 +1337,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			if (subplanstate->hashnulls) {
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1365,6 +1375,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+		if (subplanstate && subplanstate->hashtable)
+			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+		if (subplanstate && subplanstate->hashnulls) {
+			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2037,12 +2054,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, es, NULL);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, es, NULL);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2074,7 +2091,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, es, NULL);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3473,7 +3490,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, es, NULL);
 }
 
 /*
@@ -3531,18 +3548,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-			show_tuplehash_info(&sps->hashtable->instrument, es);
-		if (sps->hashnulls) {
-			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT) {
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, es);
-			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
-		}
+					relationship, sp->plan_name, es, sps);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3559,7 +3565,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es, NULL);
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 783c1da..bc270e0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -303,11 +303,11 @@ explain (costs off, timing off, summary off, analyze)
                ->  Parallel Seq Scan on tenk1 (actual rows=2000 loops=5)
                      Filter: (NOT (hashed SubPlan 1))
                      SubPlan 1
+                       Buckets: 16384
+                       Null hashtable: Buckets: 1024
                        ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
                              Rows Removed by Filter: 1010
-                     Buckets: 16384
-                     Null hashtable: Buckets: 1024
 (13 rows)
 
 select count(*) from tenk1 where (two, four) not in
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 410daa0..a6b9595 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -788,11 +788,11 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 ----------------------------------------------
  Result (actual rows=1 loops=1)
    SubPlan 1
+     Buckets: 4 (originally 2)
+     Null hashtable: Buckets: 2
      ->  Append (actual rows=2 loops=1)
            ->  Result (actual rows=1 loops=1)
            ->  Result (actual rows=1 loops=1)
-   Buckets: 4 (originally 2)
-   Null hashtable: Buckets: 2
 (7 rows)
 
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
@@ -999,10 +999,10 @@ select * from int4_tbl where
    ->  Seq Scan on int4_tbl (actual rows=5 loops=1)
    ->  Seq Scan on tenk1 b (actual rows=8000 loops=5)
    SubPlan 1
+     Buckets: 16384
+     Null hashtable: Buckets: 2
      ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
            Heap Fetches: 0
-   Buckets: 16384
-   Null hashtable: Buckets: 2
 (10 rows)
 
 select * from int4_tbl where
-- 
2.7.4

v2-0004-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From 86939c5c0c8c835a0cd50d4a32dda0f08413c363 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v2 4/7] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.
---
 src/backend/commands/explain.c            |  2 ++
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 20 ++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 30 insertions(+)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 67a9840..d71f5f1 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2908,6 +2908,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index ae8a11d..9ae99a3 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -744,6 +746,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index e102589..d1ef07c 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -43,6 +43,7 @@
 #include "access/htup_details.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 #include "utils/hashutils.h"
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	hash_instrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1148,6 +1151,23 @@ tbm_end_iterate(TBMIterator *iterator)
 }
 
 /*
+ * tbm_instrumentation - return pointer instrumentation data
+ *
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+hash_instrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable) {
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) * (tbm->nchunks ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
+/*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
  * This doesn't free any of the shared state associated with the iterator,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f929585..f5740c7 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1607,6 +1607,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	hash_instrumentation	instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fca..811a497 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct hash_instrumentation hash_instrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern hash_instrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.7.4

v2-0005-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From ab4b5443fe597beafc4a28b0a8c5817f4670b67b Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v2 5/7] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.
---
 src/backend/commands/explain.c            | 18 +++++++++---------
 src/backend/executor/execGrouping.c       | 27 ---------------------------
 src/backend/executor/nodeAgg.c            |  9 +++++++--
 src/backend/executor/nodeRecursiveunion.c |  3 ++-
 src/backend/executor/nodeSetOp.c          |  3 ++-
 src/backend/executor/nodeSubplan.c        | 11 ++++++++---
 src/include/executor/executor.h           |  1 -
 src/include/executor/nodeAgg.h            |  1 +
 src/include/nodes/execnodes.h             | 22 +++++++++++++++++++++-
 9 files changed, 50 insertions(+), 45 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d71f5f1..1415bce 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1341,11 +1341,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			Assert(subplanstate != NULL);
 			/* Show hash stats for hashed subplan */
 			if (subplanstate->hashtable)
-				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument, es);
 			if (subplanstate->hashnulls) {
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			}
 		}
 		if (es->indent)
@@ -1376,10 +1376,10 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
 		if (subplanstate && subplanstate->hashtable)
-			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument, es);
 		if (subplanstate && subplanstate->hashnulls) {
 			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
 		}
 	}
@@ -1911,14 +1911,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, es);
+					show_tuplehash_info(&sos->instrument, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, es);
+					show_tuplehash_info(&rus->instrument, es);
 				break;
 			}
 		case T_Group:
@@ -2305,7 +2305,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes<=1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2332,7 +2332,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 	// this will show things twice??
 	show_grouping_set_keys(aggstate, agg, NULL,
 						   context, useprefix, ancestors, es,
-						   aggstate->num_hashes ? &aggstate->perhash[0].hashtable->instrument : NULL);
+						   aggstate->num_hashes ? &aggstate->perhash[0].instrument : NULL);
 
 	foreach(lc, agg->chain)
 	{
@@ -2344,7 +2344,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED) {
 			int	nth = list_cell_number(agg->chain, lc);
 			Assert(nth < aggstate->num_hashes);
-			inst = &aggstate->perhash[nth].hashtable->instrument;
+			inst = &aggstate->perhash[nth].instrument;
 		}
 		else
 			inst = NULL;
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 5641b3f..de0205f 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,7 +191,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -207,7 +206,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -286,34 +284,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial) {
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		hashtable->instrument.space_peak_hash = hashtable->hashtab->size * sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-#define maxself(a,b) a=Max(a,b)
-		/* hashtable->entrysize includes additionalsize */
-		maxself(hashtable->instrument.space_peak_hash,
-				hashtable->hashtab->size * sizeof(TupleHashEntryData) +
-				hashtable->hashtab->members * (hashtable->entrysize - sizeof(TupleHashEntryData)));
-
-		maxself(hashtable->instrument.space_peak_tuples,
-				hashtable->hashtab->members * sizeof(MinimalTuple));
-#undef maxself
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 170dfc7..4008e27 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1268,6 +1268,7 @@ build_hash_table(AggState *aggstate)
 		if (perhash->hashtable)
 			ResetTupleHashTable(perhash->hashtable);
 		else
+		{
 			perhash->hashtable = BuildTupleHashTableExt(&aggstate->ss.ps,
 														perhash->hashslot->tts_tupleDescriptor,
 														perhash->numCols,
@@ -1281,6 +1282,8 @@ build_hash_table(AggState *aggstate)
 														aggstate->hashcontext->ecxt_per_tuple_memory,
 														tmpmem,
 														DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+			InitTupleHashTableStats(perhash->instrument, perhash->hashtable->hashtab, additionalsize);
+		}
 	}
 }
 
@@ -1666,7 +1669,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
-				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
+				UpdateTupleHashTableStats(aggstate->perhash[0].instrument, aggstate->perhash[0].hashtable->hashtab);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1910,6 +1913,7 @@ agg_fill_hash_table(AggState *aggstate)
 {
 	TupleTableSlot *outerslot;
 	ExprContext *tmpcontext = aggstate->tmpcontext;
+	AggStatePerHash	perhash = &aggstate->perhash[aggstate->current_set];
 
 	/*
 	 * Process each outer-plan tuple, and then fetch the next one, until we
@@ -1938,7 +1942,8 @@ agg_fill_hash_table(AggState *aggstate)
 	}
 
 	aggstate->table_filled = true;
-	UpdateTupleHashTableStats(aggstate->perhash[aggstate->current_set].hashtable, false);
+	UpdateTupleHashTableStats(perhash->instrument, perhash->hashtable->hashtab);
+
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
 	ResetTupleHashIterator(aggstate->perhash[0].hashtable,
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c2..594abdb 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,7 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, 0);
 }
 
 
@@ -157,7 +158,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab..4a56290 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,7 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+	InitTupleHashTableStats(setopstate->instrument, setopstate->hashtable->hashtab, 0);
 }
 
 /*
@@ -415,7 +416,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument, setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index eec849c..a5b71fa 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -507,6 +507,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -520,6 +521,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -534,7 +537,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashtable);
-		else
+		else {
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -548,6 +551,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab, 0);
+		}
 	}
 
 	/*
@@ -621,9 +626,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 34199b5..81fdfa4 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 264916f..008fda3 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -302,6 +302,7 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	hash_instrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f5740c7..f8c93dd 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -688,9 +688,26 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, addsize) \
+	do{\
+	instr.additionalsize = addsize; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = htable->size * sizeof(TupleHashEntryData); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, htable->size * sizeof(TupleHashEntryData) + htable->members * instr.additionalsize); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, htable->members * sizeof(MinimalTuple));\
+	}while(0)
+
 /* XXX: not to be confused with struct HashInstrumentation... */
 typedef struct hash_instrumentation
 {
+	size_t	additionalsize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;	/* peak memory usage in bytes */
@@ -715,7 +732,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	hash_instrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -881,6 +897,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	hash_instrumentation instrument;
+	hash_instrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1289,6 +1307,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	hash_instrumentation	instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -2322,6 +2341,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	hash_instrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.7.4

v2-0006-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From 8722ffb2d673bd90c186ae45b3d450456e41fdfd Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v2 6/7] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..d76a630 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -186,7 +186,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f8c93dd..2bcd140 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -724,7 +724,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.7.4

v2-0007-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From 11cc4891d85fb9fd5dd157f3cd808f1494f580d9 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v2 7/7] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 4008e27..ab97a35 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1499,8 +1499,7 @@ lookup_hash_entry(AggState *aggstate)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  */
-- 
2.7.4

#6Justin Pryzby
pryzby@telsasoft.com
In reply to: Justin Pryzby (#5)
7 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

Updated:

. remove from explain analyze those tests which would display sort
Memory/Disk. Oops.
. fix issue with the first patch showing zero "tuples" memory for some
grouping sets.
. reclassify memory as "tuples" if it has to do with "members". So hashtable
size is now redundant with nbuckets (if you know
sizeof(TupleHashEntryData));

--
Justin

Attachments:

v3-0001-Run-some-existing-tests-with-explain-ANALYZE.patchtext/x-diff; charset=us-asciiDownload
From c989b75f820dbda0540b3d2cd092eaf1f8629baa Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 12:03:11 -0600
Subject: [PATCH v3 1/7] Run some existing tests with explain (ANALYZE)..

..in a separate, earlier patch, to better show what bits are added by later
patches for hashtable instrumentation.
---
 src/test/regress/expected/groupingsets.out    | 87 ++++++++++++++-------------
 src/test/regress/expected/select_parallel.out | 20 +++---
 src/test/regress/expected/subselect.out       | 69 +++++++++++++++++++++
 src/test/regress/expected/union.out           | 43 ++++++-------
 src/test/regress/sql/groupingsets.sql         | 16 ++---
 src/test/regress/sql/select_parallel.sql      |  4 +-
 src/test/regress/sql/subselect.sql            | 25 ++++++++
 src/test/regress/sql/union.sql                |  4 +-
 8 files changed, 184 insertions(+), 84 deletions(-)

diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c1f802c..c052f7e 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -458,16 +458,17 @@ ERROR:  aggregate functions are not allowed in FROM clause of their own query le
 LINE 3:        lateral (select a, b, sum(v.x) from gstest_data(v.x) ...
                                      ^
 -- min max optimization should still work with GROUP BY ()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select min(unique1) from tenk1 GROUP BY ();
-                         QUERY PLAN                         
-------------------------------------------------------------
- Result
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ Result (actual rows=1 loops=1)
    InitPlan 1 (returns $0)
-     ->  Limit
-           ->  Index Only Scan using tenk1_unique1 on tenk1
+     ->  Limit (actual rows=1 loops=1)
+           ->  Index Only Scan using tenk1_unique1 on tenk1 (actual rows=1 loops=1)
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+                 Heap Fetches: 0
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
@@ -1126,14 +1127,14 @@ select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a)
 ---+---+-----+-------
 (0 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
-           QUERY PLAN           
---------------------------------
- HashAggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ HashAggregate (actual rows=0 loops=1)
    Hash Key: a, b
    Hash Key: a
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (4 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
@@ -1150,16 +1151,16 @@ select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),()
    |   |     |     0
 (3 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
-           QUERY PLAN           
---------------------------------
- MixedAggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ MixedAggregate (actual rows=3 loops=1)
    Hash Key: a, b
    Group Key: ()
    Group Key: ()
    Group Key: ()
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (6 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
@@ -1170,15 +1171,15 @@ select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
      |     0
 (3 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
-           QUERY PLAN           
---------------------------------
- Aggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Aggregate (actual rows=3 loops=1)
    Group Key: ()
    Group Key: ()
    Group Key: ()
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (5 rows)
 
 -- check that functionally dependent cols are not nulled
@@ -1193,16 +1194,16 @@ select a, d, grouping(a,b,c)
  2 | 2 |        2
 (4 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, d, grouping(a,b,c)
     from gstest3
    group by grouping sets ((a,b), (a,c));
-        QUERY PLAN         
----------------------------
- HashAggregate
+                    QUERY PLAN                     
+---------------------------------------------------
+ HashAggregate (actual rows=4 loops=1)
    Hash Key: a, b
    Hash Key: a, c
-   ->  Seq Scan on gstest3
+   ->  Seq Scan on gstest3 (actual rows=2 loops=1)
 (4 rows)
 
 -- simple rescan tests
@@ -1242,7 +1243,7 @@ select *
 ERROR:  aggregate functions are not allowed in FROM clause of their own query level
 LINE 3:        lateral (select a, b, sum(v.x) from gstest_data(v.x) ...
                                      ^
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select *
     from (values (1),(2)) v(x),
          lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
@@ -1543,15 +1544,15 @@ select array(select row(v.a,s1.*) from (select two,four, count(*) from onek grou
 -- test the knapsack
 set enable_indexscan = false;
 set work_mem = '64kB';
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
          count(*)
     from tenk1 group by grouping sets (unique1,twothousand,thousand,hundred,ten,four,two);
-          QUERY PLAN           
--------------------------------
- MixedAggregate
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ MixedAggregate (actual rows=13116 loops=1)
    Hash Key: two
    Hash Key: four
    Hash Key: ten
@@ -1561,29 +1562,31 @@ explain (costs off)
      Group Key: twothousand
    Sort Key: thousand
      Group Key: thousand
-   ->  Sort
+   ->  Sort (actual rows=10000 loops=1)
          Sort Key: unique1
-         ->  Seq Scan on tenk1
-(13 rows)
+         Sort Method: external merge  Disk: 392kB
+         ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+(14 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
          count(*)
     from tenk1 group by grouping sets (unique1,hundred,ten,four,two);
-          QUERY PLAN           
--------------------------------
- MixedAggregate
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ MixedAggregate (actual rows=10116 loops=1)
    Hash Key: two
    Hash Key: four
    Hash Key: ten
    Hash Key: hundred
    Group Key: unique1
-   ->  Sort
+   ->  Sort (actual rows=10000 loops=1)
          Sort Key: unique1
-         ->  Seq Scan on tenk1
-(9 rows)
+         Sort Method: external merge  Disk: 392kB
+         ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+(10 rows)
 
 set work_mem = '384kB';
 explain (costs off)
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 96dfb7c..94cf969 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -290,21 +290,23 @@ execute tenk1_count(1);
 deallocate tenk1_count;
 -- test parallel plans for queries containing un-correlated subplans.
 alter table tenk2 set (parallel_workers = 0);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
-                      QUERY PLAN                      
-------------------------------------------------------
- Finalize Aggregate
-   ->  Gather
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Finalize Aggregate (actual rows=1 loops=1)
+   ->  Gather (actual rows=5 loops=1)
          Workers Planned: 4
-         ->  Partial Aggregate
-               ->  Parallel Seq Scan on tenk1
+         Workers Launched: 4
+         ->  Partial Aggregate (actual rows=1 loops=5)
+               ->  Parallel Seq Scan on tenk1 (actual rows=2000 loops=5)
                      Filter: (NOT (hashed SubPlan 1))
                      SubPlan 1
-                       ->  Seq Scan on tenk2
+                       ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
-(9 rows)
+                             Rows Removed by Filter: 1010
+(11 rows)
 
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 71a677b..55991c8 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -782,6 +782,17 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
                  Output: 'bar'::name
 (8 rows)
 
+explain (analyze, timing off, summary off, costs off)
+select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
+                  QUERY PLAN                  
+----------------------------------------------
+ Result (actual rows=1 loops=1)
+   SubPlan 1
+     ->  Append (actual rows=2 loops=1)
+           ->  Result (actual rows=1 loops=1)
+           ->  Result (actual rows=1 loops=1)
+(5 rows)
+
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
  ?column? 
 ----------
@@ -974,6 +985,22 @@ select * from int4_tbl where
            Output: a.unique1
 (10 rows)
 
+explain (analyze, timing off, summary off, costs off)
+select * from int4_tbl where
+  (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
+  (select ten from tenk1 b);
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Nested Loop Semi Join (actual rows=1 loops=1)
+   Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
+   Rows Removed by Join Filter: 40000
+   ->  Seq Scan on int4_tbl (actual rows=5 loops=1)
+   ->  Seq Scan on tenk1 b (actual rows=8000 loops=5)
+   SubPlan 1
+     ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
+           Heap Fetches: 0
+(8 rows)
+
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
@@ -1377,6 +1404,29 @@ select * from x;
                        Output: z1.a
 (16 rows)
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ CTE Scan on x (actual rows=22 loops=1)
+   CTE x
+     ->  Recursive Union (actual rows=22 loops=1)
+           ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+           ->  Nested Loop (actual rows=7 loops=3)
+                 Join Filter: (length((z.a || z1.a)) < 5)
+                 Rows Removed by Join Filter: 85
+                 CTE z
+                   ->  WorkTable Scan on x x_1 (actual rows=7 loops=3)
+                 ->  CTE Scan on z (actual rows=7 loops=3)
+                 ->  CTE Scan on z z1 (actual rows=13 loops=22)
+(11 rows)
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
@@ -1431,6 +1481,25 @@ select * from x;
                  Filter: (length((x_1.a || x_1.a)) < 5)
 (9 rows)
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ CTE Scan on x (actual rows=6 loops=1)
+   CTE x
+     ->  Recursive Union (actual rows=6 loops=1)
+           ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+           ->  WorkTable Scan on x x_1 (actual rows=1 loops=3)
+                 Filter: (length((a || a)) < 5)
+                 Rows Removed by Filter: 1
+(7 rows)
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92..5ac1477 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -347,20 +347,21 @@ ERROR:  FOR NO KEY UPDATE is not allowed with UNION/INTERSECT/EXCEPT
 
 -- exercise both hashed and sorted implementations of INTERSECT/EXCEPT
 set enable_hashagg to on;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
-                                     QUERY PLAN                                     
-------------------------------------------------------------------------------------
- Aggregate
-   ->  Subquery Scan on ss
-         ->  HashSetOp Intersect
-               ->  Append
-                     ->  Subquery Scan on "*SELECT* 2"
-                           ->  Seq Scan on tenk1
-                     ->  Subquery Scan on "*SELECT* 1"
-                           ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(8 rows)
+                                                   QUERY PLAN                                                   
+----------------------------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=1)
+   ->  Subquery Scan on ss (actual rows=5000 loops=1)
+         ->  HashSetOp Intersect (actual rows=5000 loops=1)
+               ->  Append (actual rows=20000 loops=1)
+                     ->  Subquery Scan on "*SELECT* 2" (actual rows=10000 loops=1)
+                           ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+                     ->  Subquery Scan on "*SELECT* 1" (actual rows=10000 loops=1)
+                           ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1 (actual rows=10000 loops=1)
+                                 Heap Fetches: 0
+(9 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -580,16 +581,16 @@ select from generate_series(1,5) union select from generate_series(1,3);
          ->  Function Scan on generate_series generate_series_1
 (4 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select from generate_series(1,5) intersect select from generate_series(1,3);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- HashSetOp Intersect
-   ->  Append
-         ->  Subquery Scan on "*SELECT* 1"
-               ->  Function Scan on generate_series
-         ->  Subquery Scan on "*SELECT* 2"
-               ->  Function Scan on generate_series generate_series_1
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ HashSetOp Intersect (actual rows=1 loops=1)
+   ->  Append (actual rows=8 loops=1)
+         ->  Subquery Scan on "*SELECT* 1" (actual rows=5 loops=1)
+               ->  Function Scan on generate_series (actual rows=5 loops=1)
+         ->  Subquery Scan on "*SELECT* 2" (actual rows=3 loops=1)
+               ->  Function Scan on generate_series generate_series_1 (actual rows=3 loops=1)
 (6 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
diff --git a/src/test/regress/sql/groupingsets.sql b/src/test/regress/sql/groupingsets.sql
index 95ac3fb..035a3ce 100644
--- a/src/test/regress/sql/groupingsets.sql
+++ b/src/test/regress/sql/groupingsets.sql
@@ -183,7 +183,7 @@ select *
        lateral (select a, b, sum(v.x) from gstest_data(v.x) group by rollup (a,b)) s;
 
 -- min max optimization should still work with GROUP BY ()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select min(unique1) from tenk1 GROUP BY ();
 
 -- Views with GROUPING SET queries
@@ -327,21 +327,21 @@ explain (costs off)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
 
 -- check that functionally dependent cols are not nulled
 select a, d, grouping(a,b,c)
   from gstest3
  group by grouping sets ((a,b), (a,c));
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, d, grouping(a,b,c)
     from gstest3
    group by grouping sets ((a,b), (a,c));
@@ -360,7 +360,7 @@ explain (costs off)
 select *
   from (values (1),(2)) v(x),
        lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select *
     from (values (1),(2)) v(x),
          lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
@@ -409,13 +409,13 @@ select array(select row(v.a,s1.*) from (select two,four, count(*) from onek grou
 
 set enable_indexscan = false;
 set work_mem = '64kB';
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
          count(*)
     from tenk1 group by grouping sets (unique1,twothousand,thousand,hundred,ten,four,two);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select unique1,
          count(two), count(four), count(ten),
          count(hundred), count(thousand), count(twothousand),
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 11e7735..49d44e2 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -108,13 +108,13 @@ deallocate tenk1_count;
 
 -- test parallel plans for queries containing un-correlated subplans.
 alter table tenk2 set (parallel_workers = 0);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 -- this is not parallel-safe due to use of random() within SubLink's testexpr:
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select * from tenk1 where (unique1 + random())::integer not in
 	(select ten from tenk2);
 alter table tenk2 reset (parallel_workers);
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index bd8d2f6..b7e7734 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -460,6 +460,9 @@ select * from outer_text where (f1, f2) not in (select * from inner_text);
 explain (verbose, costs off)
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 
+explain (analyze, timing off, summary off, costs off)
+select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
+
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 
 --
@@ -539,6 +542,10 @@ explain (verbose, costs off)
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
+explain (analyze, timing off, summary off, costs off)
+select * from int4_tbl where
+  (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
+  (select ten from tenk1 b);
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
@@ -722,6 +729,15 @@ with recursive x(a) as
     where length(z.a || z1.a) < 5))
 select * from x;
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
@@ -739,6 +755,15 @@ with recursive x(a) as
     where length(z.a || z.a) < 5))
 select * from x;
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql
index 5f4881d..075bb1d 100644
--- a/src/test/regress/sql/union.sql
+++ b/src/test/regress/sql/union.sql
@@ -122,7 +122,7 @@ SELECT q1 FROM int8_tbl EXCEPT ALL SELECT q1 FROM int8_tbl FOR NO KEY UPDATE;
 
 set enable_hashagg to on;
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
 select count(*) from
@@ -204,7 +204,7 @@ set enable_sort = false;
 
 explain (costs off)
 select from generate_series(1,5) union select from generate_series(1,3);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select from generate_series(1,5) intersect select from generate_series(1,3);
 
 select from generate_series(1,5) union select from generate_series(1,3);
-- 
2.7.4

v3-0002-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From 7ba4c91b816bed37ec875431d14386e822bcd3de Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v3 2/7] explain to show tuplehash bucket and memory stats..

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 src/backend/commands/explain.c                | 131 +++++++++++++++++++++++---
 src/backend/executor/execGrouping.c           |  25 +++++
 src/backend/executor/nodeAgg.c                |  12 +++
 src/backend/executor/nodeRecursiveunion.c     |   3 +
 src/backend/executor/nodeSetOp.c              |   1 +
 src/backend/executor/nodeSubplan.c            |   3 +
 src/include/executor/executor.h               |   1 +
 src/include/nodes/execnodes.h                 |  10 ++
 src/test/regress/expected/groupingsets.out    |  23 ++++-
 src/test/regress/expected/select_parallel.out |   4 +-
 src/test/regress/expected/subselect.out       |   8 +-
 src/test/regress/expected/union.out           |   6 +-
 src/test/regress/sql/select_parallel.sql      |   2 +-
 13 files changed, 206 insertions(+), 23 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d901dc4..e262108 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -86,12 +87,13 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *planstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_keys(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors, ExplainState *es,
+								   hash_instrumentation *inst);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -104,6 +106,7 @@ static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
+static void show_tuplehash_info(hash_instrumentation *inst, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1489,6 +1492,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1886,6 +1890,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, es);
+				break;
+			}
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2262,21 +2280,26 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
-		else
+			show_grouping_sets(astate, plan, ancestors, es);
+		else {
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes<=1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
@@ -2289,27 +2312,43 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	// this will show things twice??
+	show_grouping_set_keys(aggstate, agg, NULL,
+						   context, useprefix, ancestors, es,
+						   aggstate->num_hashes ? &aggstate->perhash[0].hashtable->instrument : NULL);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		hash_instrumentation *inst;
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED) {
+			int	nth = list_cell_number(agg->chain, lc);
+			Assert(nth < aggstate->num_hashes);
+			inst = &aggstate->perhash[nth].hashtable->instrument;
+		}
+		else
+			inst = NULL;
+
+		show_grouping_set_keys(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors, es,
+							   inst);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_keys(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, ExplainState *es,
+					   hash_instrumentation *inst)
+
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2369,6 +2408,10 @@ show_grouping_set_keys(PlanState *planstate,
 			ExplainPropertyText(keyname, "()", es);
 		else
 			ExplainPropertyListNested(keyname, result, es);
+
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+			show_tuplehash_info(inst, es);
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
@@ -2770,6 +2813,59 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 }
 
 /*
+ * Show hash bucket stats and (optionally) memory.
+ */
+
+// fprintf(stderr, "memallocated %lu\n", astate->hashcontext->ecxt_per_query_memory->mem_allocated);
+// perhash->aggnode->numGroups; memctx; AggState->
+static void
+show_tuplehash_info(hash_instrumentation *inst, ExplainState *es)
+{
+	long	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
+
+	if (!es->analyze)
+		return;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets) {
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld",
+						inst->nbuckets);
+		}
+
+		if (es->verbose)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %ldkB, tuples: %ldkB",
+					spacePeakKb_hash, spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+	}
+}
+
+/*
  * If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
  */
 static void
@@ -3436,6 +3532,17 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+			show_tuplehash_info(&sps->hashtable->instrument, es);
+		if (sps->hashnulls) {
+			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT) {
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, es);
+			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..dcc365c 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,6 +191,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -206,6 +207,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -284,9 +286,32 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial) {
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size * sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+#define maxself(a,b) a=Max(a,b)
+		/* hashtable->entrysize includes additionalsize */
+		maxself(hashtable->instrument.space_peak_hash,
+				hashtable->hashtab->size * sizeof(TupleHashEntryData);
+		maxself(hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * hashtable->entrysize);
+#undef maxself
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index b7f49ce..81dda1b 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1666,6 +1666,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
+				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1863,8 +1864,16 @@ agg_retrieve_direct(AggState *aggstate)
 						}
 					}
 				}
+
+				if (aggstate->aggstrategy == AGG_MIXED &&
+						aggstate->current_phase == 1)
+				{
+					for (int i = 0; i < aggstate->num_hashes; i++)
+						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+				}
 			}
 
+
 			/*
 			 * Use the representative input tuple for any references to
 			 * non-aggregated input columns in aggregate direct args, the node
@@ -1937,6 +1946,9 @@ agg_fill_hash_table(AggState *aggstate)
 	}
 
 	aggstate->table_filled = true;
+	for (int i = 0; i < aggstate->num_hashes; i++)
+		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
 	ResetTupleHashIterator(aggstate->perhash[0].hashtable,
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a..93272c2 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a..9c0e0ab 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index ff95317..eec849c 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 81fdfa4..34199b5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5d5b38b..f929585 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -688,6 +688,15 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+/* XXX: not to be confused with struct HashInstrumentation... */
+typedef struct hash_instrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;	/* peak memory usage in bytes */
+	size_t	space_peak_tuples;	/* peak memory usage in bytes */
+} hash_instrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -706,6 +715,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	hash_instrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c052f7e..7c0e018 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -1133,9 +1133,11 @@ explain (costs off, timing off, summary off, analyze)
 --------------------------------------------------------
  HashAggregate (actual rows=0 loops=1)
    Hash Key: a, b
+   Buckets: 256
    Hash Key: a
+   Buckets: 256
    ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
-(4 rows)
+(6 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
  a | b | sum | count 
@@ -1157,11 +1159,12 @@ explain (costs off, timing off, summary off, analyze)
 --------------------------------------------------------
  MixedAggregate (actual rows=3 loops=1)
    Hash Key: a, b
+   Buckets: 256
    Group Key: ()
    Group Key: ()
    Group Key: ()
    ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
-(6 rows)
+(7 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
  sum | count 
@@ -1202,9 +1205,11 @@ explain (costs off, timing off, summary off, analyze)
 ---------------------------------------------------
  HashAggregate (actual rows=4 loops=1)
    Hash Key: a, b
+   Buckets: 4 (originally 2)
    Hash Key: a, c
+   Buckets: 4 (originally 2)
    ->  Seq Scan on gstest3 (actual rows=2 loops=1)
-(4 rows)
+(6 rows)
 
 -- simple rescan tests
 select a, b, sum(v.x)
@@ -1554,9 +1559,13 @@ explain (costs off, timing off, summary off, analyze)
 -----------------------------------------------------------
  MixedAggregate (actual rows=13116 loops=1)
    Hash Key: two
+   Buckets: 4 (originally 2)
    Hash Key: four
+   Buckets: 4 (originally 2)
    Hash Key: ten
+   Buckets: 8 (originally 4)
    Hash Key: hundred
+   Buckets: 16
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
@@ -1566,7 +1575,7 @@ explain (costs off, timing off, summary off, analyze)
          Sort Key: unique1
          Sort Method: external merge  Disk: 392kB
          ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
-(14 rows)
+(18 rows)
 
 explain (costs off, timing off, summary off, analyze)
   select unique1,
@@ -1578,15 +1587,19 @@ explain (costs off, timing off, summary off, analyze)
 -----------------------------------------------------------
  MixedAggregate (actual rows=10116 loops=1)
    Hash Key: two
+   Buckets: 4 (originally 2)
    Hash Key: four
+   Buckets: 4 (originally 2)
    Hash Key: ten
+   Buckets: 8 (originally 4)
    Hash Key: hundred
+   Buckets: 16
    Group Key: unique1
    ->  Sort (actual rows=10000 loops=1)
          Sort Key: unique1
          Sort Method: external merge  Disk: 392kB
          ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
-(10 rows)
+(14 rows)
 
 set work_mem = '384kB';
 explain (costs off)
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 94cf969..783c1da 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -306,7 +306,9 @@ explain (costs off, timing off, summary off, analyze)
                        ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
                              Rows Removed by Filter: 1010
-(11 rows)
+                     Buckets: 16384
+                     Null hashtable: Buckets: 1024
+(13 rows)
 
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 55991c8..410daa0 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -791,7 +791,9 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
      ->  Append (actual rows=2 loops=1)
            ->  Result (actual rows=1 loops=1)
            ->  Result (actual rows=1 loops=1)
-(5 rows)
+   Buckets: 4 (originally 2)
+   Null hashtable: Buckets: 2
+(7 rows)
 
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
  ?column? 
@@ -999,7 +1001,9 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
            Heap Fetches: 0
-(8 rows)
+   Buckets: 16384
+   Null hashtable: Buckets: 2
+(10 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 5ac1477..65797ee 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -355,13 +355,14 @@ select count(*) from
  Aggregate (actual rows=1 loops=1)
    ->  Subquery Scan on ss (actual rows=5000 loops=1)
          ->  HashSetOp Intersect (actual rows=5000 loops=1)
+               Buckets: 8192
                ->  Append (actual rows=20000 loops=1)
                      ->  Subquery Scan on "*SELECT* 2" (actual rows=10000 loops=1)
                            ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
                      ->  Subquery Scan on "*SELECT* 1" (actual rows=10000 loops=1)
                            ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1 (actual rows=10000 loops=1)
                                  Heap Fetches: 0
-(9 rows)
+(10 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -586,12 +587,13 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
                                           QUERY PLAN                                          
 ----------------------------------------------------------------------------------------------
  HashSetOp Intersect (actual rows=1 loops=1)
+   Buckets: 4 (originally 2)
    ->  Append (actual rows=8 loops=1)
          ->  Subquery Scan on "*SELECT* 1" (actual rows=5 loops=1)
                ->  Function Scan on generate_series (actual rows=5 loops=1)
          ->  Subquery Scan on "*SELECT* 2" (actual rows=3 loops=1)
                ->  Function Scan on generate_series generate_series_1 (actual rows=3 loops=1)
-(6 rows)
+(7 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 49d44e2..b8b1c8c 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -114,7 +114,7 @@ explain (costs off, timing off, summary off, analyze)
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 -- this is not parallel-safe due to use of random() within SubLink's testexpr:
-explain (costs off, timing off, summary off, analyze)
+explain (costs off)
 	select * from tenk1 where (unique1 + random())::integer not in
 	(select ten from tenk2);
 alter table tenk2 reset (parallel_workers);
-- 
2.7.4

v3-0003-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From eef7b08ecb50128dcec1de2866e0af8e49e93207 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v3 3/7] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c                | 46 +++++++++++++++------------
 src/test/regress/expected/select_parallel.out |  4 +--
 src/test/regress/expected/subselect.out       |  8 ++---
 3 files changed, 32 insertions(+), 26 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e262108..67a9840 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						ExplainState *es, SubPlanState *subplanstate);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -718,7 +718,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, es, NULL);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1080,7 +1080,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			ExplainState *es, SubPlanState *subplanstate)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1337,6 +1337,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			if (subplanstate->hashnulls) {
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1365,6 +1375,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+		if (subplanstate && subplanstate->hashtable)
+			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+		if (subplanstate && subplanstate->hashnulls) {
+			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2037,12 +2054,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, es, NULL);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, es, NULL);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2074,7 +2091,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, es, NULL);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3473,7 +3490,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, es, NULL);
 }
 
 /*
@@ -3531,18 +3548,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-			show_tuplehash_info(&sps->hashtable->instrument, es);
-		if (sps->hashnulls) {
-			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT) {
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, es);
-			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
-		}
+					relationship, sp->plan_name, es, sps);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3559,7 +3565,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es, NULL);
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 783c1da..bc270e0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -303,11 +303,11 @@ explain (costs off, timing off, summary off, analyze)
                ->  Parallel Seq Scan on tenk1 (actual rows=2000 loops=5)
                      Filter: (NOT (hashed SubPlan 1))
                      SubPlan 1
+                       Buckets: 16384
+                       Null hashtable: Buckets: 1024
                        ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
                              Rows Removed by Filter: 1010
-                     Buckets: 16384
-                     Null hashtable: Buckets: 1024
 (13 rows)
 
 select count(*) from tenk1 where (two, four) not in
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 410daa0..a6b9595 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -788,11 +788,11 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 ----------------------------------------------
  Result (actual rows=1 loops=1)
    SubPlan 1
+     Buckets: 4 (originally 2)
+     Null hashtable: Buckets: 2
      ->  Append (actual rows=2 loops=1)
            ->  Result (actual rows=1 loops=1)
            ->  Result (actual rows=1 loops=1)
-   Buckets: 4 (originally 2)
-   Null hashtable: Buckets: 2
 (7 rows)
 
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
@@ -999,10 +999,10 @@ select * from int4_tbl where
    ->  Seq Scan on int4_tbl (actual rows=5 loops=1)
    ->  Seq Scan on tenk1 b (actual rows=8000 loops=5)
    SubPlan 1
+     Buckets: 16384
+     Null hashtable: Buckets: 2
      ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
            Heap Fetches: 0
-   Buckets: 16384
-   Null hashtable: Buckets: 2
 (10 rows)
 
 select * from int4_tbl where
-- 
2.7.4

v3-0004-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From ab8425964a7cacf4bf613c25436d6b2c0df67284 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v3 4/7] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.
---
 src/backend/commands/explain.c            |  2 ++
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 20 ++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 30 insertions(+)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 67a9840..d71f5f1 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2908,6 +2908,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index ae8a11d..9ae99a3 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -744,6 +746,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index e102589..d1ef07c 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -43,6 +43,7 @@
 #include "access/htup_details.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 #include "utils/hashutils.h"
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	hash_instrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1148,6 +1151,23 @@ tbm_end_iterate(TBMIterator *iterator)
 }
 
 /*
+ * tbm_instrumentation - return pointer instrumentation data
+ *
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+hash_instrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable) {
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) * (tbm->nchunks ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
+/*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
  * This doesn't free any of the shared state associated with the iterator,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f929585..f5740c7 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1607,6 +1607,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	hash_instrumentation	instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fca..811a497 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct hash_instrumentation hash_instrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern hash_instrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.7.4

v3-0005-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From 54632109e4e57fd80594df6860e4e683aed15fac Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v3 5/7] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.
---
 src/backend/commands/explain.c            | 18 +++++++++---------
 src/backend/executor/execGrouping.c       | 25 -------------------------
 src/backend/executor/nodeAgg.c            |  9 ++++++---
 src/backend/executor/nodeRecursiveunion.c |  3 ++-
 src/backend/executor/nodeSetOp.c          |  3 ++-
 src/backend/executor/nodeSubplan.c        | 11 ++++++++---
 src/include/executor/executor.h           |  1 -
 src/include/executor/nodeAgg.h            |  1 +
 src/include/nodes/execnodes.h             | 22 +++++++++++++++++++++-
 9 files changed, 49 insertions(+), 44 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d71f5f1..1415bce 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1341,11 +1341,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			Assert(subplanstate != NULL);
 			/* Show hash stats for hashed subplan */
 			if (subplanstate->hashtable)
-				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument, es);
 			if (subplanstate->hashnulls) {
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			}
 		}
 		if (es->indent)
@@ -1376,10 +1376,10 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
 		if (subplanstate && subplanstate->hashtable)
-			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument, es);
 		if (subplanstate && subplanstate->hashnulls) {
 			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
 		}
 	}
@@ -1911,14 +1911,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, es);
+					show_tuplehash_info(&sos->instrument, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, es);
+					show_tuplehash_info(&rus->instrument, es);
 				break;
 			}
 		case T_Group:
@@ -2305,7 +2305,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes<=1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2332,7 +2332,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 	// this will show things twice??
 	show_grouping_set_keys(aggstate, agg, NULL,
 						   context, useprefix, ancestors, es,
-						   aggstate->num_hashes ? &aggstate->perhash[0].hashtable->instrument : NULL);
+						   aggstate->num_hashes ? &aggstate->perhash[0].instrument : NULL);
 
 	foreach(lc, agg->chain)
 	{
@@ -2344,7 +2344,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED) {
 			int	nth = list_cell_number(agg->chain, lc);
 			Assert(nth < aggstate->num_hashes);
-			inst = &aggstate->perhash[nth].hashtable->instrument;
+			inst = &aggstate->perhash[nth].instrument;
 		}
 		else
 			inst = NULL;
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index dcc365c..de0205f 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,7 +191,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -207,7 +206,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -286,32 +284,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial) {
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		hashtable->instrument.space_peak_hash = hashtable->hashtab->size * sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-#define maxself(a,b) a=Max(a,b)
-		/* hashtable->entrysize includes additionalsize */
-		maxself(hashtable->instrument.space_peak_hash,
-				hashtable->hashtab->size * sizeof(TupleHashEntryData);
-		maxself(hashtable->instrument.space_peak_tuples,
-				hashtable->hashtab->members * hashtable->entrysize);
-#undef maxself
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 81dda1b..396446b 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1268,6 +1268,7 @@ build_hash_table(AggState *aggstate)
 		if (perhash->hashtable)
 			ResetTupleHashTable(perhash->hashtable);
 		else
+		{
 			perhash->hashtable = BuildTupleHashTableExt(&aggstate->ss.ps,
 														perhash->hashslot->tts_tupleDescriptor,
 														perhash->numCols,
@@ -1281,6 +1282,8 @@ build_hash_table(AggState *aggstate)
 														aggstate->hashcontext->ecxt_per_tuple_memory,
 														tmpmem,
 														DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+			InitTupleHashTableStats(perhash->instrument, perhash->hashtable->hashtab, additionalsize);
+		}
 	}
 }
 
@@ -1666,7 +1669,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
-				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
+				UpdateTupleHashTableStats(aggstate->perhash[0].instrument, aggstate->perhash[0].hashtable->hashtab);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1869,7 +1872,7 @@ agg_retrieve_direct(AggState *aggstate)
 						aggstate->current_phase == 1)
 				{
 					for (int i = 0; i < aggstate->num_hashes; i++)
-						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+						UpdateTupleHashTableStats(aggstate->perhash[i].instrument, aggstate->perhash[i].hashtable->hashtab);
 				}
 			}
 
@@ -1947,7 +1950,7 @@ agg_fill_hash_table(AggState *aggstate)
 
 	aggstate->table_filled = true;
 	for (int i = 0; i < aggstate->num_hashes; i++)
-		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+		UpdateTupleHashTableStats(aggstate->perhash[i].instrument, aggstate->perhash[i].hashtable->hashtab);
 
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c2..594abdb 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,7 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, 0);
 }
 
 
@@ -157,7 +158,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab..4a56290 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,7 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+	InitTupleHashTableStats(setopstate->instrument, setopstate->hashtable->hashtab, 0);
 }
 
 /*
@@ -415,7 +416,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument, setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index eec849c..a5b71fa 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -507,6 +507,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -520,6 +521,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -534,7 +537,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashtable);
-		else
+		else {
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -548,6 +551,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab, 0);
+		}
 	}
 
 	/*
@@ -621,9 +626,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 34199b5..81fdfa4 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 264916f..008fda3 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -302,6 +302,7 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	hash_instrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f5740c7..df56330 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -688,9 +688,26 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, addsize) \
+	do{\
+	instr.entrysize = sizeof(MinimalTuple) + addsize; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = htable->size * sizeof(TupleHashEntryData); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, htable->size*sizeof(TupleHashEntryData)); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, htable->members*instr.entrysize );\
+	}while(0)
+
 /* XXX: not to be confused with struct HashInstrumentation... */
 typedef struct hash_instrumentation
 {
+	size_t	entrysize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;	/* peak memory usage in bytes */
@@ -715,7 +732,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	hash_instrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -881,6 +897,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	hash_instrumentation instrument;
+	hash_instrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1289,6 +1307,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	hash_instrumentation	instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -2322,6 +2341,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	hash_instrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.7.4

v3-0006-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From e7bfc9499ff75b2d6cd7a364ee1c164d20ba9697 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v3 6/7] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..d76a630 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -186,7 +186,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index df56330..6eac7de 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -724,7 +724,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.7.4

v3-0007-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From c0d1138e7d84375567ab958dc4bbabf039a9add7 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v3 7/7] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 396446b..472b2b1 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1499,8 +1499,7 @@ lookup_hash_entry(AggState *aggstate)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  */
-- 
2.7.4

#7Justin Pryzby
pryzby@telsasoft.com
In reply to: Justin Pryzby (#6)
7 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

On Sun, Feb 16, 2020 at 11:53:07AM -0600, Justin Pryzby wrote:

Updated:

. remove from explain analyze those tests which would display sort
Memory/Disk. Oops.

. Rebased on top of 5b618e1f48aecc66e3a9f60289491da520faae19
. Updated to avoid sort's Disk output, for real this time.
. And fixed a syntax error in an intermediate commit.

--
Justin

Attachments:

v4-0001-Run-some-existing-tests-with-explain-ANALYZE.patchtext/x-diff; charset=us-asciiDownload
From 19ad84696710de7b5ac19e1124856701697d28c0 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 12:03:11 -0600
Subject: [PATCH v4 1/7] Run some existing tests with explain (ANALYZE)..

..in a separate, earlier patch, to better show what bits are added by later
patches for hashtable instrumentation.
---
 src/test/regress/expected/groupingsets.out    | 57 +++++++++++-----------
 src/test/regress/expected/select_parallel.out | 20 ++++----
 src/test/regress/expected/subselect.out       | 69 +++++++++++++++++++++++++++
 src/test/regress/expected/union.out           | 43 +++++++++--------
 src/test/regress/sql/groupingsets.sql         | 12 ++---
 src/test/regress/sql/select_parallel.sql      |  4 +-
 src/test/regress/sql/subselect.sql            | 25 ++++++++++
 src/test/regress/sql/union.sql                |  4 +-
 8 files changed, 166 insertions(+), 68 deletions(-)

diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c1f802c..95d619c 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -458,16 +458,17 @@ ERROR:  aggregate functions are not allowed in FROM clause of their own query le
 LINE 3:        lateral (select a, b, sum(v.x) from gstest_data(v.x) ...
                                      ^
 -- min max optimization should still work with GROUP BY ()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select min(unique1) from tenk1 GROUP BY ();
-                         QUERY PLAN                         
-------------------------------------------------------------
- Result
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ Result (actual rows=1 loops=1)
    InitPlan 1 (returns $0)
-     ->  Limit
-           ->  Index Only Scan using tenk1_unique1 on tenk1
+     ->  Limit (actual rows=1 loops=1)
+           ->  Index Only Scan using tenk1_unique1 on tenk1 (actual rows=1 loops=1)
                  Index Cond: (unique1 IS NOT NULL)
-(5 rows)
+                 Heap Fetches: 0
+(6 rows)
 
 -- Views with GROUPING SET queries
 CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c)
@@ -1126,14 +1127,14 @@ select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a)
 ---+---+-----+-------
 (0 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
-           QUERY PLAN           
---------------------------------
- HashAggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ HashAggregate (actual rows=0 loops=1)
    Hash Key: a, b
    Hash Key: a
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (4 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
@@ -1150,16 +1151,16 @@ select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),()
    |   |     |     0
 (3 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
-           QUERY PLAN           
---------------------------------
- MixedAggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ MixedAggregate (actual rows=3 loops=1)
    Hash Key: a, b
    Group Key: ()
    Group Key: ()
    Group Key: ()
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (6 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
@@ -1170,15 +1171,15 @@ select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
      |     0
 (3 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
-           QUERY PLAN           
---------------------------------
- Aggregate
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Aggregate (actual rows=3 loops=1)
    Group Key: ()
    Group Key: ()
    Group Key: ()
-   ->  Seq Scan on gstest_empty
+   ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
 (5 rows)
 
 -- check that functionally dependent cols are not nulled
@@ -1193,16 +1194,16 @@ select a, d, grouping(a,b,c)
  2 | 2 |        2
 (4 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, d, grouping(a,b,c)
     from gstest3
    group by grouping sets ((a,b), (a,c));
-        QUERY PLAN         
----------------------------
- HashAggregate
+                    QUERY PLAN                     
+---------------------------------------------------
+ HashAggregate (actual rows=4 loops=1)
    Hash Key: a, b
    Hash Key: a, c
-   ->  Seq Scan on gstest3
+   ->  Seq Scan on gstest3 (actual rows=2 loops=1)
 (4 rows)
 
 -- simple rescan tests
@@ -1242,7 +1243,7 @@ select *
 ERROR:  aggregate functions are not allowed in FROM clause of their own query level
 LINE 3:        lateral (select a, b, sum(v.x) from gstest_data(v.x) ...
                                      ^
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select *
     from (values (1),(2)) v(x),
          lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 96dfb7c..94cf969 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -290,21 +290,23 @@ execute tenk1_count(1);
 deallocate tenk1_count;
 -- test parallel plans for queries containing un-correlated subplans.
 alter table tenk2 set (parallel_workers = 0);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
-                      QUERY PLAN                      
-------------------------------------------------------
- Finalize Aggregate
-   ->  Gather
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Finalize Aggregate (actual rows=1 loops=1)
+   ->  Gather (actual rows=5 loops=1)
          Workers Planned: 4
-         ->  Partial Aggregate
-               ->  Parallel Seq Scan on tenk1
+         Workers Launched: 4
+         ->  Partial Aggregate (actual rows=1 loops=5)
+               ->  Parallel Seq Scan on tenk1 (actual rows=2000 loops=5)
                      Filter: (NOT (hashed SubPlan 1))
                      SubPlan 1
-                       ->  Seq Scan on tenk2
+                       ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
-(9 rows)
+                             Rows Removed by Filter: 1010
+(11 rows)
 
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 71a677b..55991c8 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -782,6 +782,17 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
                  Output: 'bar'::name
 (8 rows)
 
+explain (analyze, timing off, summary off, costs off)
+select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
+                  QUERY PLAN                  
+----------------------------------------------
+ Result (actual rows=1 loops=1)
+   SubPlan 1
+     ->  Append (actual rows=2 loops=1)
+           ->  Result (actual rows=1 loops=1)
+           ->  Result (actual rows=1 loops=1)
+(5 rows)
+
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
  ?column? 
 ----------
@@ -974,6 +985,22 @@ select * from int4_tbl where
            Output: a.unique1
 (10 rows)
 
+explain (analyze, timing off, summary off, costs off)
+select * from int4_tbl where
+  (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
+  (select ten from tenk1 b);
+                                          QUERY PLAN                                           
+-----------------------------------------------------------------------------------------------
+ Nested Loop Semi Join (actual rows=1 loops=1)
+   Join Filter: (CASE WHEN (hashed SubPlan 1) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
+   Rows Removed by Join Filter: 40000
+   ->  Seq Scan on int4_tbl (actual rows=5 loops=1)
+   ->  Seq Scan on tenk1 b (actual rows=8000 loops=5)
+   SubPlan 1
+     ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
+           Heap Fetches: 0
+(8 rows)
+
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
@@ -1377,6 +1404,29 @@ select * from x;
                        Output: z1.a
 (16 rows)
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ CTE Scan on x (actual rows=22 loops=1)
+   CTE x
+     ->  Recursive Union (actual rows=22 loops=1)
+           ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+           ->  Nested Loop (actual rows=7 loops=3)
+                 Join Filter: (length((z.a || z1.a)) < 5)
+                 Rows Removed by Join Filter: 85
+                 CTE z
+                   ->  WorkTable Scan on x x_1 (actual rows=7 loops=3)
+                 ->  CTE Scan on z (actual rows=7 loops=3)
+                 ->  CTE Scan on z z1 (actual rows=13 loops=22)
+(11 rows)
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
@@ -1431,6 +1481,25 @@ select * from x;
                  Filter: (length((x_1.a || x_1.a)) < 5)
 (9 rows)
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ CTE Scan on x (actual rows=6 loops=1)
+   CTE x
+     ->  Recursive Union (actual rows=6 loops=1)
+           ->  Values Scan on "*VALUES*" (actual rows=2 loops=1)
+           ->  WorkTable Scan on x x_1 (actual rows=1 loops=3)
+                 Filter: (length((a || a)) < 5)
+                 Rows Removed by Filter: 1
+(7 rows)
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92..5ac1477 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -347,20 +347,21 @@ ERROR:  FOR NO KEY UPDATE is not allowed with UNION/INTERSECT/EXCEPT
 
 -- exercise both hashed and sorted implementations of INTERSECT/EXCEPT
 set enable_hashagg to on;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
-                                     QUERY PLAN                                     
-------------------------------------------------------------------------------------
- Aggregate
-   ->  Subquery Scan on ss
-         ->  HashSetOp Intersect
-               ->  Append
-                     ->  Subquery Scan on "*SELECT* 2"
-                           ->  Seq Scan on tenk1
-                     ->  Subquery Scan on "*SELECT* 1"
-                           ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(8 rows)
+                                                   QUERY PLAN                                                   
+----------------------------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=1)
+   ->  Subquery Scan on ss (actual rows=5000 loops=1)
+         ->  HashSetOp Intersect (actual rows=5000 loops=1)
+               ->  Append (actual rows=20000 loops=1)
+                     ->  Subquery Scan on "*SELECT* 2" (actual rows=10000 loops=1)
+                           ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
+                     ->  Subquery Scan on "*SELECT* 1" (actual rows=10000 loops=1)
+                           ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1 (actual rows=10000 loops=1)
+                                 Heap Fetches: 0
+(9 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -580,16 +581,16 @@ select from generate_series(1,5) union select from generate_series(1,3);
          ->  Function Scan on generate_series generate_series_1
 (4 rows)
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select from generate_series(1,5) intersect select from generate_series(1,3);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- HashSetOp Intersect
-   ->  Append
-         ->  Subquery Scan on "*SELECT* 1"
-               ->  Function Scan on generate_series
-         ->  Subquery Scan on "*SELECT* 2"
-               ->  Function Scan on generate_series generate_series_1
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ HashSetOp Intersect (actual rows=1 loops=1)
+   ->  Append (actual rows=8 loops=1)
+         ->  Subquery Scan on "*SELECT* 1" (actual rows=5 loops=1)
+               ->  Function Scan on generate_series (actual rows=5 loops=1)
+         ->  Subquery Scan on "*SELECT* 2" (actual rows=3 loops=1)
+               ->  Function Scan on generate_series generate_series_1 (actual rows=3 loops=1)
 (6 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
diff --git a/src/test/regress/sql/groupingsets.sql b/src/test/regress/sql/groupingsets.sql
index 95ac3fb..e6a18bf 100644
--- a/src/test/regress/sql/groupingsets.sql
+++ b/src/test/regress/sql/groupingsets.sql
@@ -183,7 +183,7 @@ select *
        lateral (select a, b, sum(v.x) from gstest_data(v.x) group by rollup (a,b)) s;
 
 -- min max optimization should still work with GROUP BY ()
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select min(unique1) from tenk1 GROUP BY ();
 
 -- Views with GROUPING SET queries
@@ -327,21 +327,21 @@ explain (costs off)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),(),(),());
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
 
 -- check that functionally dependent cols are not nulled
 select a, d, grouping(a,b,c)
   from gstest3
  group by grouping sets ((a,b), (a,c));
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select a, d, grouping(a,b,c)
     from gstest3
    group by grouping sets ((a,b), (a,c));
@@ -360,7 +360,7 @@ explain (costs off)
 select *
   from (values (1),(2)) v(x),
        lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
   select *
     from (values (1),(2)) v(x),
          lateral (select a, b, sum(v.x) from gstest_data(v.x) group by grouping sets (a,b)) s;
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 11e7735..49d44e2 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -108,13 +108,13 @@ deallocate tenk1_count;
 
 -- test parallel plans for queries containing un-correlated subplans.
 alter table tenk2 set (parallel_workers = 0);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 -- this is not parallel-safe due to use of random() within SubLink's testexpr:
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 	select * from tenk1 where (unique1 + random())::integer not in
 	(select ten from tenk2);
 alter table tenk2 reset (parallel_workers);
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index bd8d2f6..b7e7734 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -460,6 +460,9 @@ select * from outer_text where (f1, f2) not in (select * from inner_text);
 explain (verbose, costs off)
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 
+explain (analyze, timing off, summary off, costs off)
+select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
+
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 
 --
@@ -539,6 +542,10 @@ explain (verbose, costs off)
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
+explain (analyze, timing off, summary off, costs off)
+select * from int4_tbl where
+  (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
+  (select ten from tenk1 b);
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
   (select ten from tenk1 b);
@@ -722,6 +729,15 @@ with recursive x(a) as
     where length(z.a || z1.a) < 5))
 select * from x;
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z1.a as a from z cross join z as z1
+    where length(z.a || z1.a) < 5))
+select * from x;
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
@@ -739,6 +755,15 @@ with recursive x(a) as
     where length(z.a || z.a) < 5))
 select * from x;
 
+explain (analyze, timing off, summary off, costs off)
+with recursive x(a) as
+  ((values ('a'), ('b'))
+   union all
+   (with z as not materialized (select * from x)
+    select z.a || z.a as a from z
+    where length(z.a || z.a) < 5))
+select * from x;
+
 with recursive x(a) as
   ((values ('a'), ('b'))
    union all
diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql
index 5f4881d..075bb1d 100644
--- a/src/test/regress/sql/union.sql
+++ b/src/test/regress/sql/union.sql
@@ -122,7 +122,7 @@ SELECT q1 FROM int8_tbl EXCEPT ALL SELECT q1 FROM int8_tbl FOR NO KEY UPDATE;
 
 set enable_hashagg to on;
 
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
 select count(*) from
@@ -204,7 +204,7 @@ set enable_sort = false;
 
 explain (costs off)
 select from generate_series(1,5) union select from generate_series(1,3);
-explain (costs off)
+explain (costs off, timing off, summary off, analyze)
 select from generate_series(1,5) intersect select from generate_series(1,3);
 
 select from generate_series(1,5) union select from generate_series(1,3);
-- 
2.7.4

v4-0002-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From 02f3cc981ccdf5ca3f69d6f80172e63db247955e Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v4 2/7] explain to show tuplehash bucket and memory stats..

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 src/backend/commands/explain.c                | 131 +++++++++++++++++++++++---
 src/backend/executor/execGrouping.c           |  25 +++++
 src/backend/executor/nodeAgg.c                |  11 +++
 src/backend/executor/nodeRecursiveunion.c     |   3 +
 src/backend/executor/nodeSetOp.c              |   1 +
 src/backend/executor/nodeSubplan.c            |   3 +
 src/include/executor/executor.h               |   1 +
 src/include/nodes/execnodes.h                 |  10 ++
 src/test/regress/expected/groupingsets.out    |  11 ++-
 src/test/regress/expected/select_parallel.out |   4 +-
 src/test/regress/expected/subselect.out       |   8 +-
 src/test/regress/expected/union.out           |   6 +-
 src/test/regress/sql/select_parallel.sql      |   2 +-
 13 files changed, 195 insertions(+), 21 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d901dc4..e262108 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -86,12 +87,13 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *planstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_keys(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors, ExplainState *es,
+								   hash_instrumentation *inst);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -104,6 +106,7 @@ static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
+static void show_tuplehash_info(hash_instrumentation *inst, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1489,6 +1492,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1886,6 +1890,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, es);
+				break;
+			}
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2262,21 +2280,26 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
-		else
+			show_grouping_sets(astate, plan, ancestors, es);
+		else {
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes<=1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
@@ -2289,27 +2312,43 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	// this will show things twice??
+	show_grouping_set_keys(aggstate, agg, NULL,
+						   context, useprefix, ancestors, es,
+						   aggstate->num_hashes ? &aggstate->perhash[0].hashtable->instrument : NULL);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		hash_instrumentation *inst;
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED) {
+			int	nth = list_cell_number(agg->chain, lc);
+			Assert(nth < aggstate->num_hashes);
+			inst = &aggstate->perhash[nth].hashtable->instrument;
+		}
+		else
+			inst = NULL;
+
+		show_grouping_set_keys(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors, es,
+							   inst);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_keys(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, ExplainState *es,
+					   hash_instrumentation *inst)
+
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2369,6 +2408,10 @@ show_grouping_set_keys(PlanState *planstate,
 			ExplainPropertyText(keyname, "()", es);
 		else
 			ExplainPropertyListNested(keyname, result, es);
+
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+			show_tuplehash_info(inst, es);
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
@@ -2770,6 +2813,59 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 }
 
 /*
+ * Show hash bucket stats and (optionally) memory.
+ */
+
+// fprintf(stderr, "memallocated %lu\n", astate->hashcontext->ecxt_per_query_memory->mem_allocated);
+// perhash->aggnode->numGroups; memctx; AggState->
+static void
+show_tuplehash_info(hash_instrumentation *inst, ExplainState *es)
+{
+	long	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
+
+	if (!es->analyze)
+		return;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets) {
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld",
+						inst->nbuckets);
+		}
+
+		if (es->verbose)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %ldkB, tuples: %ldkB",
+					spacePeakKb_hash, spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+	}
+}
+
+/*
  * If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
  */
 static void
@@ -3436,6 +3532,17 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+			show_tuplehash_info(&sps->hashtable->instrument, es);
+		if (sps->hashnulls) {
+			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT) {
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, es);
+			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..cedb023 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,6 +191,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -206,6 +207,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -284,9 +286,32 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial) {
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size * sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+#define maxself(a,b) a=Max(a,b)
+		/* hashtable->entrysize includes additionalsize */
+		maxself(hashtable->instrument.space_peak_hash,
+				hashtable->hashtab->size * sizeof(TupleHashEntryData));
+		maxself(hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * hashtable->entrysize);
+#undef maxself
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 2e9a21b..f90453c 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1704,6 +1704,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
+				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1901,6 +1902,13 @@ agg_retrieve_direct(AggState *aggstate)
 						}
 					}
 				}
+
+				if (aggstate->aggstrategy == AGG_MIXED &&
+						aggstate->current_phase == 1)
+				{
+					for (int i = 0; i < aggstate->num_hashes; i++)
+						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+				}
 			}
 
 			/*
@@ -1975,6 +1983,9 @@ agg_fill_hash_table(AggState *aggstate)
 	}
 
 	aggstate->table_filled = true;
+	for (int i = 0; i < aggstate->num_hashes; i++)
+		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
 	ResetTupleHashIterator(aggstate->perhash[0].hashtable,
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a..93272c2 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a..9c0e0ab 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index ff95317..eec849c 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 81fdfa4..34199b5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cd3ddf7..504b557 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,6 +691,15 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+/* XXX: not to be confused with struct HashInstrumentation... */
+typedef struct hash_instrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;	/* peak memory usage in bytes */
+	size_t	space_peak_tuples;	/* peak memory usage in bytes */
+} hash_instrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -709,6 +718,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	hash_instrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index 95d619c..b77c43c 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -1133,9 +1133,11 @@ explain (costs off, timing off, summary off, analyze)
 --------------------------------------------------------
  HashAggregate (actual rows=0 loops=1)
    Hash Key: a, b
+   Buckets: 256
    Hash Key: a
+   Buckets: 256
    ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
-(4 rows)
+(6 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
  a | b | sum | count 
@@ -1157,11 +1159,12 @@ explain (costs off, timing off, summary off, analyze)
 --------------------------------------------------------
  MixedAggregate (actual rows=3 loops=1)
    Hash Key: a, b
+   Buckets: 256
    Group Key: ()
    Group Key: ()
    Group Key: ()
    ->  Seq Scan on gstest_empty (actual rows=0 loops=1)
-(6 rows)
+(7 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
  sum | count 
@@ -1202,9 +1205,11 @@ explain (costs off, timing off, summary off, analyze)
 ---------------------------------------------------
  HashAggregate (actual rows=4 loops=1)
    Hash Key: a, b
+   Buckets: 4 (originally 2)
    Hash Key: a, c
+   Buckets: 4 (originally 2)
    ->  Seq Scan on gstest3 (actual rows=2 loops=1)
-(4 rows)
+(6 rows)
 
 -- simple rescan tests
 select a, b, sum(v.x)
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 94cf969..783c1da 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -306,7 +306,9 @@ explain (costs off, timing off, summary off, analyze)
                        ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
                              Rows Removed by Filter: 1010
-(11 rows)
+                     Buckets: 16384
+                     Null hashtable: Buckets: 1024
+(13 rows)
 
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 55991c8..410daa0 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -791,7 +791,9 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
      ->  Append (actual rows=2 loops=1)
            ->  Result (actual rows=1 loops=1)
            ->  Result (actual rows=1 loops=1)
-(5 rows)
+   Buckets: 4 (originally 2)
+   Null hashtable: Buckets: 2
+(7 rows)
 
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
  ?column? 
@@ -999,7 +1001,9 @@ select * from int4_tbl where
    SubPlan 1
      ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
            Heap Fetches: 0
-(8 rows)
+   Buckets: 16384
+   Null hashtable: Buckets: 2
+(10 rows)
 
 select * from int4_tbl where
   (case when f1 in (select unique1 from tenk1 a) then f1 else null end) in
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 5ac1477..65797ee 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -355,13 +355,14 @@ select count(*) from
  Aggregate (actual rows=1 loops=1)
    ->  Subquery Scan on ss (actual rows=5000 loops=1)
          ->  HashSetOp Intersect (actual rows=5000 loops=1)
+               Buckets: 8192
                ->  Append (actual rows=20000 loops=1)
                      ->  Subquery Scan on "*SELECT* 2" (actual rows=10000 loops=1)
                            ->  Seq Scan on tenk1 (actual rows=10000 loops=1)
                      ->  Subquery Scan on "*SELECT* 1" (actual rows=10000 loops=1)
                            ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1 (actual rows=10000 loops=1)
                                  Heap Fetches: 0
-(9 rows)
+(10 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -586,12 +587,13 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
                                           QUERY PLAN                                          
 ----------------------------------------------------------------------------------------------
  HashSetOp Intersect (actual rows=1 loops=1)
+   Buckets: 4 (originally 2)
    ->  Append (actual rows=8 loops=1)
          ->  Subquery Scan on "*SELECT* 1" (actual rows=5 loops=1)
                ->  Function Scan on generate_series (actual rows=5 loops=1)
          ->  Subquery Scan on "*SELECT* 2" (actual rows=3 loops=1)
                ->  Function Scan on generate_series generate_series_1 (actual rows=3 loops=1)
-(6 rows)
+(7 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 49d44e2..b8b1c8c 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -114,7 +114,7 @@ explain (costs off, timing off, summary off, analyze)
 select count(*) from tenk1 where (two, four) not in
 	(select hundred, thousand from tenk2 where thousand > 100);
 -- this is not parallel-safe due to use of random() within SubLink's testexpr:
-explain (costs off, timing off, summary off, analyze)
+explain (costs off)
 	select * from tenk1 where (unique1 + random())::integer not in
 	(select ten from tenk2);
 alter table tenk2 reset (parallel_workers);
-- 
2.7.4

v4-0003-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From 2aab5cf3e3486845039aeaf536cf7a6e728fa266 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v4 3/7] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c                | 46 +++++++++++++++------------
 src/test/regress/expected/select_parallel.out |  4 +--
 src/test/regress/expected/subselect.out       |  8 ++---
 3 files changed, 32 insertions(+), 26 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e262108..67a9840 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						ExplainState *es, SubPlanState *subplanstate);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -718,7 +718,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, es, NULL);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1080,7 +1080,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			ExplainState *es, SubPlanState *subplanstate)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1337,6 +1337,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			if (subplanstate->hashnulls) {
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1365,6 +1375,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+		if (subplanstate && subplanstate->hashtable)
+			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+		if (subplanstate && subplanstate->hashnulls) {
+			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2037,12 +2054,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, es, NULL);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, es, NULL);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2074,7 +2091,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, es, NULL);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3473,7 +3490,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, es, NULL);
 }
 
 /*
@@ -3531,18 +3548,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-			show_tuplehash_info(&sps->hashtable->instrument, es);
-		if (sps->hashnulls) {
-			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT) {
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, es);
-			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
-		}
+					relationship, sp->plan_name, es, sps);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3559,7 +3565,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es, NULL);
 }
 
 /*
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 783c1da..bc270e0 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -303,11 +303,11 @@ explain (costs off, timing off, summary off, analyze)
                ->  Parallel Seq Scan on tenk1 (actual rows=2000 loops=5)
                      Filter: (NOT (hashed SubPlan 1))
                      SubPlan 1
+                       Buckets: 16384
+                       Null hashtable: Buckets: 1024
                        ->  Seq Scan on tenk2 (actual rows=8990 loops=5)
                              Filter: (thousand > 100)
                              Rows Removed by Filter: 1010
-                     Buckets: 16384
-                     Null hashtable: Buckets: 1024
 (13 rows)
 
 select count(*) from tenk1 where (two, four) not in
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 410daa0..a6b9595 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -788,11 +788,11 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
 ----------------------------------------------
  Result (actual rows=1 loops=1)
    SubPlan 1
+     Buckets: 4 (originally 2)
+     Null hashtable: Buckets: 2
      ->  Append (actual rows=2 loops=1)
            ->  Result (actual rows=1 loops=1)
            ->  Result (actual rows=1 loops=1)
-   Buckets: 4 (originally 2)
-   Null hashtable: Buckets: 2
 (7 rows)
 
 select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
@@ -999,10 +999,10 @@ select * from int4_tbl where
    ->  Seq Scan on int4_tbl (actual rows=5 loops=1)
    ->  Seq Scan on tenk1 b (actual rows=8000 loops=5)
    SubPlan 1
+     Buckets: 16384
+     Null hashtable: Buckets: 2
      ->  Index Only Scan using tenk1_unique1 on tenk1 a (actual rows=10000 loops=1)
            Heap Fetches: 0
-   Buckets: 16384
-   Null hashtable: Buckets: 2
 (10 rows)
 
 select * from int4_tbl where
-- 
2.7.4

v4-0004-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From 6d7db8ce29948b9b919d3aa634db53a731d7c9d3 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v4 4/7] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.
---
 src/backend/commands/explain.c            |  2 ++
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 20 ++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 30 insertions(+)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 67a9840..d71f5f1 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2908,6 +2908,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index ae8a11d..9ae99a3 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -744,6 +746,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index e102589..d1ef07c 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -43,6 +43,7 @@
 #include "access/htup_details.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 #include "utils/hashutils.h"
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	hash_instrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1148,6 +1151,23 @@ tbm_end_iterate(TBMIterator *iterator)
 }
 
 /*
+ * tbm_instrumentation - return pointer instrumentation data
+ *
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+hash_instrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable) {
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) * (tbm->nchunks ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
+/*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
  * This doesn't free any of the shared state associated with the iterator,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 504b557..5de84e4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1610,6 +1610,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	hash_instrumentation	instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fca..811a497 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct hash_instrumentation hash_instrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern hash_instrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.7.4

v4-0005-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From 52911a18dac295637f99b6346809e56738e2ab41 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v4 5/7] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.
---
 src/backend/commands/explain.c            | 18 +++++++++---------
 src/backend/executor/execGrouping.c       | 25 -------------------------
 src/backend/executor/nodeAgg.c            |  7 ++++---
 src/backend/executor/nodeRecursiveunion.c |  3 ++-
 src/backend/executor/nodeSetOp.c          |  3 ++-
 src/backend/executor/nodeSubplan.c        | 11 ++++++++---
 src/include/executor/executor.h           |  1 -
 src/include/executor/nodeAgg.h            |  1 +
 src/include/nodes/execnodes.h             | 22 +++++++++++++++++++++-
 9 files changed, 47 insertions(+), 44 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d71f5f1..1415bce 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1341,11 +1341,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			Assert(subplanstate != NULL);
 			/* Show hash stats for hashed subplan */
 			if (subplanstate->hashtable)
-				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument, es);
 			if (subplanstate->hashnulls) {
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			}
 		}
 		if (es->indent)
@@ -1376,10 +1376,10 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
 		if (subplanstate && subplanstate->hashtable)
-			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument, es);
 		if (subplanstate && subplanstate->hashnulls) {
 			ExplainOpenGroup("Null hashtable", "Null hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			ExplainCloseGroup("Null hashtable", "Null hashtable", true, es);
 		}
 	}
@@ -1911,14 +1911,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, es);
+					show_tuplehash_info(&sos->instrument, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, es);
+					show_tuplehash_info(&rus->instrument, es);
 				break;
 			}
 		case T_Group:
@@ -2305,7 +2305,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes<=1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2332,7 +2332,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 	// this will show things twice??
 	show_grouping_set_keys(aggstate, agg, NULL,
 						   context, useprefix, ancestors, es,
-						   aggstate->num_hashes ? &aggstate->perhash[0].hashtable->instrument : NULL);
+						   aggstate->num_hashes ? &aggstate->perhash[0].instrument : NULL);
 
 	foreach(lc, agg->chain)
 	{
@@ -2344,7 +2344,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED) {
 			int	nth = list_cell_number(agg->chain, lc);
 			Assert(nth < aggstate->num_hashes);
-			inst = &aggstate->perhash[nth].hashtable->instrument;
+			inst = &aggstate->perhash[nth].instrument;
 		}
 		else
 			inst = NULL;
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index cedb023..de0205f 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,7 +191,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -207,7 +206,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -286,32 +284,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial) {
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		hashtable->instrument.space_peak_hash = hashtable->hashtab->size * sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-#define maxself(a,b) a=Max(a,b)
-		/* hashtable->entrysize includes additionalsize */
-		maxself(hashtable->instrument.space_peak_hash,
-				hashtable->hashtab->size * sizeof(TupleHashEntryData));
-		maxself(hashtable->instrument.space_peak_tuples,
-				hashtable->hashtab->members * hashtable->entrysize);
-#undef maxself
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index f90453c..f432825 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1328,6 +1328,7 @@ build_hash_table(AggState *aggstate, int setno, long nbuckets)
 		hashcxt,
 		tmpcxt,
 		DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+	InitTupleHashTableStats(perhash->instrument, perhash->hashtable->hashtab, additionalsize);
 }
 
 /*
@@ -1704,7 +1705,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
-				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
+				UpdateTupleHashTableStats(aggstate->perhash[0].instrument, aggstate->perhash[0].hashtable->hashtab);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1907,7 +1908,7 @@ agg_retrieve_direct(AggState *aggstate)
 						aggstate->current_phase == 1)
 				{
 					for (int i = 0; i < aggstate->num_hashes; i++)
-						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+						UpdateTupleHashTableStats(aggstate->perhash[i].instrument, aggstate->perhash[i].hashtable->hashtab);
 				}
 			}
 
@@ -1984,7 +1985,7 @@ agg_fill_hash_table(AggState *aggstate)
 
 	aggstate->table_filled = true;
 	for (int i = 0; i < aggstate->num_hashes; i++)
-		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+		UpdateTupleHashTableStats(aggstate->perhash[i].instrument, aggstate->perhash[i].hashtable->hashtab);
 
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c2..594abdb 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,7 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, 0);
 }
 
 
@@ -157,7 +158,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab..4a56290 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,7 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+	InitTupleHashTableStats(setopstate->instrument, setopstate->hashtable->hashtab, 0);
 }
 
 /*
@@ -415,7 +416,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument, setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index eec849c..a5b71fa 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -507,6 +507,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -520,6 +521,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -534,7 +537,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashtable);
-		else
+		else {
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -548,6 +551,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab, 0);
+		}
 	}
 
 	/*
@@ -621,9 +626,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 34199b5..81fdfa4 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 264916f..008fda3 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -302,6 +302,7 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	hash_instrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5de84e4..87b1127 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,9 +691,26 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, addsize) \
+	do{\
+	instr.entrysize = sizeof(MinimalTuple) + addsize; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = htable->size * sizeof(TupleHashEntryData); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, htable->size*sizeof(TupleHashEntryData)); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, htable->members*instr.entrysize );\
+	}while(0)
+
 /* XXX: not to be confused with struct HashInstrumentation... */
 typedef struct hash_instrumentation
 {
+	size_t	entrysize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;	/* peak memory usage in bytes */
@@ -718,7 +735,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	hash_instrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -884,6 +900,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	hash_instrumentation instrument;
+	hash_instrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1292,6 +1310,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	hash_instrumentation	instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -2325,6 +2344,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	hash_instrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.7.4

v4-0006-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From ac9b18cc581f19c07031e6e2a7bbe172b8c1b50d Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v4 6/7] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..d76a630 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -186,7 +186,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 87b1127..3f9ac5e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -727,7 +727,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.7.4

v4-0007-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From d987f98ab8cb531b687c63a68e185d8bbbe8b71b Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v4 7/7] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index f432825..1faea5b 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1530,8 +1530,7 @@ lookup_hash_entry(AggState *aggstate, uint32 hash)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  */
-- 
2.7.4

#8Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Justin Pryzby (#7)
Re: explain HashAggregate to report bucket and memory stats

Hi,

I've started looking at this patch, because I've been long missing the
information about hashagg hash table, so I'm pleased someone is working
on this. In general I agree it may be useful to add simila information
to other nodes using a hashtable, but IMHO the hashagg bit is the most
useful, so maybe we should focus on it first.

A couple of comments after an initial review of the patches:

1) explain.c API

The functions in explain.c (even the static ones) follow the convention
that the last parameter is (ExplainState *es). I think we should stick
to this, so the new parameters should be added before it.

For example instead of

static void ExplainNode(PlanState *planstate, List *ancestors,
const char *relationship, const char *plan_name,
ExplainState *es, SubPlanState *subplanstate);

we should do e.g.

static void ExplainNode(PlanState *planstate, SubPlanState *subplanstate,
List *ancestors,
const char *relationship, const char *plan_name,
ExplainState *es);

Also, the first show_grouping_sets should be renamed to aggstate to make
it consistent with the type change.

2) The hash_instrumentation is a bit inconsistent with what we already
have. We have Instrumentation, JitInstrumentation, WorkerInstrumentation
and HashInstrumentation, so I suggest we follow the same patter and call
this HashTableInstrumentation or something like that.

3) Almost all executor nodes that are modified to include this new
instrumentation struct also include TupleHashTable, and the data are
essentially about the hash table. So my question is why not to include
this into TupleHashTable - that would mean we don't need to modify any
executor nodes, and it'd probably simplify code in explain.c too because
we could simply pass the hashtable.

It'd also simplify e.g. SubPlanState where we have to add two new fields
and make sure to update the right one.

4) The one exception to (3) is BitmapHeapScanState, which does include
TIDBitmap and not TupleHashTable. And then we have tbm_instrumentation
which "fakes" the data based on the pagetable. Maybe this is a sign that
TIDBitmap needs a slightly different struct? Also, I'm not sure why we
actually need tbm_instrumentation()? It just copies the instrumentation
data from TIDBitmap into the node level, but why couldn't we just look
at the instrumentation data in TIDBitmap directly?

5) I think the explain for grouping sets need a rething. Teh function
show_grouping_set_keys was originally meant to print just the keys, but
now it's also printing the hash table stats. IMO we need a new function
printing a grouping set info - calling show_grouping_set_keys to print
the keys, but then also printing the extra hashtable info.

6) subplan explain

I probably agree the hashtable info should be included in the subplan,
not in the parent node. Otherwise it's confusing, particularly when the
node has multiple subplans. The one thing I find a bit strange is this:

explain (analyze, timing off, summary off, costs off)
select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
QUERY PLAN
----------------------------------------------
Result (actual rows=1 loops=1)
SubPlan 1
Buckets: 4 (originally 2)
Null hashtable: Buckets: 2
-> Append (actual rows=2 loops=1)
-> Result (actual rows=1 loops=1)
-> Result (actual rows=1 loops=1)
(7 rows)

That is, there's no indication why would this use a hash table, because
the "hashed subplan" is included only in verbose mode:

explain (analyze, verbose, timing off, summary off, costs off)
select 'foo'::text in (select 'bar'::name union all select 'bar'::name);
QUERY PLAN
----------------------------------------------------------------------------------
Result (actual rows=1 loops=1)
Output: (hashed SubPlan 1)
SubPlan 1
Buckets: 4 (originally 2) Memory Usage Hash: 1kB Memory Usage Tuples: 1kB
Null hashtable: Buckets: 2 Memory Usage Hash: 1kB Memory Usage Tuples: 0kB
-> Append (actual rows=2 loops=1)
-> Result (actual rows=1 loops=1)
Output: 'bar'::name
-> Result (actual rows=1 loops=1)
Output: 'bar'::name
(10 rows)

Not sure if this is an issue, maybe it's fine. But it's definitely
strange that we only print memory info in verbose mode - IMHO it's much
more useful info than the number of buckets etc.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#9Justin Pryzby
pryzby@telsasoft.com
In reply to: Tomas Vondra (#8)
Re: explain HashAggregate to report bucket and memory stats

On Sat, Feb 22, 2020 at 10:53:35PM +0100, Tomas Vondra wrote:

I've started looking at this patch, because I've been long missing the

Thanks for looking

I have brief, initial comments before I revisit the patch.

3) Almost all executor nodes that are modified to include this new
instrumentation struct also include TupleHashTable, and the data are
essentially about the hash table. So my question is why not to include
this into TupleHashTable - that would mean we don't need to modify any
executor nodes, and it'd probably simplify code in explain.c too because
we could simply pass the hashtable.

I considered this. From 0004 commit message:

| Also, if instrumentation were implemented in simplehash.h, I think every
| insertion or deletion would need to check ->members and ->size (which isn't
| necessary for Agg, but is necessary in the general case, and specifically for
| tidbitmap, since it actually DELETEs hashtable entries). Or else simplehash
| would need a new function like UpdateTupleHashStats, which the higher level nodes
| would need to call after filling the hashtable or before deleting tuples, which
| seems to defeat the purpose of implementing stats at a lower layer.

4) The one exception to (3) is BitmapHeapScanState, which does include
TIDBitmap and not TupleHashTable. And then we have tbm_instrumentation
which "fakes" the data based on the pagetable. Maybe this is a sign that
TIDBitmap needs a slightly different struct?

Hm, I'd say that it "collects" the data that's not immediately present, not
fake it. But maybe I did it poorly. Also, maybe TIDBitmap shouldn't be
included in the patch..

Also, I'm not sure why we
actually need tbm_instrumentation()? It just copies the instrumentation
data from TIDBitmap into the node level, but why couldn't we just look
at the instrumentation data in TIDBitmap directly?

See 0004 commit message:

| TIDBitmap is a private structure, so add an accessor function to return its
| instrumentation, and duplicate instrumentation struct in BitmapHeapState.

Also, I don't know what anyone else thinks, but I think 0005 is a throwaway
commit. It's implemented more nicely in execGrouping.c.

But it's definitely strange that we only print memory info in verbose mode -
IMHO it's much more useful info than the number of buckets etc.

Because I wanted to be able to put "explain analyze" into regression tests
(which can show: "Buckets: 4 (originally 2)"). But cannot get stable output
for any plan which uses Sort, without hacks like explain_sq_limit and
explain_parallel_sort_stats.

Actually, I wish there were a way to control Sort nodes' Memory/Disk output,
too. I'm sure most of regression tests were meant to be run as explain(analyze NO),
but it'd be much better if analyze YES were reasonably easy in the general
case that might include Sort. If someone seconds that, I will start a separate
thread.

--
Justin Pryzby

#10Justin Pryzby
pryzby@telsasoft.com
In reply to: Tomas Vondra (#8)
7 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

On Sat, Feb 22, 2020 at 10:53:35PM +0100, Tomas Vondra wrote:

1) explain.c API

The functions in explain.c (even the static ones) follow the convention
that the last parameter is (ExplainState *es). I think we should stick
to this, so the new parameters should be added before it.

I found it weird to have the "constant" arguments at the end rather than at the
beginning. (Also, these don't follow that convention: show_buffer_usage
ExplainSaveGroup ExplainRestoreGroup ExplainOneQuery ExplainPrintJIT).

But done.

Also, the first show_grouping_sets should be renamed to aggstate to make
it consistent with the type change.

The prototype wasn't updated - fixed.

2) The hash_instrumentation is a bit inconsistent with what we already
have ..HashTableInstrumentation..

Thanks for thinking of a better name.

5) I think the explain for grouping sets need a rething. Teh function
show_grouping_set_keys was originally meant to print just the keys, but
now it's also printing the hash table stats. IMO we need a new function
printing a grouping set info - calling show_grouping_set_keys to print
the keys, but then also printing the extra hashtable info.

I renamed it, and did the rest in a separate patch for now, since I'm only
partially convinced it's an improvement.

6) subplan explain

That is, there's no indication why would this use a hash table, because
the "hashed subplan" is included only in verbose mode:

Need to think about that..

Not sure if this is an issue, maybe it's fine. But it's definitely
strange that we only print memory info in verbose mode - IMHO it's much
more useful info than the number of buckets etc.

You're right that verbose isn't right for this.

I wrote patches creating new explain options to allow stable output of "explain
analyze", by avoiding Memory/Disk. The only other way to handle it seems to be
to avoid "explain analyze" in regression tests, which is what's in common
practice anyway, so did that instead.

I also fixed wrong output and wrong non-text formatting for grouping sets,
tweaked output for subplan, and broke style rules less often.

--
Justin

Attachments:

v5-0003-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From ceaad72654d565404e3dab20e47d622abe7c0de3 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v5 3/7] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c | 70 +++++++++++++++++++++++-------------------
 1 file changed, 38 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index f416f60..5791ee0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						SubPlanState *subplanstate, ExplainState *es);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -721,7 +721,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, NULL, es);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1083,7 +1083,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			SubPlanState *subplanstate, ExplainState *es)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1340,6 +1340,21 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			}
+			if (subplanstate->hashnulls)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1368,6 +1383,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+
+		if (subplanstate && subplanstate->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (subplanstate && subplanstate->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2040,12 +2069,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, NULL, es);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, NULL, es);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2077,7 +2106,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, NULL, es);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3480,7 +3509,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, NULL, es);
 }
 
 /*
@@ -3538,30 +3567,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-		{
-			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashtable->instrument, es);
-			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
-		}
-
-		if (sps->hashnulls)
-		{
-			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, es);
-			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
-		}
+					relationship, sp->plan_name, sps, es);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3578,7 +3584,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, NULL, es);
 }
 
 /*
-- 
2.7.4

v5-0004-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From 0627c3d2db359eab8c55582533188776e101a76e Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v5 4/7] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.

Note, this doesn't affect any regression tests, since hashtable isn't allocated
during "explain".  Note that "explain analyze" would show memory stats, which
we'd have to filter.
---
 src/backend/commands/explain.c            |  5 +++--
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 29 +++++++++++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5791ee0..637480d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1734,8 +1734,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
-			if (es->analyze)
-				show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
+			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
 			break;
 		case T_SampleScan:
 			show_tablesample(((SampleScan *) plan)->tablesample,
@@ -2927,6 +2926,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index ae8a11d..9ae99a3 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -744,6 +746,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index e102589..f01805f 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -43,6 +43,7 @@
 #include "access/htup_details.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 #include "utils/hashutils.h"
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	HashTableInstrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1148,6 +1151,32 @@ tbm_end_iterate(TBMIterator *iterator)
 }
 
 /*
+ * tbm_instrumentation - update stored stats and return pointer to
+ * instrumentation structure
+ *
+ * This updates stats when called.
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+HashTableInstrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable)
+	{
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+
+		/*
+		 * If there are lossy pages, then at one point, we filled maxentries;
+		 * otherwise, number of pages is "->members".
+		 */
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) *
+			(tbm->nchunks>0 ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
+/*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
  * This doesn't free any of the shared state associated with the iterator,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cfeada5..b1e2d1f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1609,6 +1609,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	HashTableInstrumentation	instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fca..de0cdfb 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct HashTableInstrumentation HashTableInstrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern HashTableInstrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.7.4

v5-0001-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From 4edb6652f8e8923e0ae7f044817a30b9024b3f49 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v5 1/7] explain to show tuplehash bucket and memory stats..

Note that hashed SubPlan and recursiveUnion aren't affected in explain output,
probably since hashtables aren't allocated at that point.

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 src/backend/commands/explain.c                    | 137 ++++++++++++++++++--
 src/backend/executor/execGrouping.c               |  27 ++++
 src/backend/executor/nodeAgg.c                    |  11 ++
 src/backend/executor/nodeRecursiveunion.c         |   3 +
 src/backend/executor/nodeSetOp.c                  |   1 +
 src/backend/executor/nodeSubplan.c                |   3 +
 src/include/executor/executor.h                   |   1 +
 src/include/nodes/execnodes.h                     |   9 ++
 src/test/regress/expected/aggregates.out          |  36 ++++--
 src/test/regress/expected/groupingsets.out        |  64 +++++++---
 src/test/regress/expected/join.out                |   3 +-
 src/test/regress/expected/matview.out             |   9 +-
 src/test/regress/expected/partition_aggregate.out | 145 +++++++++++++++++-----
 src/test/regress/expected/partition_join.out      |  13 +-
 src/test/regress/expected/pg_lsn.out              |   3 +-
 src/test/regress/expected/select_distinct.out     |   3 +-
 src/test/regress/expected/select_parallel.out     |  11 +-
 src/test/regress/expected/subselect.out           |   3 +-
 src/test/regress/expected/tablesample.out         |   3 +-
 src/test/regress/expected/union.out               |  15 ++-
 src/test/regress/expected/window.out              |   3 +-
 src/test/regress/expected/write_parallel.out      |  16 ++-
 22 files changed, 429 insertions(+), 90 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d901dc4..ff22181 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -86,12 +87,14 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *aggstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_info(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors,
+								   HashTableInstrumentation *inst,
+								   ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -104,6 +107,7 @@ static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
+static void show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1489,6 +1493,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1886,6 +1891,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, es);
+				break;
+			}
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2262,24 +2281,31 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
+			show_grouping_sets(astate, plan, ancestors, es);
 		else
+		{
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes <= 1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
+	int			setno = 0;
 
 	/* Set up deparsing context */
 	context = set_deparse_context_plan(es->deparse_cxt,
@@ -2289,27 +2315,41 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
+			aggstate->num_hashes ?
+			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			es);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		HashTableInstrumentation *inst = NULL;
+
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+		{
+			Assert(setno < aggstate->num_hashes);
+			inst = &aggstate->perhash[setno++].hashtable->instrument;
+		}
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		show_grouping_set_info(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors,
+							   inst, es);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
+/* Show keys and any hash instrumentation for a grouping set */
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_info(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, HashTableInstrumentation *inst,
+					   ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2373,6 +2413,10 @@ show_grouping_set_keys(PlanState *planstate,
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
 
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, es);
+
 	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
 		es->indent--;
 
@@ -2770,6 +2814,54 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 }
 
 /*
+ * Show hash bucket stats and (optionally) memory.
+ */
+static void
+show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es)
+{
+	long	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld",
+						inst->nbuckets);
+		}
+
+		if (es->analyze)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %ldkB, tuples: %ldkB",
+					spacePeakKb_hash, spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+	}
+}
+
+/*
  * If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
  */
 static void
@@ -3436,6 +3528,29 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashtable->instrument, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (sps->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..52f955a 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,6 +191,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -206,6 +207,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -284,9 +286,34 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial)
+	{
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
+			sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+#define maxself(a,b) a=Max(a,b)
+		/* hashtable->entrysize includes additionalsize */
+		maxself(hashtable->instrument.space_peak_hash,
+				hashtable->hashtab->size * sizeof(TupleHashEntryData));
+		maxself(hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * hashtable->entrysize);
+#undef maxself
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index a99b4a6..94c5bb0 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1707,6 +1707,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
+				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1904,6 +1905,13 @@ agg_retrieve_direct(AggState *aggstate)
 						}
 					}
 				}
+
+				if (aggstate->aggstrategy == AGG_MIXED &&
+						aggstate->current_phase == 1)
+				{
+					for (int i = 0; i < aggstate->num_hashes; i++)
+						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+				}
 			}
 
 			/*
@@ -1978,6 +1986,9 @@ agg_fill_hash_table(AggState *aggstate)
 	}
 
 	aggstate->table_filled = true;
+	for (int i = 0; i < aggstate->num_hashes; i++)
+		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
 	ResetTupleHashIterator(aggstate->perhash[0].hashtable,
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a..93272c2 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a..9c0e0ab 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index ff95317..eec849c 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 81fdfa4..34199b5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cd3ddf7..cfeada5 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,6 +691,14 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+typedef struct HashTableInstrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;	/* peak memory usage in bytes */
+	size_t	space_peak_tuples;	/* peak memory usage in bytes */
+} HashTableInstrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -709,6 +717,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b..b173b32 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -517,10 +517,11 @@ order by 1, 2;
          ->  HashAggregate
                Output: s2.s2, sum((s1.s1 + s2.s2))
                Group Key: s2.s2
+               Buckets: 4
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -556,10 +557,11 @@ select array(select sum(x+y) s
            ->  HashAggregate
                  Output: sum((x.x + y.y)), y.y
                  Group Key: y.y
+                 Buckets: 4
                  ->  Function Scan on pg_catalog.generate_series y
                        Output: y.y
                        Function Call: generate_series(1, 3)
-(13 rows)
+(14 rows)
 
 select array(select sum(x+y) s
             from generate_series(1,3) y group by y order by s)
@@ -872,12 +874,13 @@ explain (costs off)
 ---------------------------------------------------------------------
  HashAggregate
    Group Key: $0
+   Buckets: 2
    InitPlan 1 (returns $0)
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1096,8 +1099,9 @@ explain (costs off) select * from t1 group by a,b,c,d;
 ----------------------
  HashAggregate
    Group Key: a, b
+   Buckets: 256
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 -- No removal can happen if the complete PK is not present in GROUP BY
 explain (costs off) select a,c from t1 group by a,c,d;
@@ -1105,8 +1109,9 @@ explain (costs off) select a,c from t1 group by a,c,d;
 ----------------------
  HashAggregate
    Group Key: a, c, d
+   Buckets: 256
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 -- Test removal across multiple relations
 explain (costs off) select *
@@ -1116,12 +1121,13 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
 ------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
+   Buckets: 128
    ->  Hash Join
          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
          ->  Seq Scan on t2
          ->  Hash
                ->  Seq Scan on t1
-(7 rows)
+(8 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
@@ -1131,12 +1137,13 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
 ------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
+   Buckets: 128
    ->  Hash Join
          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
          ->  Seq Scan on t2
          ->  Hash
                ->  Seq Scan on t1
-(7 rows)
+(8 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
@@ -1144,8 +1151,9 @@ explain (costs off) select * from t3 group by a,b,c;
 ----------------------
  HashAggregate
    Group Key: a, b, c
+   Buckets: 256
    ->  Seq Scan on t3
-(3 rows)
+(4 rows)
 
 create temp table t1c () inherits (t1);
 -- Ensure we don't remove any columns when t1 has a child table
@@ -1154,10 +1162,11 @@ explain (costs off) select * from t1 group by a,b,c,d;
 -------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t1.c, t1.d
+   Buckets: 256
    ->  Append
          ->  Seq Scan on t1 t1_1
          ->  Seq Scan on t1c t1_2
-(5 rows)
+(6 rows)
 
 -- Okay to remove columns if we're only querying the parent.
 explain (costs off) select * from only t1 group by a,b,c,d;
@@ -1165,8 +1174,9 @@ explain (costs off) select * from only t1 group by a,b,c,d;
 ----------------------
  HashAggregate
    Group Key: a, b
+   Buckets: 2
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 create temp table p_t1 (
   a int,
@@ -1183,10 +1193,11 @@ explain (costs off) select * from p_t1 group by a,b,c,d;
 --------------------------------
  HashAggregate
    Group Key: p_t1.a, p_t1.b
+   Buckets: 512
    ->  Append
          ->  Seq Scan on p_t1_1
          ->  Seq Scan on p_t1_2
-(5 rows)
+(6 rows)
 
 drop table t1 cascade;
 NOTICE:  drop cascades to table t1c
@@ -2354,6 +2365,7 @@ explain (costs off)
    ->  Hash
          ->  HashAggregate
                Group Key: onek.twothousand, onek.twothousand
+               Buckets: 256
                ->  Seq Scan on onek
-(8 rows)
+(9 rows)
 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c1f802c..be38673 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -974,9 +974,11 @@ explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
    ->  HashAggregate
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          ->  Values Scan on "*VALUES*"
-(6 rows)
+(8 rows)
 
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
@@ -1008,11 +1010,14 @@ explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
    ->  MixedAggregate
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          Group Key: ()
          ->  Values Scan on "*VALUES*"
-(8 rows)
+(11 rows)
 
 -- shouldn't try and hash
 explain (costs off)
@@ -1071,11 +1076,12 @@ explain (costs off)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
    ->  MixedAggregate
          Hash Key: unsortable_col
+         Buckets: 256
          Group Key: unhashable_col
          ->  Sort
                Sort Key: unhashable_col
                ->  Seq Scan on gstest4
-(8 rows)
+(9 rows)
 
 select unhashable_col, unsortable_col,
        grouping(unhashable_col, unsortable_col),
@@ -1114,11 +1120,12 @@ explain (costs off)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
    ->  MixedAggregate
          Hash Key: v, unsortable_col
+         Buckets: 256
          Group Key: v, unhashable_col
          ->  Sort
                Sort Key: v, unhashable_col
                ->  Seq Scan on gstest4
-(8 rows)
+(9 rows)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
@@ -1132,9 +1139,11 @@ explain (costs off)
 --------------------------------
  HashAggregate
    Hash Key: a, b
+   Buckets: 256
    Hash Key: a
+   Buckets: 256
    ->  Seq Scan on gstest_empty
-(4 rows)
+(6 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
  a | b | sum | count 
@@ -1156,11 +1165,12 @@ explain (costs off)
 --------------------------------
  MixedAggregate
    Hash Key: a, b
+   Buckets: 256
    Group Key: ()
    Group Key: ()
    Group Key: ()
    ->  Seq Scan on gstest_empty
-(6 rows)
+(7 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
  sum | count 
@@ -1201,9 +1211,11 @@ explain (costs off)
 ---------------------------
  HashAggregate
    Hash Key: a, b
+   Buckets: 2
    Hash Key: a, c
+   Buckets: 2
    ->  Seq Scan on gstest3
-(4 rows)
+(6 rows)
 
 -- simple rescan tests
 select a, b, sum(v.x)
@@ -1230,11 +1242,13 @@ explain (costs off)
    Sort Key: (sum("*VALUES*".column1)), gstest_data.a, gstest_data.b
    ->  HashAggregate
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(8 rows)
+(10 rows)
 
 select *
   from (values (1),(2)) v(x),
@@ -1286,10 +1300,13 @@ explain (costs off)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), (max("*VALUES*".column3))
    ->  HashAggregate
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 1), ("*VALUES*".column2 + 1)
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 2), ("*VALUES*".column2 + 2)
+         Buckets: 16
          ->  Values Scan on "*VALUES*"
-(7 rows)
+(10 rows)
 
 select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
   from gstest2 group by cube (a,b) order by rsum, a, b;
@@ -1317,11 +1334,14 @@ explain (costs off)
                Sort Key: a, b
                ->  MixedAggregate
                      Hash Key: a, b
+                     Buckets: 256
                      Hash Key: a
+                     Buckets: 256
                      Hash Key: b
+                     Buckets: 256
                      Group Key: ()
                      ->  Seq Scan on gstest2
-(11 rows)
+(14 rows)
 
 select a, b, sum(v.x)
   from (values (1),(2)) v(x), gstest_data(v.x)
@@ -1352,13 +1372,16 @@ explain (costs off)
    Sort Key: gstest_data.a, gstest_data.b
    ->  MixedAggregate
          Hash Key: gstest_data.a, gstest_data.b
+         Buckets: 256
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          Group Key: ()
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(10 rows)
+(13 rows)
 
 -- Verify that we correctly handle the child node returning a
 -- non-minimal slot, which happens if the input is pre-sorted,
@@ -1553,9 +1576,13 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
@@ -1564,7 +1591,7 @@ explain (costs off)
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(13 rows)
+(17 rows)
 
 explain (costs off)
   select unique1,
@@ -1576,14 +1603,18 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Group Key: unique1
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(9 rows)
+(13 rows)
 
 set work_mem = '384kB';
 explain (costs off)
@@ -1596,17 +1627,22 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Hash Key: thousand
+   Buckets: 2048
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(12 rows)
+(17 rows)
 
 -- check collation-sensitive matching between grouping expressions
 -- (similar to a check for aggregates, but there are additional code
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 761376b..9f07501 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -6199,6 +6199,7 @@ where exists (select 1 from tenk1 t3
          ->  HashAggregate
                Output: t3.thousand, t3.tenthous
                Group Key: t3.thousand, t3.tenthous
+               Buckets: 16384
                ->  Index Only Scan using tenk1_thous_tenthous on public.tenk1 t3
                      Output: t3.thousand, t3.tenthous
          ->  Hash
@@ -6209,7 +6210,7 @@ where exists (select 1 from tenk1 t3
    ->  Index Only Scan using tenk1_hundred on public.tenk1 t2
          Output: t2.hundred
          Index Cond: (t2.hundred = t3.tenthous)
-(18 rows)
+(19 rows)
 
 -- ... unless it actually is unique
 create table j3 as select unique1, tenthous from onek;
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index d0121a7..ca8573a 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -23,8 +23,9 @@ EXPLAIN (costs off)
 ----------------------------
  HashAggregate
    Group Key: type
+   Buckets: 256
    ->  Seq Scan on mvtest_t
-(3 rows)
+(4 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tm AS SELECT type, sum(amt) AS totamt FROM mvtest_t GROUP BY type WITH NO DATA;
 SELECT relispopulated FROM pg_class WHERE oid = 'mvtest_tm'::regclass;
@@ -61,8 +62,9 @@ EXPLAIN (costs off)
    Sort Key: mvtest_t.type
    ->  HashAggregate
          Group Key: mvtest_t.type
+         Buckets: 256
          ->  Seq Scan on mvtest_t
-(5 rows)
+(6 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tvm AS SELECT * FROM mvtest_tv ORDER BY type;
 SELECT * FROM mvtest_tvm;
@@ -85,8 +87,9 @@ EXPLAIN (costs off)
  Aggregate
    ->  HashAggregate
          Group Key: mvtest_t.type
+         Buckets: 256
          ->  Seq Scan on mvtest_t
-(4 rows)
+(5 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tvvm AS SELECT * FROM mvtest_tvv;
 CREATE VIEW mvtest_tvvmv AS SELECT * FROM mvtest_tvvm;
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index fbc8d3a..5939c2a 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -27,17 +27,20 @@ SELECT c, sum(a), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY c HAVI
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab.c
+               Buckets: 4
                Filter: (avg(pagg_tab.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p1 pagg_tab
          ->  HashAggregate
                Group Key: pagg_tab_1.c
+               Buckets: 4
                Filter: (avg(pagg_tab_1.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p2 pagg_tab_1
          ->  HashAggregate
                Group Key: pagg_tab_2.c
+               Buckets: 4
                Filter: (avg(pagg_tab_2.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(15 rows)
+(18 rows)
 
 SELECT c, sum(a), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY c HAVING avg(d) < 15 ORDER BY 1, 2, 3;
   c   | sum  |         avg         | count | min | max 
@@ -59,18 +62,22 @@ SELECT a, sum(b), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY a HAVI
    Sort Key: pagg_tab.a, (sum(pagg_tab.b)), (avg(pagg_tab.b))
    ->  Finalize HashAggregate
          Group Key: pagg_tab.a
+         Buckets: 32
          Filter: (avg(pagg_tab.d) < '15'::numeric)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p1 pagg_tab
                ->  Partial HashAggregate
                      Group Key: pagg_tab_1.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p2 pagg_tab_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_2.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(15 rows)
+(19 rows)
 
 SELECT a, sum(b), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY a HAVING avg(d) < 15 ORDER BY 1, 2, 3;
  a  | sum  |         avg         | count | min | max 
@@ -95,14 +102,17 @@ SELECT a, c, count(*) FROM pagg_tab GROUP BY a, c;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.a, pagg_tab.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.a, pagg_tab_1.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.a, pagg_tab_2.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Check with multiple columns in GROUP BY, order in GROUP BY is reversed
 EXPLAIN (COSTS OFF)
@@ -112,14 +122,17 @@ SELECT a, c, count(*) FROM pagg_tab GROUP BY c, a;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.c, pagg_tab.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.c, pagg_tab_1.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.c, pagg_tab_2.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Check with multiple columns in GROUP BY, order in target-list is reversed
 EXPLAIN (COSTS OFF)
@@ -129,14 +142,17 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.a, pagg_tab.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.a, pagg_tab_1.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.a, pagg_tab_2.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
@@ -145,9 +161,10 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 --------------------------------
  HashAggregate
    Group Key: c
+   Buckets: 2
    ->  Result
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -341,12 +358,13 @@ SELECT c, sum(a) FROM pagg_tab GROUP BY rollup(c) ORDER BY 1, 2;
    Sort Key: pagg_tab.c, (sum(pagg_tab.a))
    ->  MixedAggregate
          Hash Key: pagg_tab.c
+         Buckets: 16
          Group Key: ()
          ->  Append
                ->  Seq Scan on pagg_tab_p1 pagg_tab_1
                ->  Seq Scan on pagg_tab_p2 pagg_tab_2
                ->  Seq Scan on pagg_tab_p3 pagg_tab_3
-(9 rows)
+(10 rows)
 
 -- ORDERED SET within the aggregate.
 -- Full aggregation; since all the rows that belong to the same group come
@@ -418,6 +436,7 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
    ->  Append
          ->  HashAggregate
                Group Key: t1.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t1.x = t2.y)
                      ->  Seq Scan on pagg_tab1_p1 t1
@@ -425,6 +444,7 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p1 t2
          ->  HashAggregate
                Group Key: t1_1.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t1_1.x = t2_1.y)
                      ->  Seq Scan on pagg_tab1_p2 t1_1
@@ -432,12 +452,13 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p2 t2_1
          ->  HashAggregate
                Group Key: t1_2.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t2_2.y = t1_2.x)
                      ->  Seq Scan on pagg_tab2_p3 t2_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 t1_2
-(24 rows)
+(27 rows)
 
 SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2.y GROUP BY t1.x ORDER BY 1, 2, 3;
  x  | sum  | count 
@@ -458,6 +479,7 @@ SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t
    Sort Key: t1.x, (sum(t1.y)), (count(((t1.*)::pagg_tab1)))
    ->  HashAggregate
          Group Key: t1.x
+         Buckets: 16
          ->  Hash Join
                Hash Cond: (t1.x = t2.y)
                ->  Append
@@ -469,7 +491,7 @@ SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t
                            ->  Seq Scan on pagg_tab2_p1 t2_1
                            ->  Seq Scan on pagg_tab2_p2 t2_2
                            ->  Seq Scan on pagg_tab2_p3 t2_3
-(15 rows)
+(16 rows)
 
 SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2.y GROUP BY t1.x ORDER BY 1, 2, 3;
  x  | sum  | count 
@@ -491,6 +513,7 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
    ->  Append
          ->  HashAggregate
                Group Key: t2.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t1.x = t2.y)
                      ->  Seq Scan on pagg_tab1_p1 t1
@@ -498,6 +521,7 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p1 t2
          ->  HashAggregate
                Group Key: t2_1.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t1_1.x = t2_1.y)
                      ->  Seq Scan on pagg_tab1_p2 t1_1
@@ -505,12 +529,13 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p2 t2_1
          ->  HashAggregate
                Group Key: t2_2.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t2_2.y = t1_2.x)
                      ->  Seq Scan on pagg_tab2_p3 t2_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 t1_2
-(24 rows)
+(27 rows)
 
 -- When GROUP BY clause does not match; partial aggregation is performed for each partition.
 -- Also test GroupAggregate paths by disabling hash aggregates.
@@ -582,6 +607,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
          ->  Append
                ->  Partial HashAggregate
                      Group Key: b.y
+                     Buckets: 4
                      ->  Hash Left Join
                            Hash Cond: (a.x = b.y)
                            ->  Seq Scan on pagg_tab1_p1 a
@@ -589,6 +615,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
                                  ->  Seq Scan on pagg_tab2_p1 b
                ->  Partial HashAggregate
                      Group Key: b_1.y
+                     Buckets: 4
                      ->  Hash Left Join
                            Hash Cond: (a_1.x = b_1.y)
                            ->  Seq Scan on pagg_tab1_p2 a_1
@@ -596,12 +623,13 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
                                  ->  Seq Scan on pagg_tab2_p2 b_1
                ->  Partial HashAggregate
                      Group Key: b_2.y
+                     Buckets: 4
                      ->  Hash Right Join
                            Hash Cond: (b_2.y = a_2.x)
                            ->  Seq Scan on pagg_tab2_p3 b_2
                            ->  Hash
                                  ->  Seq Scan on pagg_tab1_p3 a_2
-(26 rows)
+(29 rows)
 
 SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY b.y ORDER BY 1 NULLS LAST;
  y  | sum  
@@ -625,6 +653,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
    ->  Append
          ->  HashAggregate
                Group Key: b.y
+               Buckets: 4
                ->  Hash Right Join
                      Hash Cond: (a.x = b.y)
                      ->  Seq Scan on pagg_tab1_p1 a
@@ -632,6 +661,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
                            ->  Seq Scan on pagg_tab2_p1 b
          ->  HashAggregate
                Group Key: b_1.y
+               Buckets: 4
                ->  Hash Right Join
                      Hash Cond: (a_1.x = b_1.y)
                      ->  Seq Scan on pagg_tab1_p2 a_1
@@ -639,12 +669,13 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
                            ->  Seq Scan on pagg_tab2_p2 b_1
          ->  HashAggregate
                Group Key: b_2.y
+               Buckets: 4
                ->  Hash Left Join
                      Hash Cond: (b_2.y = a_2.x)
                      ->  Seq Scan on pagg_tab2_p3 b_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 a_2
-(24 rows)
+(27 rows)
 
 SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP BY b.y ORDER BY 1 NULLS LAST;
  y  | sum  
@@ -674,6 +705,7 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
          ->  Append
                ->  Partial HashAggregate
                      Group Key: a.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (a.x = b.y)
                            ->  Seq Scan on pagg_tab1_p1 a
@@ -681,6 +713,7 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
                                  ->  Seq Scan on pagg_tab2_p1 b
                ->  Partial HashAggregate
                      Group Key: a_1.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (a_1.x = b_1.y)
                            ->  Seq Scan on pagg_tab1_p2 a_1
@@ -688,12 +721,13 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
                                  ->  Seq Scan on pagg_tab2_p2 b_1
                ->  Partial HashAggregate
                      Group Key: a_2.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (b_2.y = a_2.x)
                            ->  Seq Scan on pagg_tab2_p3 b_2
                            ->  Hash
                                  ->  Seq Scan on pagg_tab1_p3 a_2
-(26 rows)
+(29 rows)
 
 SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x ORDER BY 1 NULLS LAST;
  x  | sum  
@@ -728,6 +762,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOI
    Sort Key: pagg_tab1.x, pagg_tab2.y
    ->  HashAggregate
          Group Key: pagg_tab1.x, pagg_tab2.y
+         Buckets: 256
          ->  Hash Left Join
                Hash Cond: (pagg_tab1.x = pagg_tab2.y)
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
@@ -742,7 +777,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOI
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
                                  Filter: (y > 10)
-(18 rows)
+(19 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20  GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
@@ -768,6 +803,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
    Sort Key: pagg_tab1.x, pagg_tab2.y
    ->  HashAggregate
          Group Key: pagg_tab1.x, pagg_tab2.y
+         Buckets: 256
          ->  Hash Full Join
                Hash Cond: (pagg_tab1.x = pagg_tab2.y)
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
@@ -782,7 +818,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
                                  Filter: (y > 10)
-(18 rows)
+(19 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20 GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
@@ -831,18 +867,22 @@ SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a HAVING avg(c) < 22
    Sort Key: pagg_tab_m.a, (sum(pagg_tab_m.b)), (avg(pagg_tab_m.c))
    ->  Finalize HashAggregate
          Group Key: pagg_tab_m.a
+         Buckets: 64
          Filter: (avg(pagg_tab_m.c) < '22'::numeric)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m_1.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m_2.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(19 rows)
 
 SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a HAVING avg(c) < 22 ORDER BY 1, 2, 3;
  a  | sum  |         avg         | count 
@@ -865,17 +905,20 @@ SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a, (a+b)/2 HAVING su
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_m.a, ((pagg_tab_m.a + pagg_tab_m.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m.b) < 50)
                ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
          ->  HashAggregate
                Group Key: pagg_tab_m_1.a, ((pagg_tab_m_1.a + pagg_tab_m_1.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m_1.b) < 50)
                ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
          ->  HashAggregate
                Group Key: pagg_tab_m_2.a, ((pagg_tab_m_2.a + pagg_tab_m_2.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m_2.b) < 50)
                ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(18 rows)
 
 SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a, (a+b)/2 HAVING sum(b) < 50 ORDER BY 1, 2, 3;
  a  | sum |         avg         | count 
@@ -898,17 +941,20 @@ SELECT a, c, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY (a+b)/2, 2, 1 HAV
    ->  Append
          ->  HashAggregate
                Group Key: ((pagg_tab_m.a + pagg_tab_m.b) / 2), pagg_tab_m.c, pagg_tab_m.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m.b) = 50) AND (avg(pagg_tab_m.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
          ->  HashAggregate
                Group Key: ((pagg_tab_m_1.a + pagg_tab_m_1.b) / 2), pagg_tab_m_1.c, pagg_tab_m_1.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m_1.b) = 50) AND (avg(pagg_tab_m_1.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
          ->  HashAggregate
                Group Key: ((pagg_tab_m_2.a + pagg_tab_m_2.b) / 2), pagg_tab_m_2.c, pagg_tab_m_2.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m_2.b) = 50) AND (avg(pagg_tab_m_2.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(18 rows)
 
 SELECT a, c, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY (a+b)/2, 2, 1 HAVING sum(b) = 50 AND avg(c) > 25 ORDER BY 1, 2, 3;
  a  | c  | sum |         avg         | count 
@@ -1032,6 +1078,7 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_ml.a
+               Buckets: 16
                Filter: (avg(pagg_tab_ml.b) < '3'::numeric)
                ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  Finalize GroupAggregate
@@ -1042,9 +1089,11 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                      ->  Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_2.a
+                                 Buckets: 16
                                  ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_2
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_3.a
+                                 Buckets: 16
                                  ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_3
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_5.a
@@ -1054,11 +1103,13 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                      ->  Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_5.a
+                                 Buckets: 8
                                  ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_5
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_6.a
+                                 Buckets: 8
                                  ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_6
-(31 rows)
+(36 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1089,20 +1140,25 @@ SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b ORDER BY 1, 2, 3;
                ->  Append
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_1.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_2.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_3.b
+                           Buckets: 8
                            ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_4.b
+                           Buckets: 8
                            ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(22 rows)
+(27 rows)
 
 SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b HAVING avg(a) < 15 ORDER BY 1, 2, 3;
  b |  sum  | count 
@@ -1124,25 +1180,30 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 O
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_ml.a, pagg_tab_ml.b, pagg_tab_ml.c
+               Buckets: 512
                Filter: (avg(pagg_tab_ml.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  HashAggregate
                Group Key: pagg_tab_ml_1.a, pagg_tab_ml_1.b, pagg_tab_ml_1.c
+               Buckets: 256
                Filter: (avg(pagg_tab_ml_1.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
          ->  HashAggregate
                Group Key: pagg_tab_ml_2.a, pagg_tab_ml_2.b, pagg_tab_ml_2.c
+               Buckets: 256
                Filter: (avg(pagg_tab_ml_2.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
          ->  HashAggregate
                Group Key: pagg_tab_ml_3.a, pagg_tab_ml_3.b, pagg_tab_ml_3.c
+               Buckets: 128
                Filter: (avg(pagg_tab_ml_3.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
          ->  HashAggregate
                Group Key: pagg_tab_ml_4.a, pagg_tab_ml_4.b, pagg_tab_ml_4.c
+               Buckets: 128
                Filter: (avg(pagg_tab_ml_4.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(23 rows)
+(28 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1183,6 +1244,7 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            Sort Key: pagg_tab_ml.a
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml.a
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_2.a
@@ -1194,9 +1256,11 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            ->  Parallel Append
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_2.a
+                                       Buckets: 16
                                        ->  Parallel Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_2
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_3.a
+                                       Buckets: 16
                                        ->  Parallel Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_3
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_5.a
@@ -1208,11 +1272,13 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            ->  Parallel Append
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_5.a
+                                       Buckets: 8
                                        ->  Parallel Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_5
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_6.a
+                                       Buckets: 8
                                        ->  Parallel Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_6
-(41 rows)
+(46 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1245,20 +1311,25 @@ SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b ORDER BY 1, 2, 3;
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_1.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_2.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_3.b
+                                 Buckets: 8
                                  ->  Parallel Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_4.b
+                                 Buckets: 8
                                  ->  Parallel Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(24 rows)
+(29 rows)
 
 SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b HAVING avg(a) < 15 ORDER BY 1, 2, 3;
  b |  sum  | count 
@@ -1282,25 +1353,30 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 O
          ->  Parallel Append
                ->  HashAggregate
                      Group Key: pagg_tab_ml.a, pagg_tab_ml.b, pagg_tab_ml.c
+                     Buckets: 512
                      Filter: (avg(pagg_tab_ml.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                ->  HashAggregate
                      Group Key: pagg_tab_ml_1.a, pagg_tab_ml_1.b, pagg_tab_ml_1.c
+                     Buckets: 256
                      Filter: (avg(pagg_tab_ml_1.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                ->  HashAggregate
                      Group Key: pagg_tab_ml_2.a, pagg_tab_ml_2.b, pagg_tab_ml_2.c
+                     Buckets: 256
                      Filter: (avg(pagg_tab_ml_2.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                ->  HashAggregate
                      Group Key: pagg_tab_ml_3.a, pagg_tab_ml_3.b, pagg_tab_ml_3.c
+                     Buckets: 128
                      Filter: (avg(pagg_tab_ml_3.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                ->  HashAggregate
                      Group Key: pagg_tab_ml_4.a, pagg_tab_ml_4.b, pagg_tab_ml_4.c
+                     Buckets: 128
                      Filter: (avg(pagg_tab_ml_4.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(25 rows)
+(30 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1351,14 +1427,17 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_1.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_2.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+(22 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1388,14 +1467,17 @@ SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) <
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_1.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_2.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+(22 rows)
 
 SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) < 12 ORDER BY 1, 2, 3;
  y  |  sum  |         avg         | count 
@@ -1425,11 +1507,12 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      Sort Key: pagg_tab_para.x
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_para.x
+                           Buckets: 64
                            ->  Parallel Append
                                  ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para_1
                                  ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_3
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_2
-(15 rows)
+(16 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1459,11 +1542,12 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      Sort Key: pagg_tab_para.x
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_para.x
+                           Buckets: 64
                            ->  Parallel Append
                                  ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para_1
                                  ->  Seq Scan on pagg_tab_para_p2 pagg_tab_para_2
                                  ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_3
-(15 rows)
+(16 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1488,17 +1572,20 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_para.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para
          ->  HashAggregate
                Group Key: pagg_tab_para_1.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para_1.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
          ->  HashAggregate
                Group Key: pagg_tab_para_2.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para_2.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(15 rows)
+(18 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index b3fbe47..b830930 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -819,6 +819,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_2.a = t1_5.b)
                ->  HashAggregate
                      Group Key: t1_5.b
+                     Buckets: 4
                      ->  Hash Join
                            Hash Cond: (((t2_1.a + t2_1.b) / 2) = t1_5.b)
                            ->  Seq Scan on prt1_e_p1 t2_1
@@ -832,6 +833,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_3.a = t1_6.b)
                ->  HashAggregate
                      Group Key: t1_6.b
+                     Buckets: 4
                      ->  Hash Join
                            Hash Cond: (((t2_2.a + t2_2.b) / 2) = t1_6.b)
                            ->  Seq Scan on prt1_e_p2 t2_2
@@ -845,6 +847,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_4.a = t1_7.b)
                ->  HashAggregate
                      Group Key: t1_7.b
+                     Buckets: 2
                      ->  Nested Loop
                            ->  Seq Scan on prt2_p3 t1_7
                                  Filter: (a = 0)
@@ -853,7 +856,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                ->  Index Scan using iprt1_p3_a on prt1_p3 t1_4
                      Index Cond: (a = ((t2_3.a + t2_3.b) / 2))
                      Filter: (b = 0)
-(41 rows)
+(44 rows)
 
 SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHERE t1.a = 0 AND t1.b = (t2.a + t2.b)/2) AND t1.b = 0 ORDER BY t1.a;
   a  | b |  c   
@@ -874,6 +877,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_6.b
+                     Buckets: 4
                      ->  Hash Semi Join
                            Hash Cond: (t1_6.b = ((t1_9.a + t1_9.b) / 2))
                            ->  Seq Scan on prt2_p1 t1_6
@@ -886,6 +890,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_7.b
+                     Buckets: 4
                      ->  Hash Semi Join
                            Hash Cond: (t1_7.b = ((t1_10.a + t1_10.b) / 2))
                            ->  Seq Scan on prt2_p2 t1_7
@@ -898,6 +903,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_8.b
+                     Buckets: 2
                      ->  Hash Semi Join
                            Hash Cond: (t1_8.b = ((t1_11.a + t1_11.b) / 2))
                            ->  Seq Scan on prt2_p3 t1_8
@@ -907,7 +913,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
                ->  Index Scan using iprt1_p3_a on prt1_p3 t1_5
                      Index Cond: (a = t1_8.b)
                      Filter: (b = 0)
-(39 rows)
+(42 rows)
 
 SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a;
   a  | b |  c   
@@ -1466,6 +1472,7 @@ SELECT avg(t1.a), avg(t2.b), t1.c, t2.c FROM plt1 t1 RIGHT JOIN plt2 t2 ON t1.c
    Sort Key: t1.c
    ->  HashAggregate
          Group Key: t1.c, t2.c
+         Buckets: 4
          ->  Append
                ->  Hash Join
                      Hash Cond: (t2_1.c = t1_1.c)
@@ -1485,7 +1492,7 @@ SELECT avg(t1.a), avg(t2.b), t1.c, t2.c FROM plt1 t1 RIGHT JOIN plt2 t2 ON t1.c
                      ->  Hash
                            ->  Seq Scan on plt1_p3 t1_3
                                  Filter: ((a % 25) = 0)
-(23 rows)
+(24 rows)
 
 --
 -- multiple levels of partitioning
diff --git a/src/test/regress/expected/pg_lsn.out b/src/test/regress/expected/pg_lsn.out
index 64d41df..c6a5625 100644
--- a/src/test/regress/expected/pg_lsn.out
+++ b/src/test/regress/expected/pg_lsn.out
@@ -85,6 +85,7 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
    Sort Key: (((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn)
    ->  HashAggregate
          Group Key: ((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn
+         Buckets: 4
          ->  Nested Loop
                ->  Function Scan on generate_series k
                ->  Materialize
@@ -93,7 +94,7 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
                                  Filter: ((j > 0) AND (j <= 10))
                            ->  Function Scan on generate_series i
                                  Filter: (i <= 10)
-(12 rows)
+(13 rows)
 
 SELECT DISTINCT (i || '/' || j)::pg_lsn f
   FROM generate_series(1, 10) i,
diff --git a/src/test/regress/expected/select_distinct.out b/src/test/regress/expected/select_distinct.out
index f3696c6..8e2903a 100644
--- a/src/test/regress/expected/select_distinct.out
+++ b/src/test/regress/expected/select_distinct.out
@@ -137,9 +137,10 @@ SELECT count(*) FROM
    ->  HashAggregate
          Output: tenk1.two, tenk1.four, tenk1.two
          Group Key: tenk1.two, tenk1.four, tenk1.two
+         Buckets: 8
          ->  Seq Scan on public.tenk1
                Output: tenk1.two, tenk1.four, tenk1.two
-(7 rows)
+(8 rows)
 
 SELECT count(*) FROM
   (SELECT DISTINCT two, four, two FROM tenk1) ss;
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 96dfb7c..2b8a253 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -226,12 +226,14 @@ explain (costs off)
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 select length(stringu1) from tenk1 group by length(stringu1);
  length 
@@ -247,12 +249,14 @@ explain (costs off)
    Sort Key: stringu1
    ->  Finalize HashAggregate
          Group Key: stringu1
+         Buckets: 1024
          ->  Gather
                Workers Planned: 4
                ->  Partial HashAggregate
                      Group Key: stringu1
+                     Buckets: 1024
                      ->  Parallel Seq Scan on tenk1
-(9 rows)
+(11 rows)
 
 -- test that parallel plan for aggregates is not selected when
 -- target list contains parallel restricted clause.
@@ -263,10 +267,11 @@ explain (costs off)
 -------------------------------------------------------------------
  HashAggregate
    Group Key: sp_parallel_restricted(unique1)
+   Buckets: 16384
    ->  Gather
          Workers Planned: 4
          ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
-(5 rows)
+(6 rows)
 
 -- test prepared statement
 prepare tenk1_count(integer) As select  count((unique1)) from tenk1 where hundred > $1;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 71a677b..ec2e599 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -1007,9 +1007,10 @@ select * from int4_tbl o where (f1, f1) in
                            ->  HashAggregate
                                  Output: i.f1
                                  Group Key: i.f1
+                                 Buckets: 8
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,50) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/tablesample.out b/src/test/regress/expected/tablesample.out
index 078358d..fc41a81 100644
--- a/src/test/regress/expected/tablesample.out
+++ b/src/test/regress/expected/tablesample.out
@@ -255,11 +255,12 @@ select pct, count(unique1) from
 --------------------------------------------------------
  HashAggregate
    Group Key: "*VALUES*".column1
+   Buckets: 2
    ->  Nested Loop
          ->  Values Scan on "*VALUES*"
          ->  Sample Scan on tenk1
                Sampling: bernoulli ("*VALUES*".column1)
-(6 rows)
+(7 rows)
 
 select pct, count(unique1) from
   (values (0),(100)) v(pct),
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92..2153974 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -355,12 +355,13 @@ select count(*) from
  Aggregate
    ->  Subquery Scan on ss
          ->  HashSetOp Intersect
+               Buckets: 8192
                ->  Append
                      ->  Subquery Scan on "*SELECT* 2"
                            ->  Seq Scan on tenk1
                      ->  Subquery Scan on "*SELECT* 1"
                            ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(8 rows)
+(9 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -374,13 +375,14 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  HashSetOp Except
+   Buckets: 16384
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Index Only Scan using tenk1_unique1 on tenk1
          ->  Subquery Scan on "*SELECT* 2"
                ->  Index Only Scan using tenk1_unique2 on tenk1 tenk1_1
                      Filter: (unique2 <> 10)
-(7 rows)
+(8 rows)
 
 select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
  unique1 
@@ -585,12 +587,13 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
                               QUERY PLAN                              
 ----------------------------------------------------------------------
  HashSetOp Intersect
+   Buckets: 2
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Function Scan on generate_series
          ->  Subquery Scan on "*SELECT* 2"
                ->  Function Scan on generate_series generate_series_1
-(6 rows)
+(7 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
@@ -726,12 +729,13 @@ explain (costs off)
 ---------------------------------------------------
  HashAggregate
    Group Key: ((t1.a || t1.b))
+   Buckets: 8
    ->  Append
          ->  Index Scan using t1_ab_idx on t1
                Index Cond: ((a || b) = 'ab'::text)
          ->  Index Only Scan using t2_pkey on t2
                Index Cond: (ab = 'ab'::text)
-(7 rows)
+(8 rows)
 
 --
 -- Test that ORDER BY for UNION ALL can be pushed down to inheritance
@@ -864,11 +868,12 @@ ORDER BY x;
          Filter: (ss.x < 4)
          ->  HashAggregate
                Group Key: (1), (generate_series(1, 10))
+               Buckets: 16
                ->  Append
                      ->  ProjectSet
                            ->  Result
                      ->  Result
-(10 rows)
+(11 rows)
 
 SELECT * FROM
   (SELECT 1 AS t, generate_series(1,10) AS x
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index d5fd404..ea34daf 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -624,8 +624,9 @@ select first_value(max(x)) over (), y
  WindowAgg
    ->  HashAggregate
          Group Key: (tenk1.ten + tenk1.four)
+         Buckets: 64
          ->  Seq Scan on tenk1
-(4 rows)
+(5 rows)
 
 -- test non-default frame specifications
 SELECT four, ten,
diff --git a/src/test/regress/expected/write_parallel.out b/src/test/regress/expected/write_parallel.out
index 0c4da25..25193a8 100644
--- a/src/test/regress/expected/write_parallel.out
+++ b/src/test/regress/expected/write_parallel.out
@@ -19,12 +19,14 @@ explain (costs off) create table parallel_write as
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create table parallel_write as
     select length(stringu1) from tenk1 group by length(stringu1);
@@ -35,12 +37,14 @@ explain (costs off) select length(stringu1) into parallel_write
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 select length(stringu1) into parallel_write
     from tenk1 group by length(stringu1);
@@ -51,12 +55,14 @@ explain (costs off) create materialized view parallel_mat_view as
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create materialized view parallel_mat_view as
     select length(stringu1) from tenk1 group by length(stringu1);
@@ -67,12 +73,14 @@ explain (costs off) create table parallel_write as execute prep_stmt;
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create table parallel_write as execute prep_stmt;
 drop table parallel_write;
-- 
2.7.4

v5-0002-refactor-show_grouping_set_keys.patchtext/x-diff; charset=us-asciiDownload
From c1335ce0899e4b085fde58592db5258e2fb2c71b Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 23 Feb 2020 23:13:07 -0600
Subject: [PATCH v5 2/7] refactor show_grouping_set_keys

---
 src/backend/commands/explain.c | 55 +++++++++++++++++++++++++-----------------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ff22181..f416f60 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -95,6 +95,8 @@ static void show_grouping_set_info(AggState *aggstate,
 								   List *ancestors,
 								   HashTableInstrumentation *inst,
 								   ExplainState *es);
+static void show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List
+		*context, bool useprefix, ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -2350,6 +2352,37 @@ show_grouping_set_info(AggState *aggstate,
 					   ExplainState *es)
 {
 	PlanState	*planstate = outerPlanState(aggstate);
+
+	ExplainOpenGroup("Grouping Set", NULL, true, es);
+
+	if (sortnode)
+	{
+		show_sort_group_keys(planstate, "Sort Key",
+							 sortnode->numCols, sortnode->sortColIdx,
+							 sortnode->sortOperators, sortnode->collations,
+							 sortnode->nullsFirst,
+							 ancestors, es);
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			es->indent++;
+	}
+
+	show_grouping_set_keys(aggstate, aggnode, context, useprefix, es);
+
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, es);
+
+	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
+		es->indent--;
+
+	ExplainCloseGroup("Grouping Set", NULL, true, es);
+}
+
+/* Show keys of a grouping set */
+static void
+show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List *context, bool useprefix, ExplainState *es)
+{
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2369,19 +2402,6 @@ show_grouping_set_info(AggState *aggstate,
 		keysetname = "Group Keys";
 	}
 
-	ExplainOpenGroup("Grouping Set", NULL, true, es);
-
-	if (sortnode)
-	{
-		show_sort_group_keys(planstate, "Sort Key",
-							 sortnode->numCols, sortnode->sortColIdx,
-							 sortnode->sortOperators, sortnode->collations,
-							 sortnode->nullsFirst,
-							 ancestors, es);
-		if (es->format == EXPLAIN_FORMAT_TEXT)
-			es->indent++;
-	}
-
 	ExplainOpenGroup(keysetname, keysetname, false, es);
 
 	foreach(lc, gsets)
@@ -2412,15 +2432,6 @@ show_grouping_set_info(AggState *aggstate,
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
-
-	if (aggnode->aggstrategy == AGG_HASHED ||
-			aggnode->aggstrategy == AGG_MIXED)
-		show_tuplehash_info(inst, es);
-
-	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
-		es->indent--;
-
-	ExplainCloseGroup("Grouping Set", NULL, true, es);
 }
 
 /*
-- 
2.7.4

v5-0005-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From 2fc507a69a330bb469e41682dfc3dd1905bb7a53 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v5 5/7] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.

This might be unimportant and maybe clearer left in execGrouping.c.
---
 src/backend/commands/explain.c            | 18 +++++++++---------
 src/backend/executor/execGrouping.c       | 27 ---------------------------
 src/backend/executor/nodeAgg.c            | 14 ++++++++++----
 src/backend/executor/nodeRecursiveunion.c |  3 ++-
 src/backend/executor/nodeSetOp.c          |  6 +++++-
 src/backend/executor/nodeSubplan.c        | 10 ++++++++--
 src/include/executor/executor.h           |  1 -
 src/include/executor/nodeAgg.h            |  1 +
 src/include/nodes/execnodes.h             | 24 ++++++++++++++++++++++--
 9 files changed, 57 insertions(+), 47 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 637480d..eb09302 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1347,13 +1347,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument, es);
 			}
 			if (subplanstate->hashnulls)
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			}
 		}
 		if (es->indent)
@@ -1387,14 +1387,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (subplanstate && subplanstate->hashtable)
 		{
 			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument, es);
 			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
 		}
 
 		if (subplanstate && subplanstate->hashnulls)
 		{
 			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
 		}
 	}
@@ -1925,14 +1925,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, es);
+					show_tuplehash_info(&sos->instrument, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, es);
+					show_tuplehash_info(&rus->instrument, es);
 				break;
 			}
 		case T_Group:
@@ -2320,7 +2320,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes <= 1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2347,7 +2347,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 
 	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
 			aggstate->num_hashes ?
-			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			&aggstate->perhash[setno++].instrument : NULL,
 			es);
 
 	foreach(lc, agg->chain)
@@ -2360,7 +2360,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED)
 		{
 			Assert(setno < aggstate->num_hashes);
-			inst = &aggstate->perhash[setno++].hashtable->instrument;
+			inst = &aggstate->perhash[setno++].instrument;
 		}
 
 		show_grouping_set_info(aggstate, aggnode, sortnode,
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 52f955a..de0205f 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -191,7 +191,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -207,7 +206,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -286,34 +284,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial)
-	{
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
-			sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-#define maxself(a,b) a=Max(a,b)
-		/* hashtable->entrysize includes additionalsize */
-		maxself(hashtable->instrument.space_peak_hash,
-				hashtable->hashtab->size * sizeof(TupleHashEntryData));
-		maxself(hashtable->instrument.space_peak_tuples,
-				hashtable->hashtab->members * hashtable->entrysize);
-#undef maxself
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 94c5bb0..c338ace 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1331,6 +1331,9 @@ build_hash_table(AggState *aggstate, int setno, long nbuckets)
 		hashcxt,
 		tmpcxt,
 		DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+
+	InitTupleHashTableStats(perhash->instrument,
+			perhash->hashtable->hashtab, additionalsize);
 }
 
 /*
@@ -1707,9 +1710,10 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
-				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
+				UpdateTupleHashTableStats(aggstate->perhash[0].instrument,
+						aggstate->perhash[0].hashtable->hashtab);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
-									   &aggstate->perhash[0].hashiter);
+						&aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
 				return agg_retrieve_hash_table(aggstate);
 			}
@@ -1910,7 +1914,8 @@ agg_retrieve_direct(AggState *aggstate)
 						aggstate->current_phase == 1)
 				{
 					for (int i = 0; i < aggstate->num_hashes; i++)
-						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+						UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+								aggstate->perhash[i].hashtable->hashtab);
 				}
 			}
 
@@ -1987,7 +1992,8 @@ agg_fill_hash_table(AggState *aggstate)
 
 	aggstate->table_filled = true;
 	for (int i = 0; i < aggstate->num_hashes; i++)
-		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+		UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+				aggstate->perhash[i].hashtable->hashtab);
 
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c2..594abdb 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,7 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, 0);
 }
 
 
@@ -157,7 +158,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab..a386075 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,9 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+
+	InitTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab, 0);
 }
 
 /*
@@ -415,7 +418,8 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index eec849c..c39fdd8 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -507,6 +507,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -520,6 +521,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -535,6 +538,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashtable);
 		else
+		{
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -548,6 +552,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab, 0);
+		}
 	}
 
 	/*
@@ -621,9 +627,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 34199b5..81fdfa4 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 264916f..2072c18 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -302,6 +302,7 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	HashTableInstrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index b1e2d1f..11fe866 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,8 +691,25 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, addsize) \
+	do{\
+	instr.entrysize = sizeof(MinimalTuple) + addsize; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = htable->size * sizeof(TupleHashEntryData); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, htable->size*sizeof(TupleHashEntryData)); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, htable->members*instr.entrysize );\
+	}while(0)
+
 typedef struct HashTableInstrumentation
 {
+	size_t	entrysize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;	/* peak memory usage in bytes */
@@ -717,7 +734,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -883,6 +899,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	HashTableInstrumentation instrument;
+	HashTableInstrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1291,6 +1309,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	HashTableInstrumentation instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -1609,7 +1628,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
-	HashTableInstrumentation	instrument;
+	HashTableInstrumentation instrument;
 } BitmapHeapScanState;
 
 /* ----------------
@@ -2324,6 +2343,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	HashTableInstrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.7.4

v5-0006-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From ef13daa87713d86cf9d7bed45ad1a1bf9872de83 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v5 6/7] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index de0205f..d76a630 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -186,7 +186,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 11fe866..3a335d8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -726,7 +726,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.7.4

v5-0007-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From d21993bfdb7f6713a1fc0b8facf0a24157363700 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v5 7/7] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index c338ace..e5cd87d 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1535,8 +1535,7 @@ lookup_hash_entry(AggState *aggstate, uint32 hash)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  */
-- 
2.7.4

#11Justin Pryzby
pryzby@telsasoft.com
In reply to: Justin Pryzby (#10)
7 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

Updated for new tests in 58c47ccfff20b8c125903482725c1dbfd30beade
and rebased.

Attachments:

v6-0001-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From 3e1904c6c36ee3ff4f56a2808c2400a3b2d2a0e5 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v6 1/7] explain to show tuplehash bucket and memory stats..

Note that hashed SubPlan and recursiveUnion aren't affected in explain output,
probably since hashtables aren't allocated at that point.

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 .../postgres_fdw/expected/postgres_fdw.out    |  56 +++++--
 src/backend/commands/explain.c                | 137 +++++++++++++++--
 src/backend/executor/execGrouping.c           |  27 ++++
 src/backend/executor/nodeAgg.c                |  11 ++
 src/backend/executor/nodeRecursiveunion.c     |   3 +
 src/backend/executor/nodeSetOp.c              |   1 +
 src/backend/executor/nodeSubplan.c            |   3 +
 src/include/executor/executor.h               |   1 +
 src/include/nodes/execnodes.h                 |   9 ++
 src/test/regress/expected/aggregates.out      |  36 +++--
 src/test/regress/expected/groupingsets.out    |  64 ++++++--
 src/test/regress/expected/join.out            |   3 +-
 src/test/regress/expected/matview.out         |   9 +-
 .../regress/expected/partition_aggregate.out  | 145 ++++++++++++++----
 src/test/regress/expected/partition_join.out  |  13 +-
 src/test/regress/expected/pg_lsn.out          |   3 +-
 src/test/regress/expected/select_distinct.out |   3 +-
 src/test/regress/expected/select_parallel.out |  11 +-
 src/test/regress/expected/subselect.out       |   9 +-
 src/test/regress/expected/tablesample.out     |   3 +-
 src/test/regress/expected/union.out           |  15 +-
 src/test/regress/expected/window.out          |   3 +-
 src/test/regress/expected/write_parallel.out  |  16 +-
 23 files changed, 473 insertions(+), 108 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62c2697920..2ddae83178 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2086,9 +2086,11 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
          ->  HashAggregate
                Output: t1.c1, avg((t1.c1 + t2.c1))
                Group Key: t1.c1
+               Buckets: 256
                ->  HashAggregate
                      Output: t1.c1, t2.c1
                      Group Key: t1.c1, t2.c1
+                     Buckets: 4096
                      ->  Append
                            ->  Foreign Scan
                                  Output: t1.c1, t2.c1
@@ -2098,7 +2100,7 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                                  Output: t1_1.c1, t2_1.c1
                                  Relations: (public.ft1 t1_1) INNER JOIN (public.ft2 t2_1)
                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
-(20 rows)
+(22 rows)
 
 SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
  t1c1 |         avg          
@@ -2129,11 +2131,12 @@ SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
          ->  HashAggregate
                Output: t2.c1, t3.c1
                Group Key: t2.c1, t3.c1
+               Buckets: 2
                ->  Foreign Scan
                      Output: t2.c1, t3.c1
                      Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
-(13 rows)
+(14 rows)
 
 SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  C 1 
@@ -2610,10 +2613,11 @@ select c2 * (random() <= 1)::int as c2 from ft2 group by c2 * (random() <= 1)::i
    ->  HashAggregate
          Output: ((c2 * ((random() <= '1'::double precision))::integer))
          Group Key: (ft2.c2 * ((random() <= '1'::double precision))::integer)
+         Buckets: 2
          ->  Foreign Scan on public.ft2
                Output: (c2 * ((random() <= '1'::double precision))::integer)
                Remote SQL: SELECT c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 -- GROUP BY clause in various forms, cardinal, alias and constant expression
 explain (verbose, costs off)
@@ -2713,11 +2717,12 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
    ->  HashAggregate
          Output: sum(c1), c2
          Group Key: ft1.c2
+         Buckets: 16
          Filter: (avg((ft1.c1 * ((random() <= '1'::double precision))::integer)) > '100'::numeric)
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(10 rows)
+(11 rows)
 
 -- Remote aggregate in combination with a local Param (for the output
 -- of an initplan) can be trouble, per bug #15781
@@ -2963,10 +2968,11 @@ select sum(c1) filter (where (c1 / c1) * random() <= 1) from ft1 group by c2 ord
    ->  HashAggregate
          Output: sum(c1) FILTER (WHERE ((((c1 / c1))::double precision * random()) <= '1'::double precision)), c2
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 explain (verbose, costs off)
 select sum(c2) filter (where c2 in (select c2 from ft1 where c2 < 5)) from ft1;
@@ -3229,6 +3235,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
    ->  HashAggregate
          Output: count(*), x.b
          Group Key: x.b
+         Buckets: 16
          ->  Hash Join
                Output: x.b
                Inner Unique: true
@@ -3244,7 +3251,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
                                  Output: ft1_1.c2, (sum(ft1_1.c1))
                                  Relations: Aggregate on (public.ft1 ft1_1)
                                  Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1
-(21 rows)
+(22 rows)
 
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
  count |   b   
@@ -3449,11 +3456,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls la
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3474,11 +3482,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3499,11 +3508,13 @@ select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) orde
    ->  HashAggregate
          Output: c2, c6, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Hash Key: ft1.c6
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c6, c1
                Remote SQL: SELECT "C 1", c2, c6 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(12 rows)
 
 select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) order by 1 nulls last, 2 nulls last;
  c2 | c6 |  sum  
@@ -3526,10 +3537,11 @@ select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nu
    ->  HashAggregate
          Output: c2, sum(c1), GROUPING(c2)
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(9 rows)
+(10 rows)
 
 select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nulls last;
  c2 |  sum  | grouping 
@@ -7147,13 +7159,14 @@ select * from bar where f1 in (select f1 from foo) for update;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for update;
  f1 | f2 
@@ -7185,13 +7198,14 @@ select * from bar where f1 in (select f1 from foo) for share;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for share;
  f1 | f2 
@@ -7222,6 +7236,7 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
@@ -7240,13 +7255,14 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(39 rows)
+(41 rows)
 
 update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
 select tableoid::regclass, * from bar order by 1,2;
@@ -8751,12 +8767,13 @@ SELECT a, sum(b), min(b), count(*) FROM pagg_tab GROUP BY a HAVING avg(b) < 22 O
    Sort Key: pagg_tab.a
    ->  HashAggregate
          Group Key: pagg_tab.a
+         Buckets: 64
          Filter: (avg(pagg_tab.b) < '22'::numeric)
          ->  Append
                ->  Foreign Scan on fpagg_tab_p1 pagg_tab_1
                ->  Foreign Scan on fpagg_tab_p2 pagg_tab_2
                ->  Foreign Scan on fpagg_tab_p3 pagg_tab_3
-(9 rows)
+(10 rows)
 
 -- Plan with partitionwise aggregates is enabled
 SET enable_partitionwise_aggregate TO true;
@@ -8799,6 +8816,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1.a, count(((t1.*)::pagg_tab))
                Group Key: t1.a
+               Buckets: 16
                Filter: (avg(t1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p1 t1
                      Output: t1.a, t1.*, t1.b
@@ -8806,6 +8824,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_1.a, count(((t1_1.*)::pagg_tab))
                Group Key: t1_1.a
+               Buckets: 16
                Filter: (avg(t1_1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p2 t1_1
                      Output: t1_1.a, t1_1.*, t1_1.b
@@ -8813,11 +8832,12 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_2.a, count(((t1_2.*)::pagg_tab))
                Group Key: t1_2.a
+               Buckets: 16
                Filter: (avg(t1_2.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p3 t1_2
                      Output: t1_2.a, t1_2.*, t1_2.b
                      Remote SQL: SELECT a, b, c FROM public.pagg_tab_p3
-(25 rows)
+(28 rows)
 
 SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
  a  | count 
@@ -8839,18 +8859,22 @@ SELECT b, avg(a), max(a), count(*) FROM pagg_tab GROUP BY b HAVING sum(a) < 700
    Sort Key: pagg_tab.b
    ->  Finalize HashAggregate
          Group Key: pagg_tab.b
+         Buckets: 64
          Filter: (sum(pagg_tab.a) < 700)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p1 pagg_tab
                ->  Partial HashAggregate
                      Group Key: pagg_tab_1.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p2 pagg_tab_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_2.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p3 pagg_tab_2
-(15 rows)
+(19 rows)
 
 -- ===================================================================
 -- access rights and superuser
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d901dc4a50..ff22181510 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -86,12 +87,14 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *aggstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_info(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors,
+								   HashTableInstrumentation *inst,
+								   ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -104,6 +107,7 @@ static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
+static void show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1489,6 +1493,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1886,6 +1891,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, es);
+				break;
+			}
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2262,24 +2281,31 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
+			show_grouping_sets(astate, plan, ancestors, es);
 		else
+		{
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes <= 1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
+	int			setno = 0;
 
 	/* Set up deparsing context */
 	context = set_deparse_context_plan(es->deparse_cxt,
@@ -2289,27 +2315,41 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
+			aggstate->num_hashes ?
+			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			es);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		HashTableInstrumentation *inst = NULL;
+
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+		{
+			Assert(setno < aggstate->num_hashes);
+			inst = &aggstate->perhash[setno++].hashtable->instrument;
+		}
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		show_grouping_set_info(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors,
+							   inst, es);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
+/* Show keys and any hash instrumentation for a grouping set */
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_info(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, HashTableInstrumentation *inst,
+					   ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2373,6 +2413,10 @@ show_grouping_set_keys(PlanState *planstate,
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
 
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, es);
+
 	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
 		es->indent--;
 
@@ -2769,6 +2813,54 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 	}
 }
 
+/*
+ * Show hash bucket stats and (optionally) memory.
+ */
+static void
+show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es)
+{
+	long	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld",
+						inst->nbuckets);
+		}
+
+		if (es->analyze)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %ldkB, tuples: %ldkB",
+					spacePeakKb_hash, spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+	}
+}
+
 /*
  * If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
  */
@@ -3436,6 +3528,29 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashtable->instrument, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (sps->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..2f5258e1d2 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,6 +188,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -203,6 +204,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -281,9 +283,34 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial)
+	{
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
+			sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+#define maxself(a,b) a=Max(a,b)
+		/* hashtable->entrysize includes additionalsize */
+		maxself(hashtable->instrument.space_peak_hash,
+				hashtable->hashtab->size * sizeof(TupleHashEntryData));
+		maxself(hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * hashtable->entrysize);
+#undef maxself
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 13c21ffe9a..4b8525edb6 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1710,6 +1710,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
+				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1907,6 +1908,13 @@ agg_retrieve_direct(AggState *aggstate)
 						}
 					}
 				}
+
+				if (aggstate->aggstrategy == AGG_MIXED &&
+						aggstate->current_phase == 1)
+				{
+					for (int i = 0; i < aggstate->num_hashes; i++)
+						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+				}
 			}
 
 			/*
@@ -1981,6 +1989,9 @@ agg_fill_hash_table(AggState *aggstate)
 	}
 
 	aggstate->table_filled = true;
+	for (int i = 0; i < aggstate->num_hashes; i++)
+		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
 	ResetTupleHashIterator(aggstate->perhash[0].hashtable,
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a1ed..93272c28b1 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a41a..9c0e0ab96e 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 298b7757f5..22c32612ba 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 81fdfa4add..34199b57e6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cd3ddf781f..cfeada5f1d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,6 +691,14 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+typedef struct HashTableInstrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;	/* peak memory usage in bytes */
+	size_t	space_peak_tuples;	/* peak memory usage in bytes */
+} HashTableInstrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -709,6 +717,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b150..b173b32cab 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -517,10 +517,11 @@ order by 1, 2;
          ->  HashAggregate
                Output: s2.s2, sum((s1.s1 + s2.s2))
                Group Key: s2.s2
+               Buckets: 4
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -556,10 +557,11 @@ select array(select sum(x+y) s
            ->  HashAggregate
                  Output: sum((x.x + y.y)), y.y
                  Group Key: y.y
+                 Buckets: 4
                  ->  Function Scan on pg_catalog.generate_series y
                        Output: y.y
                        Function Call: generate_series(1, 3)
-(13 rows)
+(14 rows)
 
 select array(select sum(x+y) s
             from generate_series(1,3) y group by y order by s)
@@ -872,12 +874,13 @@ explain (costs off)
 ---------------------------------------------------------------------
  HashAggregate
    Group Key: $0
+   Buckets: 2
    InitPlan 1 (returns $0)
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1096,8 +1099,9 @@ explain (costs off) select * from t1 group by a,b,c,d;
 ----------------------
  HashAggregate
    Group Key: a, b
+   Buckets: 256
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 -- No removal can happen if the complete PK is not present in GROUP BY
 explain (costs off) select a,c from t1 group by a,c,d;
@@ -1105,8 +1109,9 @@ explain (costs off) select a,c from t1 group by a,c,d;
 ----------------------
  HashAggregate
    Group Key: a, c, d
+   Buckets: 256
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 -- Test removal across multiple relations
 explain (costs off) select *
@@ -1116,12 +1121,13 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
 ------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
+   Buckets: 128
    ->  Hash Join
          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
          ->  Seq Scan on t2
          ->  Hash
                ->  Seq Scan on t1
-(7 rows)
+(8 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
@@ -1131,12 +1137,13 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
 ------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
+   Buckets: 128
    ->  Hash Join
          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
          ->  Seq Scan on t2
          ->  Hash
                ->  Seq Scan on t1
-(7 rows)
+(8 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
@@ -1144,8 +1151,9 @@ explain (costs off) select * from t3 group by a,b,c;
 ----------------------
  HashAggregate
    Group Key: a, b, c
+   Buckets: 256
    ->  Seq Scan on t3
-(3 rows)
+(4 rows)
 
 create temp table t1c () inherits (t1);
 -- Ensure we don't remove any columns when t1 has a child table
@@ -1154,10 +1162,11 @@ explain (costs off) select * from t1 group by a,b,c,d;
 -------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t1.c, t1.d
+   Buckets: 256
    ->  Append
          ->  Seq Scan on t1 t1_1
          ->  Seq Scan on t1c t1_2
-(5 rows)
+(6 rows)
 
 -- Okay to remove columns if we're only querying the parent.
 explain (costs off) select * from only t1 group by a,b,c,d;
@@ -1165,8 +1174,9 @@ explain (costs off) select * from only t1 group by a,b,c,d;
 ----------------------
  HashAggregate
    Group Key: a, b
+   Buckets: 2
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 create temp table p_t1 (
   a int,
@@ -1183,10 +1193,11 @@ explain (costs off) select * from p_t1 group by a,b,c,d;
 --------------------------------
  HashAggregate
    Group Key: p_t1.a, p_t1.b
+   Buckets: 512
    ->  Append
          ->  Seq Scan on p_t1_1
          ->  Seq Scan on p_t1_2
-(5 rows)
+(6 rows)
 
 drop table t1 cascade;
 NOTICE:  drop cascades to table t1c
@@ -2354,6 +2365,7 @@ explain (costs off)
    ->  Hash
          ->  HashAggregate
                Group Key: onek.twothousand, onek.twothousand
+               Buckets: 256
                ->  Seq Scan on onek
-(8 rows)
+(9 rows)
 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c1f802c88a..be386731ce 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -974,9 +974,11 @@ explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
    ->  HashAggregate
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          ->  Values Scan on "*VALUES*"
-(6 rows)
+(8 rows)
 
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
@@ -1008,11 +1010,14 @@ explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
    ->  MixedAggregate
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          Group Key: ()
          ->  Values Scan on "*VALUES*"
-(8 rows)
+(11 rows)
 
 -- shouldn't try and hash
 explain (costs off)
@@ -1071,11 +1076,12 @@ explain (costs off)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
    ->  MixedAggregate
          Hash Key: unsortable_col
+         Buckets: 256
          Group Key: unhashable_col
          ->  Sort
                Sort Key: unhashable_col
                ->  Seq Scan on gstest4
-(8 rows)
+(9 rows)
 
 select unhashable_col, unsortable_col,
        grouping(unhashable_col, unsortable_col),
@@ -1114,11 +1120,12 @@ explain (costs off)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
    ->  MixedAggregate
          Hash Key: v, unsortable_col
+         Buckets: 256
          Group Key: v, unhashable_col
          ->  Sort
                Sort Key: v, unhashable_col
                ->  Seq Scan on gstest4
-(8 rows)
+(9 rows)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
@@ -1132,9 +1139,11 @@ explain (costs off)
 --------------------------------
  HashAggregate
    Hash Key: a, b
+   Buckets: 256
    Hash Key: a
+   Buckets: 256
    ->  Seq Scan on gstest_empty
-(4 rows)
+(6 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
  a | b | sum | count 
@@ -1156,11 +1165,12 @@ explain (costs off)
 --------------------------------
  MixedAggregate
    Hash Key: a, b
+   Buckets: 256
    Group Key: ()
    Group Key: ()
    Group Key: ()
    ->  Seq Scan on gstest_empty
-(6 rows)
+(7 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
  sum | count 
@@ -1201,9 +1211,11 @@ explain (costs off)
 ---------------------------
  HashAggregate
    Hash Key: a, b
+   Buckets: 2
    Hash Key: a, c
+   Buckets: 2
    ->  Seq Scan on gstest3
-(4 rows)
+(6 rows)
 
 -- simple rescan tests
 select a, b, sum(v.x)
@@ -1230,11 +1242,13 @@ explain (costs off)
    Sort Key: (sum("*VALUES*".column1)), gstest_data.a, gstest_data.b
    ->  HashAggregate
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(8 rows)
+(10 rows)
 
 select *
   from (values (1),(2)) v(x),
@@ -1286,10 +1300,13 @@ explain (costs off)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), (max("*VALUES*".column3))
    ->  HashAggregate
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 1), ("*VALUES*".column2 + 1)
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 2), ("*VALUES*".column2 + 2)
+         Buckets: 16
          ->  Values Scan on "*VALUES*"
-(7 rows)
+(10 rows)
 
 select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
   from gstest2 group by cube (a,b) order by rsum, a, b;
@@ -1317,11 +1334,14 @@ explain (costs off)
                Sort Key: a, b
                ->  MixedAggregate
                      Hash Key: a, b
+                     Buckets: 256
                      Hash Key: a
+                     Buckets: 256
                      Hash Key: b
+                     Buckets: 256
                      Group Key: ()
                      ->  Seq Scan on gstest2
-(11 rows)
+(14 rows)
 
 select a, b, sum(v.x)
   from (values (1),(2)) v(x), gstest_data(v.x)
@@ -1352,13 +1372,16 @@ explain (costs off)
    Sort Key: gstest_data.a, gstest_data.b
    ->  MixedAggregate
          Hash Key: gstest_data.a, gstest_data.b
+         Buckets: 256
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          Group Key: ()
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(10 rows)
+(13 rows)
 
 -- Verify that we correctly handle the child node returning a
 -- non-minimal slot, which happens if the input is pre-sorted,
@@ -1553,9 +1576,13 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
@@ -1564,7 +1591,7 @@ explain (costs off)
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(13 rows)
+(17 rows)
 
 explain (costs off)
   select unique1,
@@ -1576,14 +1603,18 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Group Key: unique1
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(9 rows)
+(13 rows)
 
 set work_mem = '384kB';
 explain (costs off)
@@ -1596,17 +1627,22 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Hash Key: thousand
+   Buckets: 2048
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(12 rows)
+(17 rows)
 
 -- check collation-sensitive matching between grouping expressions
 -- (similar to a check for aggregates, but there are additional code
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 761376b007..9f07501369 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -6199,6 +6199,7 @@ where exists (select 1 from tenk1 t3
          ->  HashAggregate
                Output: t3.thousand, t3.tenthous
                Group Key: t3.thousand, t3.tenthous
+               Buckets: 16384
                ->  Index Only Scan using tenk1_thous_tenthous on public.tenk1 t3
                      Output: t3.thousand, t3.tenthous
          ->  Hash
@@ -6209,7 +6210,7 @@ where exists (select 1 from tenk1 t3
    ->  Index Only Scan using tenk1_hundred on public.tenk1 t2
          Output: t2.hundred
          Index Cond: (t2.hundred = t3.tenthous)
-(18 rows)
+(19 rows)
 
 -- ... unless it actually is unique
 create table j3 as select unique1, tenthous from onek;
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index d0121a7b0b..ca8573ac6d 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -23,8 +23,9 @@ EXPLAIN (costs off)
 ----------------------------
  HashAggregate
    Group Key: type
+   Buckets: 256
    ->  Seq Scan on mvtest_t
-(3 rows)
+(4 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tm AS SELECT type, sum(amt) AS totamt FROM mvtest_t GROUP BY type WITH NO DATA;
 SELECT relispopulated FROM pg_class WHERE oid = 'mvtest_tm'::regclass;
@@ -61,8 +62,9 @@ EXPLAIN (costs off)
    Sort Key: mvtest_t.type
    ->  HashAggregate
          Group Key: mvtest_t.type
+         Buckets: 256
          ->  Seq Scan on mvtest_t
-(5 rows)
+(6 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tvm AS SELECT * FROM mvtest_tv ORDER BY type;
 SELECT * FROM mvtest_tvm;
@@ -85,8 +87,9 @@ EXPLAIN (costs off)
  Aggregate
    ->  HashAggregate
          Group Key: mvtest_t.type
+         Buckets: 256
          ->  Seq Scan on mvtest_t
-(4 rows)
+(5 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tvvm AS SELECT * FROM mvtest_tvv;
 CREATE VIEW mvtest_tvvmv AS SELECT * FROM mvtest_tvvm;
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index fbc8d3ac6c..5939c2a128 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -27,17 +27,20 @@ SELECT c, sum(a), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY c HAVI
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab.c
+               Buckets: 4
                Filter: (avg(pagg_tab.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p1 pagg_tab
          ->  HashAggregate
                Group Key: pagg_tab_1.c
+               Buckets: 4
                Filter: (avg(pagg_tab_1.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p2 pagg_tab_1
          ->  HashAggregate
                Group Key: pagg_tab_2.c
+               Buckets: 4
                Filter: (avg(pagg_tab_2.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(15 rows)
+(18 rows)
 
 SELECT c, sum(a), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY c HAVING avg(d) < 15 ORDER BY 1, 2, 3;
   c   | sum  |         avg         | count | min | max 
@@ -59,18 +62,22 @@ SELECT a, sum(b), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY a HAVI
    Sort Key: pagg_tab.a, (sum(pagg_tab.b)), (avg(pagg_tab.b))
    ->  Finalize HashAggregate
          Group Key: pagg_tab.a
+         Buckets: 32
          Filter: (avg(pagg_tab.d) < '15'::numeric)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p1 pagg_tab
                ->  Partial HashAggregate
                      Group Key: pagg_tab_1.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p2 pagg_tab_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_2.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(15 rows)
+(19 rows)
 
 SELECT a, sum(b), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY a HAVING avg(d) < 15 ORDER BY 1, 2, 3;
  a  | sum  |         avg         | count | min | max 
@@ -95,14 +102,17 @@ SELECT a, c, count(*) FROM pagg_tab GROUP BY a, c;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.a, pagg_tab.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.a, pagg_tab_1.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.a, pagg_tab_2.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Check with multiple columns in GROUP BY, order in GROUP BY is reversed
 EXPLAIN (COSTS OFF)
@@ -112,14 +122,17 @@ SELECT a, c, count(*) FROM pagg_tab GROUP BY c, a;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.c, pagg_tab.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.c, pagg_tab_1.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.c, pagg_tab_2.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Check with multiple columns in GROUP BY, order in target-list is reversed
 EXPLAIN (COSTS OFF)
@@ -129,14 +142,17 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.a, pagg_tab.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.a, pagg_tab_1.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.a, pagg_tab_2.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
@@ -145,9 +161,10 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 --------------------------------
  HashAggregate
    Group Key: c
+   Buckets: 2
    ->  Result
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -341,12 +358,13 @@ SELECT c, sum(a) FROM pagg_tab GROUP BY rollup(c) ORDER BY 1, 2;
    Sort Key: pagg_tab.c, (sum(pagg_tab.a))
    ->  MixedAggregate
          Hash Key: pagg_tab.c
+         Buckets: 16
          Group Key: ()
          ->  Append
                ->  Seq Scan on pagg_tab_p1 pagg_tab_1
                ->  Seq Scan on pagg_tab_p2 pagg_tab_2
                ->  Seq Scan on pagg_tab_p3 pagg_tab_3
-(9 rows)
+(10 rows)
 
 -- ORDERED SET within the aggregate.
 -- Full aggregation; since all the rows that belong to the same group come
@@ -418,6 +436,7 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
    ->  Append
          ->  HashAggregate
                Group Key: t1.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t1.x = t2.y)
                      ->  Seq Scan on pagg_tab1_p1 t1
@@ -425,6 +444,7 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p1 t2
          ->  HashAggregate
                Group Key: t1_1.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t1_1.x = t2_1.y)
                      ->  Seq Scan on pagg_tab1_p2 t1_1
@@ -432,12 +452,13 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p2 t2_1
          ->  HashAggregate
                Group Key: t1_2.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t2_2.y = t1_2.x)
                      ->  Seq Scan on pagg_tab2_p3 t2_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 t1_2
-(24 rows)
+(27 rows)
 
 SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2.y GROUP BY t1.x ORDER BY 1, 2, 3;
  x  | sum  | count 
@@ -458,6 +479,7 @@ SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t
    Sort Key: t1.x, (sum(t1.y)), (count(((t1.*)::pagg_tab1)))
    ->  HashAggregate
          Group Key: t1.x
+         Buckets: 16
          ->  Hash Join
                Hash Cond: (t1.x = t2.y)
                ->  Append
@@ -469,7 +491,7 @@ SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t
                            ->  Seq Scan on pagg_tab2_p1 t2_1
                            ->  Seq Scan on pagg_tab2_p2 t2_2
                            ->  Seq Scan on pagg_tab2_p3 t2_3
-(15 rows)
+(16 rows)
 
 SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2.y GROUP BY t1.x ORDER BY 1, 2, 3;
  x  | sum  | count 
@@ -491,6 +513,7 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
    ->  Append
          ->  HashAggregate
                Group Key: t2.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t1.x = t2.y)
                      ->  Seq Scan on pagg_tab1_p1 t1
@@ -498,6 +521,7 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p1 t2
          ->  HashAggregate
                Group Key: t2_1.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t1_1.x = t2_1.y)
                      ->  Seq Scan on pagg_tab1_p2 t1_1
@@ -505,12 +529,13 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p2 t2_1
          ->  HashAggregate
                Group Key: t2_2.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t2_2.y = t1_2.x)
                      ->  Seq Scan on pagg_tab2_p3 t2_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 t1_2
-(24 rows)
+(27 rows)
 
 -- When GROUP BY clause does not match; partial aggregation is performed for each partition.
 -- Also test GroupAggregate paths by disabling hash aggregates.
@@ -582,6 +607,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
          ->  Append
                ->  Partial HashAggregate
                      Group Key: b.y
+                     Buckets: 4
                      ->  Hash Left Join
                            Hash Cond: (a.x = b.y)
                            ->  Seq Scan on pagg_tab1_p1 a
@@ -589,6 +615,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
                                  ->  Seq Scan on pagg_tab2_p1 b
                ->  Partial HashAggregate
                      Group Key: b_1.y
+                     Buckets: 4
                      ->  Hash Left Join
                            Hash Cond: (a_1.x = b_1.y)
                            ->  Seq Scan on pagg_tab1_p2 a_1
@@ -596,12 +623,13 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
                                  ->  Seq Scan on pagg_tab2_p2 b_1
                ->  Partial HashAggregate
                      Group Key: b_2.y
+                     Buckets: 4
                      ->  Hash Right Join
                            Hash Cond: (b_2.y = a_2.x)
                            ->  Seq Scan on pagg_tab2_p3 b_2
                            ->  Hash
                                  ->  Seq Scan on pagg_tab1_p3 a_2
-(26 rows)
+(29 rows)
 
 SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY b.y ORDER BY 1 NULLS LAST;
  y  | sum  
@@ -625,6 +653,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
    ->  Append
          ->  HashAggregate
                Group Key: b.y
+               Buckets: 4
                ->  Hash Right Join
                      Hash Cond: (a.x = b.y)
                      ->  Seq Scan on pagg_tab1_p1 a
@@ -632,6 +661,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
                            ->  Seq Scan on pagg_tab2_p1 b
          ->  HashAggregate
                Group Key: b_1.y
+               Buckets: 4
                ->  Hash Right Join
                      Hash Cond: (a_1.x = b_1.y)
                      ->  Seq Scan on pagg_tab1_p2 a_1
@@ -639,12 +669,13 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
                            ->  Seq Scan on pagg_tab2_p2 b_1
          ->  HashAggregate
                Group Key: b_2.y
+               Buckets: 4
                ->  Hash Left Join
                      Hash Cond: (b_2.y = a_2.x)
                      ->  Seq Scan on pagg_tab2_p3 b_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 a_2
-(24 rows)
+(27 rows)
 
 SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP BY b.y ORDER BY 1 NULLS LAST;
  y  | sum  
@@ -674,6 +705,7 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
          ->  Append
                ->  Partial HashAggregate
                      Group Key: a.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (a.x = b.y)
                            ->  Seq Scan on pagg_tab1_p1 a
@@ -681,6 +713,7 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
                                  ->  Seq Scan on pagg_tab2_p1 b
                ->  Partial HashAggregate
                      Group Key: a_1.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (a_1.x = b_1.y)
                            ->  Seq Scan on pagg_tab1_p2 a_1
@@ -688,12 +721,13 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
                                  ->  Seq Scan on pagg_tab2_p2 b_1
                ->  Partial HashAggregate
                      Group Key: a_2.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (b_2.y = a_2.x)
                            ->  Seq Scan on pagg_tab2_p3 b_2
                            ->  Hash
                                  ->  Seq Scan on pagg_tab1_p3 a_2
-(26 rows)
+(29 rows)
 
 SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x ORDER BY 1 NULLS LAST;
  x  | sum  
@@ -728,6 +762,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOI
    Sort Key: pagg_tab1.x, pagg_tab2.y
    ->  HashAggregate
          Group Key: pagg_tab1.x, pagg_tab2.y
+         Buckets: 256
          ->  Hash Left Join
                Hash Cond: (pagg_tab1.x = pagg_tab2.y)
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
@@ -742,7 +777,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOI
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
                                  Filter: (y > 10)
-(18 rows)
+(19 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20  GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
@@ -768,6 +803,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
    Sort Key: pagg_tab1.x, pagg_tab2.y
    ->  HashAggregate
          Group Key: pagg_tab1.x, pagg_tab2.y
+         Buckets: 256
          ->  Hash Full Join
                Hash Cond: (pagg_tab1.x = pagg_tab2.y)
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
@@ -782,7 +818,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
                                  Filter: (y > 10)
-(18 rows)
+(19 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20 GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
@@ -831,18 +867,22 @@ SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a HAVING avg(c) < 22
    Sort Key: pagg_tab_m.a, (sum(pagg_tab_m.b)), (avg(pagg_tab_m.c))
    ->  Finalize HashAggregate
          Group Key: pagg_tab_m.a
+         Buckets: 64
          Filter: (avg(pagg_tab_m.c) < '22'::numeric)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m_1.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m_2.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(19 rows)
 
 SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a HAVING avg(c) < 22 ORDER BY 1, 2, 3;
  a  | sum  |         avg         | count 
@@ -865,17 +905,20 @@ SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a, (a+b)/2 HAVING su
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_m.a, ((pagg_tab_m.a + pagg_tab_m.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m.b) < 50)
                ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
          ->  HashAggregate
                Group Key: pagg_tab_m_1.a, ((pagg_tab_m_1.a + pagg_tab_m_1.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m_1.b) < 50)
                ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
          ->  HashAggregate
                Group Key: pagg_tab_m_2.a, ((pagg_tab_m_2.a + pagg_tab_m_2.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m_2.b) < 50)
                ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(18 rows)
 
 SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a, (a+b)/2 HAVING sum(b) < 50 ORDER BY 1, 2, 3;
  a  | sum |         avg         | count 
@@ -898,17 +941,20 @@ SELECT a, c, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY (a+b)/2, 2, 1 HAV
    ->  Append
          ->  HashAggregate
                Group Key: ((pagg_tab_m.a + pagg_tab_m.b) / 2), pagg_tab_m.c, pagg_tab_m.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m.b) = 50) AND (avg(pagg_tab_m.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
          ->  HashAggregate
                Group Key: ((pagg_tab_m_1.a + pagg_tab_m_1.b) / 2), pagg_tab_m_1.c, pagg_tab_m_1.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m_1.b) = 50) AND (avg(pagg_tab_m_1.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
          ->  HashAggregate
                Group Key: ((pagg_tab_m_2.a + pagg_tab_m_2.b) / 2), pagg_tab_m_2.c, pagg_tab_m_2.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m_2.b) = 50) AND (avg(pagg_tab_m_2.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(18 rows)
 
 SELECT a, c, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY (a+b)/2, 2, 1 HAVING sum(b) = 50 AND avg(c) > 25 ORDER BY 1, 2, 3;
  a  | c  | sum |         avg         | count 
@@ -1032,6 +1078,7 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_ml.a
+               Buckets: 16
                Filter: (avg(pagg_tab_ml.b) < '3'::numeric)
                ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  Finalize GroupAggregate
@@ -1042,9 +1089,11 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                      ->  Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_2.a
+                                 Buckets: 16
                                  ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_2
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_3.a
+                                 Buckets: 16
                                  ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_3
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_5.a
@@ -1054,11 +1103,13 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                      ->  Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_5.a
+                                 Buckets: 8
                                  ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_5
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_6.a
+                                 Buckets: 8
                                  ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_6
-(31 rows)
+(36 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1089,20 +1140,25 @@ SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b ORDER BY 1, 2, 3;
                ->  Append
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_1.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_2.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_3.b
+                           Buckets: 8
                            ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_4.b
+                           Buckets: 8
                            ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(22 rows)
+(27 rows)
 
 SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b HAVING avg(a) < 15 ORDER BY 1, 2, 3;
  b |  sum  | count 
@@ -1124,25 +1180,30 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 O
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_ml.a, pagg_tab_ml.b, pagg_tab_ml.c
+               Buckets: 512
                Filter: (avg(pagg_tab_ml.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  HashAggregate
                Group Key: pagg_tab_ml_1.a, pagg_tab_ml_1.b, pagg_tab_ml_1.c
+               Buckets: 256
                Filter: (avg(pagg_tab_ml_1.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
          ->  HashAggregate
                Group Key: pagg_tab_ml_2.a, pagg_tab_ml_2.b, pagg_tab_ml_2.c
+               Buckets: 256
                Filter: (avg(pagg_tab_ml_2.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
          ->  HashAggregate
                Group Key: pagg_tab_ml_3.a, pagg_tab_ml_3.b, pagg_tab_ml_3.c
+               Buckets: 128
                Filter: (avg(pagg_tab_ml_3.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
          ->  HashAggregate
                Group Key: pagg_tab_ml_4.a, pagg_tab_ml_4.b, pagg_tab_ml_4.c
+               Buckets: 128
                Filter: (avg(pagg_tab_ml_4.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(23 rows)
+(28 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1183,6 +1244,7 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            Sort Key: pagg_tab_ml.a
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml.a
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_2.a
@@ -1194,9 +1256,11 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            ->  Parallel Append
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_2.a
+                                       Buckets: 16
                                        ->  Parallel Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_2
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_3.a
+                                       Buckets: 16
                                        ->  Parallel Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_3
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_5.a
@@ -1208,11 +1272,13 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            ->  Parallel Append
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_5.a
+                                       Buckets: 8
                                        ->  Parallel Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_5
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_6.a
+                                       Buckets: 8
                                        ->  Parallel Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_6
-(41 rows)
+(46 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1245,20 +1311,25 @@ SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b ORDER BY 1, 2, 3;
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_1.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_2.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_3.b
+                                 Buckets: 8
                                  ->  Parallel Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_4.b
+                                 Buckets: 8
                                  ->  Parallel Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(24 rows)
+(29 rows)
 
 SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b HAVING avg(a) < 15 ORDER BY 1, 2, 3;
  b |  sum  | count 
@@ -1282,25 +1353,30 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 O
          ->  Parallel Append
                ->  HashAggregate
                      Group Key: pagg_tab_ml.a, pagg_tab_ml.b, pagg_tab_ml.c
+                     Buckets: 512
                      Filter: (avg(pagg_tab_ml.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                ->  HashAggregate
                      Group Key: pagg_tab_ml_1.a, pagg_tab_ml_1.b, pagg_tab_ml_1.c
+                     Buckets: 256
                      Filter: (avg(pagg_tab_ml_1.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                ->  HashAggregate
                      Group Key: pagg_tab_ml_2.a, pagg_tab_ml_2.b, pagg_tab_ml_2.c
+                     Buckets: 256
                      Filter: (avg(pagg_tab_ml_2.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                ->  HashAggregate
                      Group Key: pagg_tab_ml_3.a, pagg_tab_ml_3.b, pagg_tab_ml_3.c
+                     Buckets: 128
                      Filter: (avg(pagg_tab_ml_3.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                ->  HashAggregate
                      Group Key: pagg_tab_ml_4.a, pagg_tab_ml_4.b, pagg_tab_ml_4.c
+                     Buckets: 128
                      Filter: (avg(pagg_tab_ml_4.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(25 rows)
+(30 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1351,14 +1427,17 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_1.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_2.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+(22 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1388,14 +1467,17 @@ SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) <
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_1.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_2.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+(22 rows)
 
 SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) < 12 ORDER BY 1, 2, 3;
  y  |  sum  |         avg         | count 
@@ -1425,11 +1507,12 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      Sort Key: pagg_tab_para.x
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_para.x
+                           Buckets: 64
                            ->  Parallel Append
                                  ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para_1
                                  ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_3
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_2
-(15 rows)
+(16 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1459,11 +1542,12 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      Sort Key: pagg_tab_para.x
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_para.x
+                           Buckets: 64
                            ->  Parallel Append
                                  ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para_1
                                  ->  Seq Scan on pagg_tab_para_p2 pagg_tab_para_2
                                  ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_3
-(15 rows)
+(16 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1488,17 +1572,20 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_para.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para
          ->  HashAggregate
                Group Key: pagg_tab_para_1.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para_1.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
          ->  HashAggregate
                Group Key: pagg_tab_para_2.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para_2.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(15 rows)
+(18 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index b3fbe47bde..b8309304c0 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -819,6 +819,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_2.a = t1_5.b)
                ->  HashAggregate
                      Group Key: t1_5.b
+                     Buckets: 4
                      ->  Hash Join
                            Hash Cond: (((t2_1.a + t2_1.b) / 2) = t1_5.b)
                            ->  Seq Scan on prt1_e_p1 t2_1
@@ -832,6 +833,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_3.a = t1_6.b)
                ->  HashAggregate
                      Group Key: t1_6.b
+                     Buckets: 4
                      ->  Hash Join
                            Hash Cond: (((t2_2.a + t2_2.b) / 2) = t1_6.b)
                            ->  Seq Scan on prt1_e_p2 t2_2
@@ -845,6 +847,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_4.a = t1_7.b)
                ->  HashAggregate
                      Group Key: t1_7.b
+                     Buckets: 2
                      ->  Nested Loop
                            ->  Seq Scan on prt2_p3 t1_7
                                  Filter: (a = 0)
@@ -853,7 +856,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                ->  Index Scan using iprt1_p3_a on prt1_p3 t1_4
                      Index Cond: (a = ((t2_3.a + t2_3.b) / 2))
                      Filter: (b = 0)
-(41 rows)
+(44 rows)
 
 SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHERE t1.a = 0 AND t1.b = (t2.a + t2.b)/2) AND t1.b = 0 ORDER BY t1.a;
   a  | b |  c   
@@ -874,6 +877,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_6.b
+                     Buckets: 4
                      ->  Hash Semi Join
                            Hash Cond: (t1_6.b = ((t1_9.a + t1_9.b) / 2))
                            ->  Seq Scan on prt2_p1 t1_6
@@ -886,6 +890,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_7.b
+                     Buckets: 4
                      ->  Hash Semi Join
                            Hash Cond: (t1_7.b = ((t1_10.a + t1_10.b) / 2))
                            ->  Seq Scan on prt2_p2 t1_7
@@ -898,6 +903,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_8.b
+                     Buckets: 2
                      ->  Hash Semi Join
                            Hash Cond: (t1_8.b = ((t1_11.a + t1_11.b) / 2))
                            ->  Seq Scan on prt2_p3 t1_8
@@ -907,7 +913,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
                ->  Index Scan using iprt1_p3_a on prt1_p3 t1_5
                      Index Cond: (a = t1_8.b)
                      Filter: (b = 0)
-(39 rows)
+(42 rows)
 
 SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a;
   a  | b |  c   
@@ -1466,6 +1472,7 @@ SELECT avg(t1.a), avg(t2.b), t1.c, t2.c FROM plt1 t1 RIGHT JOIN plt2 t2 ON t1.c
    Sort Key: t1.c
    ->  HashAggregate
          Group Key: t1.c, t2.c
+         Buckets: 4
          ->  Append
                ->  Hash Join
                      Hash Cond: (t2_1.c = t1_1.c)
@@ -1485,7 +1492,7 @@ SELECT avg(t1.a), avg(t2.b), t1.c, t2.c FROM plt1 t1 RIGHT JOIN plt2 t2 ON t1.c
                      ->  Hash
                            ->  Seq Scan on plt1_p3 t1_3
                                  Filter: ((a % 25) = 0)
-(23 rows)
+(24 rows)
 
 --
 -- multiple levels of partitioning
diff --git a/src/test/regress/expected/pg_lsn.out b/src/test/regress/expected/pg_lsn.out
index 64d41dfdad..c6a56254be 100644
--- a/src/test/regress/expected/pg_lsn.out
+++ b/src/test/regress/expected/pg_lsn.out
@@ -85,6 +85,7 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
    Sort Key: (((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn)
    ->  HashAggregate
          Group Key: ((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn
+         Buckets: 4
          ->  Nested Loop
                ->  Function Scan on generate_series k
                ->  Materialize
@@ -93,7 +94,7 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
                                  Filter: ((j > 0) AND (j <= 10))
                            ->  Function Scan on generate_series i
                                  Filter: (i <= 10)
-(12 rows)
+(13 rows)
 
 SELECT DISTINCT (i || '/' || j)::pg_lsn f
   FROM generate_series(1, 10) i,
diff --git a/src/test/regress/expected/select_distinct.out b/src/test/regress/expected/select_distinct.out
index f3696c6d1d..8e2903a3ed 100644
--- a/src/test/regress/expected/select_distinct.out
+++ b/src/test/regress/expected/select_distinct.out
@@ -137,9 +137,10 @@ SELECT count(*) FROM
    ->  HashAggregate
          Output: tenk1.two, tenk1.four, tenk1.two
          Group Key: tenk1.two, tenk1.four, tenk1.two
+         Buckets: 8
          ->  Seq Scan on public.tenk1
                Output: tenk1.two, tenk1.four, tenk1.two
-(7 rows)
+(8 rows)
 
 SELECT count(*) FROM
   (SELECT DISTINCT two, four, two FROM tenk1) ss;
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 96dfb7c8dd..2b8a253c79 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -226,12 +226,14 @@ explain (costs off)
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 select length(stringu1) from tenk1 group by length(stringu1);
  length 
@@ -247,12 +249,14 @@ explain (costs off)
    Sort Key: stringu1
    ->  Finalize HashAggregate
          Group Key: stringu1
+         Buckets: 1024
          ->  Gather
                Workers Planned: 4
                ->  Partial HashAggregate
                      Group Key: stringu1
+                     Buckets: 1024
                      ->  Parallel Seq Scan on tenk1
-(9 rows)
+(11 rows)
 
 -- test that parallel plan for aggregates is not selected when
 -- target list contains parallel restricted clause.
@@ -263,10 +267,11 @@ explain (costs off)
 -------------------------------------------------------------------
  HashAggregate
    Group Key: sp_parallel_restricted(unique1)
+   Buckets: 16384
    ->  Gather
          Workers Planned: 4
          ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
-(5 rows)
+(6 rows)
 
 -- test prepared statement
 prepare tenk1_count(integer) As select  count((unique1)) from tenk1 where hundred > $1;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 4c6cd5f146..e4a79170d8 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -932,6 +932,7 @@ where o.ten = 1;
                Filter: (ten = 1)
          ->  Subquery Scan on ss
                ->  HashSetOp Except
+                     Buckets: 2
                      ->  Append
                            ->  Subquery Scan on "*SELECT* 1"
                                  ->  Index Scan using onek_unique1 on onek i1
@@ -939,7 +940,7 @@ where o.ten = 1;
                            ->  Subquery Scan on "*SELECT* 2"
                                  ->  Index Scan using onek_unique1 on onek i2
                                        Index Cond: (unique1 = o.unique2)
-(13 rows)
+(14 rows)
 
 select count(*) from
   onek o cross join lateral (
@@ -976,10 +977,11 @@ where o.ten = 1;
          ->  CTE Scan on x
                CTE x
                  ->  Recursive Union
+                       Buckets: 64
                        ->  Result
                        ->  WorkTable Scan on x x_1
                              Filter: (a < 10)
-(10 rows)
+(11 rows)
 
 select sum(o.four), sum(ss.a) from
   onek o cross join lateral (
@@ -1130,9 +1132,10 @@ select * from int4_tbl o where (f1, f1) in
                            ->  HashAggregate
                                  Output: i.f1
                                  Group Key: i.f1
+                                 Buckets: 8
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,50) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/tablesample.out b/src/test/regress/expected/tablesample.out
index 078358d226..fc41a81039 100644
--- a/src/test/regress/expected/tablesample.out
+++ b/src/test/regress/expected/tablesample.out
@@ -255,11 +255,12 @@ select pct, count(unique1) from
 --------------------------------------------------------
  HashAggregate
    Group Key: "*VALUES*".column1
+   Buckets: 2
    ->  Nested Loop
          ->  Values Scan on "*VALUES*"
          ->  Sample Scan on tenk1
                Sampling: bernoulli ("*VALUES*".column1)
-(6 rows)
+(7 rows)
 
 select pct, count(unique1) from
   (values (0),(100)) v(pct),
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92d80..2153974fa7 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -355,12 +355,13 @@ select count(*) from
  Aggregate
    ->  Subquery Scan on ss
          ->  HashSetOp Intersect
+               Buckets: 8192
                ->  Append
                      ->  Subquery Scan on "*SELECT* 2"
                            ->  Seq Scan on tenk1
                      ->  Subquery Scan on "*SELECT* 1"
                            ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(8 rows)
+(9 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -374,13 +375,14 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  HashSetOp Except
+   Buckets: 16384
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Index Only Scan using tenk1_unique1 on tenk1
          ->  Subquery Scan on "*SELECT* 2"
                ->  Index Only Scan using tenk1_unique2 on tenk1 tenk1_1
                      Filter: (unique2 <> 10)
-(7 rows)
+(8 rows)
 
 select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
  unique1 
@@ -585,12 +587,13 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
                               QUERY PLAN                              
 ----------------------------------------------------------------------
  HashSetOp Intersect
+   Buckets: 2
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Function Scan on generate_series
          ->  Subquery Scan on "*SELECT* 2"
                ->  Function Scan on generate_series generate_series_1
-(6 rows)
+(7 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
@@ -726,12 +729,13 @@ explain (costs off)
 ---------------------------------------------------
  HashAggregate
    Group Key: ((t1.a || t1.b))
+   Buckets: 8
    ->  Append
          ->  Index Scan using t1_ab_idx on t1
                Index Cond: ((a || b) = 'ab'::text)
          ->  Index Only Scan using t2_pkey on t2
                Index Cond: (ab = 'ab'::text)
-(7 rows)
+(8 rows)
 
 --
 -- Test that ORDER BY for UNION ALL can be pushed down to inheritance
@@ -864,11 +868,12 @@ ORDER BY x;
          Filter: (ss.x < 4)
          ->  HashAggregate
                Group Key: (1), (generate_series(1, 10))
+               Buckets: 16
                ->  Append
                      ->  ProjectSet
                            ->  Result
                      ->  Result
-(10 rows)
+(11 rows)
 
 SELECT * FROM
   (SELECT 1 AS t, generate_series(1,10) AS x
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index d5fd4045f9..ea34daffe4 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -624,8 +624,9 @@ select first_value(max(x)) over (), y
  WindowAgg
    ->  HashAggregate
          Group Key: (tenk1.ten + tenk1.four)
+         Buckets: 64
          ->  Seq Scan on tenk1
-(4 rows)
+(5 rows)
 
 -- test non-default frame specifications
 SELECT four, ten,
diff --git a/src/test/regress/expected/write_parallel.out b/src/test/regress/expected/write_parallel.out
index 0c4da2591a..25193a8b84 100644
--- a/src/test/regress/expected/write_parallel.out
+++ b/src/test/regress/expected/write_parallel.out
@@ -19,12 +19,14 @@ explain (costs off) create table parallel_write as
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create table parallel_write as
     select length(stringu1) from tenk1 group by length(stringu1);
@@ -35,12 +37,14 @@ explain (costs off) select length(stringu1) into parallel_write
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 select length(stringu1) into parallel_write
     from tenk1 group by length(stringu1);
@@ -51,12 +55,14 @@ explain (costs off) create materialized view parallel_mat_view as
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create materialized view parallel_mat_view as
     select length(stringu1) from tenk1 group by length(stringu1);
@@ -67,12 +73,14 @@ explain (costs off) create table parallel_write as execute prep_stmt;
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create table parallel_write as execute prep_stmt;
 drop table parallel_write;
-- 
2.17.0

v6-0002-refactor-show_grouping_set_keys.patchtext/x-diff; charset=us-asciiDownload
From 41f9ea8d4a06f895872daf088603834550ce6dcc Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 23 Feb 2020 23:13:07 -0600
Subject: [PATCH v6 2/7] refactor show_grouping_set_keys

---
 src/backend/commands/explain.c | 55 ++++++++++++++++++++--------------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ff22181510..f416f60676 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -95,6 +95,8 @@ static void show_grouping_set_info(AggState *aggstate,
 								   List *ancestors,
 								   HashTableInstrumentation *inst,
 								   ExplainState *es);
+static void show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List
+		*context, bool useprefix, ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -2348,6 +2350,37 @@ show_grouping_set_info(AggState *aggstate,
 					   List *context, bool useprefix,
 					   List *ancestors, HashTableInstrumentation *inst,
 					   ExplainState *es)
+{
+	PlanState	*planstate = outerPlanState(aggstate);
+
+	ExplainOpenGroup("Grouping Set", NULL, true, es);
+
+	if (sortnode)
+	{
+		show_sort_group_keys(planstate, "Sort Key",
+							 sortnode->numCols, sortnode->sortColIdx,
+							 sortnode->sortOperators, sortnode->collations,
+							 sortnode->nullsFirst,
+							 ancestors, es);
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			es->indent++;
+	}
+
+	show_grouping_set_keys(aggstate, aggnode, context, useprefix, es);
+
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, es);
+
+	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
+		es->indent--;
+
+	ExplainCloseGroup("Grouping Set", NULL, true, es);
+}
+
+/* Show keys of a grouping set */
+static void
+show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List *context, bool useprefix, ExplainState *es)
 {
 	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
@@ -2369,19 +2402,6 @@ show_grouping_set_info(AggState *aggstate,
 		keysetname = "Group Keys";
 	}
 
-	ExplainOpenGroup("Grouping Set", NULL, true, es);
-
-	if (sortnode)
-	{
-		show_sort_group_keys(planstate, "Sort Key",
-							 sortnode->numCols, sortnode->sortColIdx,
-							 sortnode->sortOperators, sortnode->collations,
-							 sortnode->nullsFirst,
-							 ancestors, es);
-		if (es->format == EXPLAIN_FORMAT_TEXT)
-			es->indent++;
-	}
-
 	ExplainOpenGroup(keysetname, keysetname, false, es);
 
 	foreach(lc, gsets)
@@ -2412,15 +2432,6 @@ show_grouping_set_info(AggState *aggstate,
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
-
-	if (aggnode->aggstrategy == AGG_HASHED ||
-			aggnode->aggstrategy == AGG_MIXED)
-		show_tuplehash_info(inst, es);
-
-	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
-		es->indent--;
-
-	ExplainCloseGroup("Grouping Set", NULL, true, es);
 }
 
 /*
-- 
2.17.0

v6-0003-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From 2e6e2007a9c1b4c4584a1fb6fee21443f34b6cc7 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v6 3/7] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c | 70 ++++++++++++++++++----------------
 1 file changed, 38 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index f416f60676..5791ee0cb9 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						SubPlanState *subplanstate, ExplainState *es);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -721,7 +721,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, NULL, es);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1083,7 +1083,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			SubPlanState *subplanstate, ExplainState *es)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1340,6 +1340,21 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			}
+			if (subplanstate->hashnulls)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1368,6 +1383,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+
+		if (subplanstate && subplanstate->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (subplanstate && subplanstate->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2040,12 +2069,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, NULL, es);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, NULL, es);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2077,7 +2106,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, NULL, es);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3480,7 +3509,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, NULL, es);
 }
 
 /*
@@ -3538,30 +3567,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-		{
-			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashtable->instrument, es);
-			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
-		}
-
-		if (sps->hashnulls)
-		{
-			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, es);
-			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
-		}
+					relationship, sp->plan_name, sps, es);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3578,7 +3584,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, NULL, es);
 }
 
 /*
-- 
2.17.0

v6-0004-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From 80e69b60eacd1b7e3dab2533b81ed1e5502a1d02 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v6 4/7] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.

Note, this doesn't affect any regression tests, since hashtable isn't allocated
during "explain".  Note that "explain analyze" would show memory stats, which
we'd have to filter.
---
 src/backend/commands/explain.c            |  5 ++--
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 29 +++++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5791ee0cb9..637480de79 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1734,8 +1734,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
-			if (es->analyze)
-				show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
+			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
 			break;
 		case T_SampleScan:
 			show_tablesample(((SampleScan *) plan)->tablesample,
@@ -2927,6 +2926,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index ae8a11da30..9ae99a326e 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -744,6 +746,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index ad4e071ca3..ab81cce3b2 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -44,6 +44,7 @@
 #include "common/hashfn.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	HashTableInstrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1147,6 +1150,32 @@ tbm_end_iterate(TBMIterator *iterator)
 	pfree(iterator);
 }
 
+/*
+ * tbm_instrumentation - update stored stats and return pointer to
+ * instrumentation structure
+ *
+ * This updates stats when called.
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+HashTableInstrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable)
+	{
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+
+		/*
+		 * If there are lossy pages, then at one point, we filled maxentries;
+		 * otherwise, number of pages is "->members".
+		 */
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) *
+			(tbm->nchunks>0 ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
 /*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cfeada5f1d..b1e2d1f1ae 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1609,6 +1609,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	HashTableInstrumentation	instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fcae34..de0cdfb91f 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct HashTableInstrumentation HashTableInstrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern HashTableInstrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.17.0

v6-0005-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From 320bddf8fbefe31c150406187fcf8757e8a3ebba Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v6 5/7] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.

This might be unimportant and maybe clearer left in execGrouping.c.
---
 src/backend/commands/explain.c            | 18 +++++++--------
 src/backend/executor/execGrouping.c       | 27 -----------------------
 src/backend/executor/nodeAgg.c            | 14 ++++++++----
 src/backend/executor/nodeRecursiveunion.c |  3 ++-
 src/backend/executor/nodeSetOp.c          |  6 ++++-
 src/backend/executor/nodeSubplan.c        | 10 +++++++--
 src/include/executor/executor.h           |  1 -
 src/include/executor/nodeAgg.h            |  1 +
 src/include/nodes/execnodes.h             | 24 ++++++++++++++++++--
 9 files changed, 57 insertions(+), 47 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 637480de79..eb093029d9 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1347,13 +1347,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument, es);
 			}
 			if (subplanstate->hashnulls)
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			}
 		}
 		if (es->indent)
@@ -1387,14 +1387,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (subplanstate && subplanstate->hashtable)
 		{
 			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument, es);
 			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
 		}
 
 		if (subplanstate && subplanstate->hashnulls)
 		{
 			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
 		}
 	}
@@ -1925,14 +1925,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, es);
+					show_tuplehash_info(&sos->instrument, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, es);
+					show_tuplehash_info(&rus->instrument, es);
 				break;
 			}
 		case T_Group:
@@ -2320,7 +2320,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes <= 1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2347,7 +2347,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 
 	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
 			aggstate->num_hashes ?
-			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			&aggstate->perhash[setno++].instrument : NULL,
 			es);
 
 	foreach(lc, agg->chain)
@@ -2360,7 +2360,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED)
 		{
 			Assert(setno < aggstate->num_hashes);
-			inst = &aggstate->perhash[setno++].hashtable->instrument;
+			inst = &aggstate->perhash[setno++].instrument;
 		}
 
 		show_grouping_set_info(aggstate, aggnode, sortnode,
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 2f5258e1d2..009d27b9a8 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,7 +188,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -204,7 +203,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -283,34 +281,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial)
-	{
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
-			sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-#define maxself(a,b) a=Max(a,b)
-		/* hashtable->entrysize includes additionalsize */
-		maxself(hashtable->instrument.space_peak_hash,
-				hashtable->hashtab->size * sizeof(TupleHashEntryData));
-		maxself(hashtable->instrument.space_peak_tuples,
-				hashtable->hashtab->members * hashtable->entrysize);
-#undef maxself
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 4b8525edb6..3d60f7a314 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1334,6 +1334,9 @@ build_hash_table(AggState *aggstate, int setno, long nbuckets)
 		hashcxt,
 		tmpcxt,
 		DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+
+	InitTupleHashTableStats(perhash->instrument,
+			perhash->hashtable->hashtab, additionalsize);
 }
 
 /*
@@ -1710,9 +1713,10 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
-				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
+				UpdateTupleHashTableStats(aggstate->perhash[0].instrument,
+						aggstate->perhash[0].hashtable->hashtab);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
-									   &aggstate->perhash[0].hashiter);
+						&aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
 				return agg_retrieve_hash_table(aggstate);
 			}
@@ -1913,7 +1917,8 @@ agg_retrieve_direct(AggState *aggstate)
 						aggstate->current_phase == 1)
 				{
 					for (int i = 0; i < aggstate->num_hashes; i++)
-						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+						UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+								aggstate->perhash[i].hashtable->hashtab);
 				}
 			}
 
@@ -1990,7 +1995,8 @@ agg_fill_hash_table(AggState *aggstate)
 
 	aggstate->table_filled = true;
 	for (int i = 0; i < aggstate->num_hashes; i++)
-		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+		UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+				aggstate->perhash[i].hashtable->hashtab);
 
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c28b1..594abdbdf0 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,7 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, 0);
 }
 
 
@@ -157,7 +158,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab96e..a3860758eb 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,9 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+
+	InitTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab, 0);
 }
 
 /*
@@ -415,7 +418,8 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 22c32612ba..03533538f6 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -505,6 +505,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -518,6 +519,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -533,6 +536,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashnulls);
 		else
+		{
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -546,6 +550,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab, 0);
+		}
 	}
 	else
 		node->hashnulls = NULL;
@@ -621,9 +627,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 34199b57e6..81fdfa4add 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 264916f9a9..2072c18b54 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -302,6 +302,7 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	HashTableInstrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index b1e2d1f1ae..11fe866c1c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,8 +691,25 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, addsize) \
+	do{\
+	instr.entrysize = sizeof(MinimalTuple) + addsize; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = htable->size * sizeof(TupleHashEntryData); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, htable->size*sizeof(TupleHashEntryData)); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, htable->members*instr.entrysize );\
+	}while(0)
+
 typedef struct HashTableInstrumentation
 {
+	size_t	entrysize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;	/* peak memory usage in bytes */
@@ -717,7 +734,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -883,6 +899,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	HashTableInstrumentation instrument;
+	HashTableInstrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1291,6 +1309,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	HashTableInstrumentation instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -1609,7 +1628,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
-	HashTableInstrumentation	instrument;
+	HashTableInstrumentation instrument;
 } BitmapHeapScanState;
 
 /* ----------------
@@ -2324,6 +2343,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	HashTableInstrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.17.0

v6-0006-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From 94597b3250353c6f6915235026ded3e1c2bde24b Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v6 6/7] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..98fd4bf8bd 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -183,7 +183,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 11fe866c1c..3a335d87a0 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -726,7 +726,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.17.0

v6-0007-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From f65ce612edc9545188c8531637980420b101f94f Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v6 7/7] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 3d60f7a314..567d65bb6c 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1538,8 +1538,7 @@ lookup_hash_entry(AggState *aggstate, uint32 hash)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  */
-- 
2.17.0

#12Andres Freund
andres@anarazel.de
In reply to: Justin Pryzby (#11)
Re: explain HashAggregate to report bucket and memory stats

Hi,

On 2020-03-01 09:45:40 -0600, Justin Pryzby wrote:

+/*
+ * Show hash bucket stats and (optionally) memory.
+ */
+static void
+show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es)
+{
+	long	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;

Let's not add further uses of long. It's terrible because it has
different widths on 64bit windows and everything else. Specify the
widths explicitly, or use something like size_t.

+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);

And then you're passing the long to ExplainPropertyInteger which accepts
a int64, making the use of long above particularly suspicious.

I wonder if it would make sense to add a ExplainPropertyBytes(), that
would do the rounding etc automatically. It could then also switch units
as appropriate.

+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld",
+						inst->nbuckets);
+		}
+
+		if (es->analyze)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %ldkB, tuples: %ldkB",
+					spacePeakKb_hash, spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');

I'm not sure I like the alternative output formats here. All the other
fields are separated with a comma, but the original size is in
parens. I'd probably just format it as "Buckets: %lld " and then add
", Original Buckets: %lld" when differing.

Also, %ld is problematic, because it's only 32bit wide on some platforms
(including 64bit windows), but 64bit on others. The easiest way to deal
with that is to use %lld and cast the argument to long long - yes that's
a sad workaround.

+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial)
+	{
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
+			sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+#define maxself(a,b) a=Max(a,b)
+		/* hashtable->entrysize includes additionalsize */
+		maxself(hashtable->instrument.space_peak_hash,
+				hashtable->hashtab->size * sizeof(TupleHashEntryData));
+		maxself(hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * hashtable->entrysize);
+#undef maxself
+	}
+}

Not a fan of this macro.

I'm also not sure I understand what you're trying to do here?

diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b150..b173b32cab 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -517,10 +517,11 @@ order by 1, 2;
->  HashAggregate
Output: s2.s2, sum((s1.s1 + s2.s2))
Group Key: s2.s2
+               Buckets: 4
->  Function Scan on pg_catalog.generate_series s2
Output: s2.s2
Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)

These tests probably won't be portable. The number of hash buckets
calculated will e.g. depend onthe size of the contained elements. And
that'll e.g. will depend on whether pointers are 4 or 8 bytes.

Greetings,

Andres Freund

#13Tomas Vondra
tomas.vondra@2ndquadrant.com
In reply to: Andres Freund (#12)
Re: explain HashAggregate to report bucket and memory stats

On Fri, Mar 06, 2020 at 09:58:59AM -0800, Andres Freund wrote:

...

+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld",
+						inst->nbuckets);
+		}
+
+		if (es->analyze)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %ldkB, tuples: %ldkB",
+					spacePeakKb_hash, spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');

I'm not sure I like the alternative output formats here. All the other
fields are separated with a comma, but the original size is in
parens. I'd probably just format it as "Buckets: %lld " and then add
", Original Buckets: %lld" when differing.

FWIW this copies hashjoin precedent, which does this:

appendStringInfo(es->str,
"Buckets: %d (originally %d) Batches: %d (originally %d) Memory Usage: %ldkB\n",
hinstrument.nbuckets,
...

I agree it's not ideal, but maybe let's not invent new ways to format
the same type of info.

regards

--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#14Justin Pryzby
pryzby@telsasoft.com
In reply to: Andres Freund (#12)
9 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

On Fri, Mar 06, 2020 at 09:58:59AM -0800, Andres Freund wrote:

+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);

I'm not sure I like the alternative output formats here. All the other
fields are separated with a comma, but the original size is in
parens. I'd probably just format it as "Buckets: %lld " and then add
", Original Buckets: %lld" when differing.

It's done that way for consistency with hashJoin in show_hash_info().

+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial)
+	{
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
+			sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+#define maxself(a,b) a=Max(a,b)
+		/* hashtable->entrysize includes additionalsize */
+		maxself(hashtable->instrument.space_peak_hash,
+				hashtable->hashtab->size * sizeof(TupleHashEntryData));
+		maxself(hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * hashtable->entrysize);
+#undef maxself
+	}
+}

Not a fan of this macro.

I'm also not sure I understand what you're trying to do here?

I have to call UpdateTupleHashTableStats() from the callers at deliberate
locations. If the caller fills the hashtable all at once, I can populate the
stats immediately after that, but if it's populated incrementally, then need to
update stats right before it's destroyed or reset, otherwise we can show tuple
size of the hashtable since its most recent reset, rather than a larger,
previous incarnation.

diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b150..b173b32cab 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -517,10 +517,11 @@ order by 1, 2;
->  HashAggregate
Output: s2.s2, sum((s1.s1 + s2.s2))
Group Key: s2.s2
+               Buckets: 4
->  Function Scan on pg_catalog.generate_series s2
Output: s2.s2
Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)

These tests probably won't be portable. The number of hash buckets
calculated will e.g. depend onthe size of the contained elements. And
that'll e.g. will depend on whether pointers are 4 or 8 bytes.

I was aware and afraid of that. Previously, I added this output only to
"explain analyze", and (as an quick, interim implementation) changed various
tests to use analyze, and memory only shown in "verbose" mode. But as Tomas
pointed out, that's consistent with what's done elsewhere.

So is the solution to show stats only during explain ANALYZE ?

Or ... I have a patch to create a new explain(MACHINE) option to allow more
stable output, by avoiding Memory/Disk. That doesn't attempt to make all
"explain analyze" output stable - there's other issues, I think mostly related
to parallel workers (see 4ea03f3f, 13e8b2ee). But does allow retiring
explain_sq_limit and explain_parallel_sort_stats. I'm including my patch to
show what I mean, but I didn't enable it for hashtable "Buckets:". I guess in
either case, the tests shouldn't be included.

--
Justin

Attachments:

v7-0007-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From c74d35e2fb8f6b9a5f59a26fcda37846a4d677e8 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v7 7/9] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 867da6eebf..9aa2941457 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1538,8 +1538,7 @@ lookup_hash_entry(AggState *aggstate, uint32 hash)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  */
-- 
2.17.0

v7-0008-Add-explain-MACHINE.patchtext/x-diff; charset=us-asciiDownload
From 15c30ee58d899013af98bf9f7397fd4ca543d797 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 22 Feb 2020 18:45:22 -0600
Subject: [PATCH v7 8/9] Add explain(MACHINE)...

..to allow regression tests with stable output, by using
explain(ANALYZE,MACHINE OFF).

Note, this does *not* attempt to handle variations in "Workers Launched", or
any of the other bits which would also need to be handled for stable test
output across all machines.
---
 src/backend/commands/explain.c                | 68 +++++++++++++------
 src/include/commands/explain.h                |  1 +
 src/test/regress/expected/select_parallel.out | 32 +++------
 src/test/regress/expected/subselect.out       | 21 ++----
 src/test/regress/sql/select_parallel.sql      | 21 ++----
 src/test/regress/sql/subselect.sql            | 17 +----
 6 files changed, 70 insertions(+), 90 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index cef19c684b..9c7a243a78 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -166,6 +166,7 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 	ListCell   *lc;
 	bool		timing_set = false;
 	bool		summary_set = false;
+	bool		machine_set = false;
 
 	/* Parse options list. */
 	foreach(lc, stmt->options)
@@ -192,6 +193,11 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 			summary_set = true;
 			es->summary = defGetBoolean(opt);
 		}
+		else if (strcmp(opt->defname, "machine") == 0)
+		{
+			machine_set = true;
+			es->machine = defGetBoolean(opt);
+		}
 		else if (strcmp(opt->defname, "format") == 0)
 		{
 			char	   *p = defGetString(opt);
@@ -233,9 +239,18 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("EXPLAIN option TIMING requires ANALYZE")));
 
+	/* check that memory is used with EXPLAIN ANALYZE */
+	if (es->machine && !es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN option MEMORY requires ANALYZE")));
+
 	/* if the summary was not set explicitly, set default value */
 	es->summary = (summary_set) ? es->summary : es->analyze;
 
+	/* if the memory option was not set explicitly, set default value */
+	es->machine = (machine_set) ? es->machine : es->analyze;
+
 	/*
 	 * Parse analysis was done already, but we still have to run the rule
 	 * rewriter.  We do not do AcquireRewriteLocks: we assume the query either
@@ -1716,7 +1731,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
-			if (es->analyze)
+			if (es->analyze) // && es->machine ?
 				ExplainPropertyFloat("Heap Fetches", NULL,
 									 planstate->instrument->ntuples2, 0, es);
 			break;
@@ -2688,8 +2703,13 @@ show_sort_info(SortState *sortstate, ExplainState *es)
 		if (es->format == EXPLAIN_FORMAT_TEXT)
 		{
 			ExplainIndentText(es);
-			appendStringInfo(es->str, "Sort Method: %s  %s: %ldkB\n",
-							 sortMethod, spaceType, spaceUsed);
+			appendStringInfo(es->str, "Sort Method: %s",
+							 sortMethod);
+			if (es->machine)
+				appendStringInfo(es->str, "  %s: %ldkB",
+							 spaceType, spaceUsed);
+			appendStringInfoString(es->str, "\n");
+
 		}
 		else
 		{
@@ -2733,8 +2753,11 @@ show_sort_info(SortState *sortstate, ExplainState *es)
 			{
 				ExplainIndentText(es);
 				appendStringInfo(es->str,
-								 "Sort Method: %s  %s: %ldkB\n",
-								 sortMethod, spaceType, spaceUsed);
+								 "Sort Method: %s",
+								 sortMethod);
+				if (es->machine)
+					appendStringInfo(es->str, "  %s: %ldkB", spaceType, spaceUsed);
+				appendStringInfoString(es->str, "\n");
 			}
 			else
 			{
@@ -2829,25 +2852,26 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 			ExplainPropertyInteger("Peak Memory Usage", "kB",
 								   spacePeakKb, es);
 		}
-		else if (hinstrument.nbatch_original != hinstrument.nbatch ||
-				 hinstrument.nbuckets_original != hinstrument.nbuckets)
+		else
 		{
 			ExplainIndentText(es);
-			appendStringInfo(es->str,
-							 "Buckets: %d (originally %d)  Batches: %d (originally %d)  Memory Usage: %ldkB\n",
+			if (hinstrument.nbatch_original != hinstrument.nbatch ||
+				 hinstrument.nbuckets_original != hinstrument.nbuckets)
+				appendStringInfo(es->str,
+							 "Buckets: %d (originally %d)  Batches: %d (originally %d)",
 							 hinstrument.nbuckets,
 							 hinstrument.nbuckets_original,
 							 hinstrument.nbatch,
-							 hinstrument.nbatch_original,
-							 spacePeakKb);
-		}
-		else
-		{
-			ExplainIndentText(es);
-			appendStringInfo(es->str,
-							 "Buckets: %d  Batches: %d  Memory Usage: %ldkB\n",
-							 hinstrument.nbuckets, hinstrument.nbatch,
-							 spacePeakKb);
+							 hinstrument.nbatch_original);
+			else
+				appendStringInfo(es->str,
+							 "Buckets: %d  Batches: %d",
+							 hinstrument.nbuckets, hinstrument.nbatch);
+
+			if (es->machine)
+				appendStringInfo(es->str, "  Memory Usage: %ldkB", spacePeakKb);
+
+			appendStringInfoChar(es->str, '\n');
 		}
 	}
 }
@@ -2861,6 +2885,9 @@ show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es)
 	size_t	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
 		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
 
+	// if (!es->machine)
+		// return ;
+
 	if (es->format != EXPLAIN_FORMAT_TEXT)
 	{
 		ExplainPropertyInteger("Hash Buckets", NULL,
@@ -2906,6 +2933,9 @@ show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es)
 static void
 show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 {
+	if (!es->machine)
+		return;
+
 	if (es->format != EXPLAIN_FORMAT_TEXT)
 	{
 		ExplainPropertyInteger("Exact Heap Blocks", NULL,
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 54f6240e5e..05ec318363 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -45,6 +45,7 @@ typedef struct ExplainState
 	bool		timing;			/* print detailed node timing */
 	bool		summary;		/* print total planning and execution timing */
 	bool		settings;		/* print modified settings */
+	bool		machine;		/* print memory/disk and other machine-specific output */
 	ExplainFormat format;		/* output format */
 	/* state for output formatting --- not reset for each new plan tree */
 	int			indent;			/* current indentation level */
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 2b8a253c79..dcbf4b385f 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -569,24 +569,11 @@ explain (analyze, timing off, summary off, costs off)
 
 alter table tenk2 reset (parallel_workers);
 reset work_mem;
-create function explain_parallel_sort_stats() returns setof text
-language plpgsql as
-$$
-declare ln text;
-begin
-    for ln in
-        explain (analyze, timing off, summary off, costs off)
-          select * from
+explain (analyze, timing off, summary off, costs off, machine off)
+select * from
           (select ten from tenk1 where ten < 100 order by ten) ss
-          right join (values (1),(2),(3)) v(x) on true
-    loop
-        ln := regexp_replace(ln, 'Memory: \S*',  'Memory: xxx');
-        return next ln;
-    end loop;
-end;
-$$;
-select * from explain_parallel_sort_stats();
-                       explain_parallel_sort_stats                        
+          right join (values (1),(2),(3)) v(x) on true;
+                                QUERY PLAN                                
 --------------------------------------------------------------------------
  Nested Loop Left Join (actual rows=30000 loops=1)
    ->  Values Scan on "*VALUES*" (actual rows=3 loops=1)
@@ -595,11 +582,11 @@ select * from explain_parallel_sort_stats();
          Workers Launched: 4
          ->  Sort (actual rows=2000 loops=15)
                Sort Key: tenk1.ten
-               Sort Method: quicksort  Memory: xxx
-               Worker 0:  Sort Method: quicksort  Memory: xxx
-               Worker 1:  Sort Method: quicksort  Memory: xxx
-               Worker 2:  Sort Method: quicksort  Memory: xxx
-               Worker 3:  Sort Method: quicksort  Memory: xxx
+               Sort Method: quicksort
+               Worker 0:  Sort Method: quicksort
+               Worker 1:  Sort Method: quicksort
+               Worker 2:  Sort Method: quicksort
+               Worker 3:  Sort Method: quicksort
                ->  Parallel Seq Scan on tenk1 (actual rows=2000 loops=15)
                      Filter: (ten < 100)
 (14 rows)
@@ -610,7 +597,6 @@ reset enable_mergejoin;
 reset enable_material;
 reset effective_io_concurrency;
 drop table bmscantest;
-drop function explain_parallel_sort_stats();
 -- test parallel merge join path.
 set enable_hashjoin to off;
 set enable_nestloop to off;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index e4a79170d8..899c2b82ca 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -1310,27 +1310,15 @@ insert into sq_limit values
     (6, 2, 2),
     (7, 3, 3),
     (8, 4, 4);
-create function explain_sq_limit() returns setof text language plpgsql as
-$$
-declare ln text;
-begin
-    for ln in
-        explain (analyze, summary off, timing off, costs off)
-        select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3
-    loop
-        ln := regexp_replace(ln, 'Memory: \S*',  'Memory: xxx');
-        return next ln;
-    end loop;
-end;
-$$;
-select * from explain_sq_limit();
-                        explain_sq_limit                        
+explain (analyze, summary off, timing off, costs off, machine off)
+        select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3;
+                           QUERY PLAN                           
 ----------------------------------------------------------------
  Limit (actual rows=3 loops=1)
    ->  Subquery Scan on x (actual rows=3 loops=1)
          ->  Sort (actual rows=3 loops=1)
                Sort Key: sq_limit.c1, sq_limit.pk
-               Sort Method: top-N heapsort  Memory: xxx
+               Sort Method: top-N heapsort
                ->  Seq Scan on sq_limit (actual rows=8 loops=1)
 (6 rows)
 
@@ -1343,6 +1331,7 @@ select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3;
 (3 rows)
 
 drop function explain_sq_limit();
+ERROR:  function explain_sq_limit() does not exist
 drop table sq_limit;
 --
 -- Ensure that backward scan direction isn't propagated into
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 11e7735533..5be2fef2d3 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -223,23 +223,11 @@ explain (analyze, timing off, summary off, costs off)
 alter table tenk2 reset (parallel_workers);
 
 reset work_mem;
-create function explain_parallel_sort_stats() returns setof text
-language plpgsql as
-$$
-declare ln text;
-begin
-    for ln in
-        explain (analyze, timing off, summary off, costs off)
-          select * from
+
+explain (analyze, timing off, summary off, costs off, machine off)
+select * from
           (select ten from tenk1 where ten < 100 order by ten) ss
-          right join (values (1),(2),(3)) v(x) on true
-    loop
-        ln := regexp_replace(ln, 'Memory: \S*',  'Memory: xxx');
-        return next ln;
-    end loop;
-end;
-$$;
-select * from explain_parallel_sort_stats();
+          right join (values (1),(2),(3)) v(x) on true;
 
 reset enable_indexscan;
 reset enable_hashjoin;
@@ -247,7 +235,6 @@ reset enable_mergejoin;
 reset enable_material;
 reset effective_io_concurrency;
 drop table bmscantest;
-drop function explain_parallel_sort_stats();
 
 -- test parallel merge join path.
 set enable_hashjoin to off;
diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql
index 893d8d0f62..086c974a73 100644
--- a/src/test/regress/sql/subselect.sql
+++ b/src/test/regress/sql/subselect.sql
@@ -701,21 +701,8 @@ insert into sq_limit values
     (7, 3, 3),
     (8, 4, 4);
 
-create function explain_sq_limit() returns setof text language plpgsql as
-$$
-declare ln text;
-begin
-    for ln in
-        explain (analyze, summary off, timing off, costs off)
-        select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3
-    loop
-        ln := regexp_replace(ln, 'Memory: \S*',  'Memory: xxx');
-        return next ln;
-    end loop;
-end;
-$$;
-
-select * from explain_sq_limit();
+explain (analyze, summary off, timing off, costs off, machine off)
+        select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3;
 
 select * from (select pk,c2 from sq_limit order by c1,pk) as x limit 3;
 
-- 
2.17.0

v7-0001-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From 2f6e6fda6352473da03f819eb32262a0501d746b Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v7 1/9] explain to show tuplehash bucket and memory stats..

Note that hashed SubPlan and recursiveUnion aren't affected in explain output,
probably since hashtables aren't allocated at that point.

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 .../postgres_fdw/expected/postgres_fdw.out    |  56 +++++--
 src/backend/commands/explain.c                | 137 +++++++++++++++--
 src/backend/executor/execGrouping.c           |  28 ++++
 src/backend/executor/nodeAgg.c                |  11 ++
 src/backend/executor/nodeRecursiveunion.c     |   3 +
 src/backend/executor/nodeSetOp.c              |   1 +
 src/backend/executor/nodeSubplan.c            |   3 +
 src/include/executor/executor.h               |   1 +
 src/include/nodes/execnodes.h                 |   9 ++
 src/test/regress/expected/aggregates.out      |  36 +++--
 src/test/regress/expected/groupingsets.out    |  64 ++++++--
 src/test/regress/expected/join.out            |   3 +-
 src/test/regress/expected/matview.out         |   9 +-
 .../regress/expected/partition_aggregate.out  | 145 ++++++++++++++----
 src/test/regress/expected/partition_join.out  |  13 +-
 src/test/regress/expected/pg_lsn.out          |   3 +-
 src/test/regress/expected/select_distinct.out |   3 +-
 src/test/regress/expected/select_parallel.out |  11 +-
 src/test/regress/expected/subselect.out       |   9 +-
 src/test/regress/expected/tablesample.out     |   3 +-
 src/test/regress/expected/union.out           |  15 +-
 src/test/regress/expected/window.out          |   3 +-
 src/test/regress/expected/write_parallel.out  |  16 +-
 23 files changed, 474 insertions(+), 108 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62c2697920..2ddae83178 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2086,9 +2086,11 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
          ->  HashAggregate
                Output: t1.c1, avg((t1.c1 + t2.c1))
                Group Key: t1.c1
+               Buckets: 256
                ->  HashAggregate
                      Output: t1.c1, t2.c1
                      Group Key: t1.c1, t2.c1
+                     Buckets: 4096
                      ->  Append
                            ->  Foreign Scan
                                  Output: t1.c1, t2.c1
@@ -2098,7 +2100,7 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                                  Output: t1_1.c1, t2_1.c1
                                  Relations: (public.ft1 t1_1) INNER JOIN (public.ft2 t2_1)
                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
-(20 rows)
+(22 rows)
 
 SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
  t1c1 |         avg          
@@ -2129,11 +2131,12 @@ SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
          ->  HashAggregate
                Output: t2.c1, t3.c1
                Group Key: t2.c1, t3.c1
+               Buckets: 2
                ->  Foreign Scan
                      Output: t2.c1, t3.c1
                      Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
-(13 rows)
+(14 rows)
 
 SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  C 1 
@@ -2610,10 +2613,11 @@ select c2 * (random() <= 1)::int as c2 from ft2 group by c2 * (random() <= 1)::i
    ->  HashAggregate
          Output: ((c2 * ((random() <= '1'::double precision))::integer))
          Group Key: (ft2.c2 * ((random() <= '1'::double precision))::integer)
+         Buckets: 2
          ->  Foreign Scan on public.ft2
                Output: (c2 * ((random() <= '1'::double precision))::integer)
                Remote SQL: SELECT c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 -- GROUP BY clause in various forms, cardinal, alias and constant expression
 explain (verbose, costs off)
@@ -2713,11 +2717,12 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
    ->  HashAggregate
          Output: sum(c1), c2
          Group Key: ft1.c2
+         Buckets: 16
          Filter: (avg((ft1.c1 * ((random() <= '1'::double precision))::integer)) > '100'::numeric)
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(10 rows)
+(11 rows)
 
 -- Remote aggregate in combination with a local Param (for the output
 -- of an initplan) can be trouble, per bug #15781
@@ -2963,10 +2968,11 @@ select sum(c1) filter (where (c1 / c1) * random() <= 1) from ft1 group by c2 ord
    ->  HashAggregate
          Output: sum(c1) FILTER (WHERE ((((c1 / c1))::double precision * random()) <= '1'::double precision)), c2
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 explain (verbose, costs off)
 select sum(c2) filter (where c2 in (select c2 from ft1 where c2 < 5)) from ft1;
@@ -3229,6 +3235,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
    ->  HashAggregate
          Output: count(*), x.b
          Group Key: x.b
+         Buckets: 16
          ->  Hash Join
                Output: x.b
                Inner Unique: true
@@ -3244,7 +3251,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
                                  Output: ft1_1.c2, (sum(ft1_1.c1))
                                  Relations: Aggregate on (public.ft1 ft1_1)
                                  Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1
-(21 rows)
+(22 rows)
 
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
  count |   b   
@@ -3449,11 +3456,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls la
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3474,11 +3482,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3499,11 +3508,13 @@ select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) orde
    ->  HashAggregate
          Output: c2, c6, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Hash Key: ft1.c6
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c6, c1
                Remote SQL: SELECT "C 1", c2, c6 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(12 rows)
 
 select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) order by 1 nulls last, 2 nulls last;
  c2 | c6 |  sum  
@@ -3526,10 +3537,11 @@ select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nu
    ->  HashAggregate
          Output: c2, sum(c1), GROUPING(c2)
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(9 rows)
+(10 rows)
 
 select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nulls last;
  c2 |  sum  | grouping 
@@ -7147,13 +7159,14 @@ select * from bar where f1 in (select f1 from foo) for update;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for update;
  f1 | f2 
@@ -7185,13 +7198,14 @@ select * from bar where f1 in (select f1 from foo) for share;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for share;
  f1 | f2 
@@ -7222,6 +7236,7 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
@@ -7240,13 +7255,14 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(39 rows)
+(41 rows)
 
 update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
 select tableoid::regclass, * from bar order by 1,2;
@@ -8751,12 +8767,13 @@ SELECT a, sum(b), min(b), count(*) FROM pagg_tab GROUP BY a HAVING avg(b) < 22 O
    Sort Key: pagg_tab.a
    ->  HashAggregate
          Group Key: pagg_tab.a
+         Buckets: 64
          Filter: (avg(pagg_tab.b) < '22'::numeric)
          ->  Append
                ->  Foreign Scan on fpagg_tab_p1 pagg_tab_1
                ->  Foreign Scan on fpagg_tab_p2 pagg_tab_2
                ->  Foreign Scan on fpagg_tab_p3 pagg_tab_3
-(9 rows)
+(10 rows)
 
 -- Plan with partitionwise aggregates is enabled
 SET enable_partitionwise_aggregate TO true;
@@ -8799,6 +8816,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1.a, count(((t1.*)::pagg_tab))
                Group Key: t1.a
+               Buckets: 16
                Filter: (avg(t1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p1 t1
                      Output: t1.a, t1.*, t1.b
@@ -8806,6 +8824,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_1.a, count(((t1_1.*)::pagg_tab))
                Group Key: t1_1.a
+               Buckets: 16
                Filter: (avg(t1_1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p2 t1_1
                      Output: t1_1.a, t1_1.*, t1_1.b
@@ -8813,11 +8832,12 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_2.a, count(((t1_2.*)::pagg_tab))
                Group Key: t1_2.a
+               Buckets: 16
                Filter: (avg(t1_2.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p3 t1_2
                      Output: t1_2.a, t1_2.*, t1_2.b
                      Remote SQL: SELECT a, b, c FROM public.pagg_tab_p3
-(25 rows)
+(28 rows)
 
 SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
  a  | count 
@@ -8839,18 +8859,22 @@ SELECT b, avg(a), max(a), count(*) FROM pagg_tab GROUP BY b HAVING sum(a) < 700
    Sort Key: pagg_tab.b
    ->  Finalize HashAggregate
          Group Key: pagg_tab.b
+         Buckets: 64
          Filter: (sum(pagg_tab.a) < 700)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p1 pagg_tab
                ->  Partial HashAggregate
                      Group Key: pagg_tab_1.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p2 pagg_tab_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_2.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p3 pagg_tab_2
-(15 rows)
+(19 rows)
 
 -- ===================================================================
 -- access rights and superuser
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d901dc4a50..9e522f7971 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -86,12 +87,14 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *aggstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_info(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors,
+								   HashTableInstrumentation *inst,
+								   ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -104,6 +107,7 @@ static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
+static void show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1489,6 +1493,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1886,6 +1891,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, es);
+				break;
+			}
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2262,24 +2281,31 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
+			show_grouping_sets(astate, plan, ancestors, es);
 		else
+		{
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes <= 1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
+	int			setno = 0;
 
 	/* Set up deparsing context */
 	context = set_deparse_context_plan(es->deparse_cxt,
@@ -2289,27 +2315,41 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
+			aggstate->num_hashes ?
+			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			es);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		HashTableInstrumentation *inst = NULL;
+
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+		{
+			Assert(setno < aggstate->num_hashes);
+			inst = &aggstate->perhash[setno++].hashtable->instrument;
+		}
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		show_grouping_set_info(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors,
+							   inst, es);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
+/* Show keys and any hash instrumentation for a grouping set */
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_info(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, HashTableInstrumentation *inst,
+					   ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2373,6 +2413,10 @@ show_grouping_set_keys(PlanState *planstate,
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
 
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, es);
+
 	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
 		es->indent--;
 
@@ -2769,6 +2813,54 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 	}
 }
 
+/*
+ * Show hash bucket stats and (optionally) memory.
+ */
+static void
+show_tuplehash_info(HashTableInstrumentation *inst, ExplainState *es)
+{
+	size_t	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
+
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %lld (originally %lld)",
+						(long long)inst->nbuckets,
+						(long long)inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %lld",
+						(long long)inst->nbuckets);
+		}
+
+		if (es->analyze)
+			appendStringInfo(es->str,
+					"  Memory Usage: hashtable: %lldkB, tuples: %lldkB",
+					(long long)spacePeakKb_hash, (long long)spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+	}
+}
+
 /*
  * If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
  */
@@ -3436,6 +3528,29 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashtable->instrument, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (sps->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..844dd3ba86 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,6 +188,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -203,6 +204,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -281,9 +283,35 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial)
+	{
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
+			sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+		/* hashtable->entrysize includes additionalsize */
+		hashtable->instrument.space_peak_hash = Max(
+			hashtable->instrument.space_peak_hash,
+			hashtable->hashtab->size * sizeof(TupleHashEntryData));
+
+		hashtable->instrument.space_peak_tuples = Max(
+			hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members * hashtable->entrysize);
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 7aebb247d8..e5aa0629a7 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1710,6 +1710,7 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
+				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
 									   &aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
@@ -1907,6 +1908,13 @@ agg_retrieve_direct(AggState *aggstate)
 						}
 					}
 				}
+
+				if (aggstate->aggstrategy == AGG_MIXED &&
+						aggstate->current_phase == 1)
+				{
+					for (int i = 0; i < aggstate->num_hashes; i++)
+						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+				}
 			}
 
 			/*
@@ -1981,6 +1989,9 @@ agg_fill_hash_table(AggState *aggstate)
 	}
 
 	aggstate->table_filled = true;
+	for (int i = 0; i < aggstate->num_hashes; i++)
+		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
 	ResetTupleHashIterator(aggstate->perhash[0].hashtable,
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a1ed..93272c28b1 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a41a..9c0e0ab96e 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 298b7757f5..22c32612ba 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 94890512dc..f4f2ede207 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cd3ddf781f..cfeada5f1d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,6 +691,14 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+typedef struct HashTableInstrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;	/* peak memory usage in bytes */
+	size_t	space_peak_tuples;	/* peak memory usage in bytes */
+} HashTableInstrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -709,6 +717,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b150..b173b32cab 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -517,10 +517,11 @@ order by 1, 2;
          ->  HashAggregate
                Output: s2.s2, sum((s1.s1 + s2.s2))
                Group Key: s2.s2
+               Buckets: 4
                ->  Function Scan on pg_catalog.generate_series s2
                      Output: s2.s2
                      Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)
 
 select s1, s2, sm
 from generate_series(1, 3) s1,
@@ -556,10 +557,11 @@ select array(select sum(x+y) s
            ->  HashAggregate
                  Output: sum((x.x + y.y)), y.y
                  Group Key: y.y
+                 Buckets: 4
                  ->  Function Scan on pg_catalog.generate_series y
                        Output: y.y
                        Function Call: generate_series(1, 3)
-(13 rows)
+(14 rows)
 
 select array(select sum(x+y) s
             from generate_series(1,3) y group by y order by s)
@@ -872,12 +874,13 @@ explain (costs off)
 ---------------------------------------------------------------------
  HashAggregate
    Group Key: $0
+   Buckets: 2
    InitPlan 1 (returns $0)
      ->  Limit
            ->  Index Only Scan Backward using tenk1_unique2 on tenk1
                  Index Cond: (unique2 IS NOT NULL)
    ->  Result
-(7 rows)
+(8 rows)
 
 select distinct max(unique2) from tenk1;
  max  
@@ -1096,8 +1099,9 @@ explain (costs off) select * from t1 group by a,b,c,d;
 ----------------------
  HashAggregate
    Group Key: a, b
+   Buckets: 256
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 -- No removal can happen if the complete PK is not present in GROUP BY
 explain (costs off) select a,c from t1 group by a,c,d;
@@ -1105,8 +1109,9 @@ explain (costs off) select a,c from t1 group by a,c,d;
 ----------------------
  HashAggregate
    Group Key: a, c, d
+   Buckets: 256
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 -- Test removal across multiple relations
 explain (costs off) select *
@@ -1116,12 +1121,13 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
 ------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.y
+   Buckets: 128
    ->  Hash Join
          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
          ->  Seq Scan on t2
          ->  Hash
                ->  Seq Scan on t1
-(7 rows)
+(8 rows)
 
 -- Test case where t1 can be optimized but not t2
 explain (costs off) select t1.*,t2.x,t2.z
@@ -1131,12 +1137,13 @@ group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
 ------------------------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t2.x, t2.z
+   Buckets: 128
    ->  Hash Join
          Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
          ->  Seq Scan on t2
          ->  Hash
                ->  Seq Scan on t1
-(7 rows)
+(8 rows)
 
 -- Cannot optimize when PK is deferrable
 explain (costs off) select * from t3 group by a,b,c;
@@ -1144,8 +1151,9 @@ explain (costs off) select * from t3 group by a,b,c;
 ----------------------
  HashAggregate
    Group Key: a, b, c
+   Buckets: 256
    ->  Seq Scan on t3
-(3 rows)
+(4 rows)
 
 create temp table t1c () inherits (t1);
 -- Ensure we don't remove any columns when t1 has a child table
@@ -1154,10 +1162,11 @@ explain (costs off) select * from t1 group by a,b,c,d;
 -------------------------------------
  HashAggregate
    Group Key: t1.a, t1.b, t1.c, t1.d
+   Buckets: 256
    ->  Append
          ->  Seq Scan on t1 t1_1
          ->  Seq Scan on t1c t1_2
-(5 rows)
+(6 rows)
 
 -- Okay to remove columns if we're only querying the parent.
 explain (costs off) select * from only t1 group by a,b,c,d;
@@ -1165,8 +1174,9 @@ explain (costs off) select * from only t1 group by a,b,c,d;
 ----------------------
  HashAggregate
    Group Key: a, b
+   Buckets: 2
    ->  Seq Scan on t1
-(3 rows)
+(4 rows)
 
 create temp table p_t1 (
   a int,
@@ -1183,10 +1193,11 @@ explain (costs off) select * from p_t1 group by a,b,c,d;
 --------------------------------
  HashAggregate
    Group Key: p_t1.a, p_t1.b
+   Buckets: 512
    ->  Append
          ->  Seq Scan on p_t1_1
          ->  Seq Scan on p_t1_2
-(5 rows)
+(6 rows)
 
 drop table t1 cascade;
 NOTICE:  drop cascades to table t1c
@@ -2354,6 +2365,7 @@ explain (costs off)
    ->  Hash
          ->  HashAggregate
                Group Key: onek.twothousand, onek.twothousand
+               Buckets: 256
                ->  Seq Scan on onek
-(8 rows)
+(9 rows)
 
diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out
index c1f802c88a..be386731ce 100644
--- a/src/test/regress/expected/groupingsets.out
+++ b/src/test/regress/expected/groupingsets.out
@@ -974,9 +974,11 @@ explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
    ->  HashAggregate
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          ->  Values Scan on "*VALUES*"
-(6 rows)
+(8 rows)
 
 select a, b, grouping(a,b), sum(v), count(*), max(v)
   from gstest1 group by cube(a,b) order by 3,1,2;
@@ -1008,11 +1010,14 @@ explain (costs off) select a, b, grouping(a,b), sum(v), count(*), max(v)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), "*VALUES*".column1, "*VALUES*".column2
    ->  MixedAggregate
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: "*VALUES*".column1
+         Buckets: 16
          Hash Key: "*VALUES*".column2
+         Buckets: 16
          Group Key: ()
          ->  Values Scan on "*VALUES*"
-(8 rows)
+(11 rows)
 
 -- shouldn't try and hash
 explain (costs off)
@@ -1071,11 +1076,12 @@ explain (costs off)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
    ->  MixedAggregate
          Hash Key: unsortable_col
+         Buckets: 256
          Group Key: unhashable_col
          ->  Sort
                Sort Key: unhashable_col
                ->  Seq Scan on gstest4
-(8 rows)
+(9 rows)
 
 select unhashable_col, unsortable_col,
        grouping(unhashable_col, unsortable_col),
@@ -1114,11 +1120,12 @@ explain (costs off)
    Sort Key: (GROUPING(unhashable_col, unsortable_col)), (sum(v))
    ->  MixedAggregate
          Hash Key: v, unsortable_col
+         Buckets: 256
          Group Key: v, unhashable_col
          ->  Sort
                Sort Key: v, unhashable_col
                ->  Seq Scan on gstest4
-(8 rows)
+(9 rows)
 
 -- empty input: first is 0 rows, second 1, third 3 etc.
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),a);
@@ -1132,9 +1139,11 @@ explain (costs off)
 --------------------------------
  HashAggregate
    Hash Key: a, b
+   Buckets: 256
    Hash Key: a
+   Buckets: 256
    ->  Seq Scan on gstest_empty
-(4 rows)
+(6 rows)
 
 select a, b, sum(v), count(*) from gstest_empty group by grouping sets ((a,b),());
  a | b | sum | count 
@@ -1156,11 +1165,12 @@ explain (costs off)
 --------------------------------
  MixedAggregate
    Hash Key: a, b
+   Buckets: 256
    Group Key: ()
    Group Key: ()
    Group Key: ()
    ->  Seq Scan on gstest_empty
-(6 rows)
+(7 rows)
 
 select sum(v), count(*) from gstest_empty group by grouping sets ((),(),());
  sum | count 
@@ -1201,9 +1211,11 @@ explain (costs off)
 ---------------------------
  HashAggregate
    Hash Key: a, b
+   Buckets: 2
    Hash Key: a, c
+   Buckets: 2
    ->  Seq Scan on gstest3
-(4 rows)
+(6 rows)
 
 -- simple rescan tests
 select a, b, sum(v.x)
@@ -1230,11 +1242,13 @@ explain (costs off)
    Sort Key: (sum("*VALUES*".column1)), gstest_data.a, gstest_data.b
    ->  HashAggregate
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(8 rows)
+(10 rows)
 
 select *
   from (values (1),(2)) v(x),
@@ -1286,10 +1300,13 @@ explain (costs off)
    Sort Key: (GROUPING("*VALUES*".column1, "*VALUES*".column2)), (max("*VALUES*".column3))
    ->  HashAggregate
          Hash Key: "*VALUES*".column1, "*VALUES*".column2
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 1), ("*VALUES*".column2 + 1)
+         Buckets: 16
          Hash Key: ("*VALUES*".column1 + 2), ("*VALUES*".column2 + 2)
+         Buckets: 16
          ->  Values Scan on "*VALUES*"
-(7 rows)
+(10 rows)
 
 select a, b, sum(c), sum(sum(c)) over (order by a,b) as rsum
   from gstest2 group by cube (a,b) order by rsum, a, b;
@@ -1317,11 +1334,14 @@ explain (costs off)
                Sort Key: a, b
                ->  MixedAggregate
                      Hash Key: a, b
+                     Buckets: 256
                      Hash Key: a
+                     Buckets: 256
                      Hash Key: b
+                     Buckets: 256
                      Group Key: ()
                      ->  Seq Scan on gstest2
-(11 rows)
+(14 rows)
 
 select a, b, sum(v.x)
   from (values (1),(2)) v(x), gstest_data(v.x)
@@ -1352,13 +1372,16 @@ explain (costs off)
    Sort Key: gstest_data.a, gstest_data.b
    ->  MixedAggregate
          Hash Key: gstest_data.a, gstest_data.b
+         Buckets: 256
          Hash Key: gstest_data.a
+         Buckets: 256
          Hash Key: gstest_data.b
+         Buckets: 256
          Group Key: ()
          ->  Nested Loop
                ->  Values Scan on "*VALUES*"
                ->  Function Scan on gstest_data
-(10 rows)
+(13 rows)
 
 -- Verify that we correctly handle the child node returning a
 -- non-minimal slot, which happens if the input is pre-sorted,
@@ -1553,9 +1576,13 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
@@ -1564,7 +1591,7 @@ explain (costs off)
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(13 rows)
+(17 rows)
 
 explain (costs off)
   select unique1,
@@ -1576,14 +1603,18 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Group Key: unique1
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(9 rows)
+(13 rows)
 
 set work_mem = '384kB';
 explain (costs off)
@@ -1596,17 +1627,22 @@ explain (costs off)
 -------------------------------
  MixedAggregate
    Hash Key: two
+   Buckets: 2
    Hash Key: four
+   Buckets: 4
    Hash Key: ten
+   Buckets: 16
    Hash Key: hundred
+   Buckets: 128
    Hash Key: thousand
+   Buckets: 2048
    Group Key: unique1
    Sort Key: twothousand
      Group Key: twothousand
    ->  Sort
          Sort Key: unique1
          ->  Seq Scan on tenk1
-(12 rows)
+(17 rows)
 
 -- check collation-sensitive matching between grouping expressions
 -- (similar to a check for aggregates, but there are additional code
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 761376b007..9f07501369 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -6199,6 +6199,7 @@ where exists (select 1 from tenk1 t3
          ->  HashAggregate
                Output: t3.thousand, t3.tenthous
                Group Key: t3.thousand, t3.tenthous
+               Buckets: 16384
                ->  Index Only Scan using tenk1_thous_tenthous on public.tenk1 t3
                      Output: t3.thousand, t3.tenthous
          ->  Hash
@@ -6209,7 +6210,7 @@ where exists (select 1 from tenk1 t3
    ->  Index Only Scan using tenk1_hundred on public.tenk1 t2
          Output: t2.hundred
          Index Cond: (t2.hundred = t3.tenthous)
-(18 rows)
+(19 rows)
 
 -- ... unless it actually is unique
 create table j3 as select unique1, tenthous from onek;
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index d0121a7b0b..ca8573ac6d 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -23,8 +23,9 @@ EXPLAIN (costs off)
 ----------------------------
  HashAggregate
    Group Key: type
+   Buckets: 256
    ->  Seq Scan on mvtest_t
-(3 rows)
+(4 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tm AS SELECT type, sum(amt) AS totamt FROM mvtest_t GROUP BY type WITH NO DATA;
 SELECT relispopulated FROM pg_class WHERE oid = 'mvtest_tm'::regclass;
@@ -61,8 +62,9 @@ EXPLAIN (costs off)
    Sort Key: mvtest_t.type
    ->  HashAggregate
          Group Key: mvtest_t.type
+         Buckets: 256
          ->  Seq Scan on mvtest_t
-(5 rows)
+(6 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tvm AS SELECT * FROM mvtest_tv ORDER BY type;
 SELECT * FROM mvtest_tvm;
@@ -85,8 +87,9 @@ EXPLAIN (costs off)
  Aggregate
    ->  HashAggregate
          Group Key: mvtest_t.type
+         Buckets: 256
          ->  Seq Scan on mvtest_t
-(4 rows)
+(5 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tvvm AS SELECT * FROM mvtest_tvv;
 CREATE VIEW mvtest_tvvmv AS SELECT * FROM mvtest_tvvm;
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index fbc8d3ac6c..5939c2a128 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -27,17 +27,20 @@ SELECT c, sum(a), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY c HAVI
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab.c
+               Buckets: 4
                Filter: (avg(pagg_tab.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p1 pagg_tab
          ->  HashAggregate
                Group Key: pagg_tab_1.c
+               Buckets: 4
                Filter: (avg(pagg_tab_1.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p2 pagg_tab_1
          ->  HashAggregate
                Group Key: pagg_tab_2.c
+               Buckets: 4
                Filter: (avg(pagg_tab_2.d) < '15'::numeric)
                ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(15 rows)
+(18 rows)
 
 SELECT c, sum(a), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY c HAVING avg(d) < 15 ORDER BY 1, 2, 3;
   c   | sum  |         avg         | count | min | max 
@@ -59,18 +62,22 @@ SELECT a, sum(b), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY a HAVI
    Sort Key: pagg_tab.a, (sum(pagg_tab.b)), (avg(pagg_tab.b))
    ->  Finalize HashAggregate
          Group Key: pagg_tab.a
+         Buckets: 32
          Filter: (avg(pagg_tab.d) < '15'::numeric)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p1 pagg_tab
                ->  Partial HashAggregate
                      Group Key: pagg_tab_1.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p2 pagg_tab_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_2.a
+                     Buckets: 32
                      ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(15 rows)
+(19 rows)
 
 SELECT a, sum(b), avg(b), count(*), min(a), max(b) FROM pagg_tab GROUP BY a HAVING avg(d) < 15 ORDER BY 1, 2, 3;
  a  | sum  |         avg         | count | min | max 
@@ -95,14 +102,17 @@ SELECT a, c, count(*) FROM pagg_tab GROUP BY a, c;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.a, pagg_tab.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.a, pagg_tab_1.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.a, pagg_tab_2.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Check with multiple columns in GROUP BY, order in GROUP BY is reversed
 EXPLAIN (COSTS OFF)
@@ -112,14 +122,17 @@ SELECT a, c, count(*) FROM pagg_tab GROUP BY c, a;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.c, pagg_tab.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.c, pagg_tab_1.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.c, pagg_tab_2.a
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Check with multiple columns in GROUP BY, order in target-list is reversed
 EXPLAIN (COSTS OFF)
@@ -129,14 +142,17 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c;
  Append
    ->  HashAggregate
          Group Key: pagg_tab.a, pagg_tab.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p1 pagg_tab
    ->  HashAggregate
          Group Key: pagg_tab_1.a, pagg_tab_1.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p2 pagg_tab_1
    ->  HashAggregate
          Group Key: pagg_tab_2.a, pagg_tab_2.c
+         Buckets: 128
          ->  Seq Scan on pagg_tab_p3 pagg_tab_2
-(10 rows)
+(13 rows)
 
 -- Test when input relation for grouping is dummy
 EXPLAIN (COSTS OFF)
@@ -145,9 +161,10 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
 --------------------------------
  HashAggregate
    Group Key: c
+   Buckets: 2
    ->  Result
          One-Time Filter: false
-(4 rows)
+(5 rows)
 
 SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c;
  c | sum 
@@ -341,12 +358,13 @@ SELECT c, sum(a) FROM pagg_tab GROUP BY rollup(c) ORDER BY 1, 2;
    Sort Key: pagg_tab.c, (sum(pagg_tab.a))
    ->  MixedAggregate
          Hash Key: pagg_tab.c
+         Buckets: 16
          Group Key: ()
          ->  Append
                ->  Seq Scan on pagg_tab_p1 pagg_tab_1
                ->  Seq Scan on pagg_tab_p2 pagg_tab_2
                ->  Seq Scan on pagg_tab_p3 pagg_tab_3
-(9 rows)
+(10 rows)
 
 -- ORDERED SET within the aggregate.
 -- Full aggregation; since all the rows that belong to the same group come
@@ -418,6 +436,7 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
    ->  Append
          ->  HashAggregate
                Group Key: t1.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t1.x = t2.y)
                      ->  Seq Scan on pagg_tab1_p1 t1
@@ -425,6 +444,7 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p1 t2
          ->  HashAggregate
                Group Key: t1_1.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t1_1.x = t2_1.y)
                      ->  Seq Scan on pagg_tab1_p2 t1_1
@@ -432,12 +452,13 @@ SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p2 t2_1
          ->  HashAggregate
                Group Key: t1_2.x
+               Buckets: 8
                ->  Hash Join
                      Hash Cond: (t2_2.y = t1_2.x)
                      ->  Seq Scan on pagg_tab2_p3 t2_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 t1_2
-(24 rows)
+(27 rows)
 
 SELECT t1.x, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2.y GROUP BY t1.x ORDER BY 1, 2, 3;
  x  | sum  | count 
@@ -458,6 +479,7 @@ SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t
    Sort Key: t1.x, (sum(t1.y)), (count(((t1.*)::pagg_tab1)))
    ->  HashAggregate
          Group Key: t1.x
+         Buckets: 16
          ->  Hash Join
                Hash Cond: (t1.x = t2.y)
                ->  Append
@@ -469,7 +491,7 @@ SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t
                            ->  Seq Scan on pagg_tab2_p1 t2_1
                            ->  Seq Scan on pagg_tab2_p2 t2_2
                            ->  Seq Scan on pagg_tab2_p3 t2_3
-(15 rows)
+(16 rows)
 
 SELECT t1.x, sum(t1.y), count(t1) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2.y GROUP BY t1.x ORDER BY 1, 2, 3;
  x  | sum  | count 
@@ -491,6 +513,7 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
    ->  Append
          ->  HashAggregate
                Group Key: t2.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t1.x = t2.y)
                      ->  Seq Scan on pagg_tab1_p1 t1
@@ -498,6 +521,7 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p1 t2
          ->  HashAggregate
                Group Key: t2_1.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t1_1.x = t2_1.y)
                      ->  Seq Scan on pagg_tab1_p2 t1_1
@@ -505,12 +529,13 @@ SELECT t2.y, sum(t1.y), count(*) FROM pagg_tab1 t1, pagg_tab2 t2 WHERE t1.x = t2
                            ->  Seq Scan on pagg_tab2_p2 t2_1
          ->  HashAggregate
                Group Key: t2_2.y
+               Buckets: 4
                ->  Hash Join
                      Hash Cond: (t2_2.y = t1_2.x)
                      ->  Seq Scan on pagg_tab2_p3 t2_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 t1_2
-(24 rows)
+(27 rows)
 
 -- When GROUP BY clause does not match; partial aggregation is performed for each partition.
 -- Also test GroupAggregate paths by disabling hash aggregates.
@@ -582,6 +607,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
          ->  Append
                ->  Partial HashAggregate
                      Group Key: b.y
+                     Buckets: 4
                      ->  Hash Left Join
                            Hash Cond: (a.x = b.y)
                            ->  Seq Scan on pagg_tab1_p1 a
@@ -589,6 +615,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
                                  ->  Seq Scan on pagg_tab2_p1 b
                ->  Partial HashAggregate
                      Group Key: b_1.y
+                     Buckets: 4
                      ->  Hash Left Join
                            Hash Cond: (a_1.x = b_1.y)
                            ->  Seq Scan on pagg_tab1_p2 a_1
@@ -596,12 +623,13 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP B
                                  ->  Seq Scan on pagg_tab2_p2 b_1
                ->  Partial HashAggregate
                      Group Key: b_2.y
+                     Buckets: 4
                      ->  Hash Right Join
                            Hash Cond: (b_2.y = a_2.x)
                            ->  Seq Scan on pagg_tab2_p3 b_2
                            ->  Hash
                                  ->  Seq Scan on pagg_tab1_p3 a_2
-(26 rows)
+(29 rows)
 
 SELECT b.y, sum(a.y) FROM pagg_tab1 a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY b.y ORDER BY 1 NULLS LAST;
  y  | sum  
@@ -625,6 +653,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
    ->  Append
          ->  HashAggregate
                Group Key: b.y
+               Buckets: 4
                ->  Hash Right Join
                      Hash Cond: (a.x = b.y)
                      ->  Seq Scan on pagg_tab1_p1 a
@@ -632,6 +661,7 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
                            ->  Seq Scan on pagg_tab2_p1 b
          ->  HashAggregate
                Group Key: b_1.y
+               Buckets: 4
                ->  Hash Right Join
                      Hash Cond: (a_1.x = b_1.y)
                      ->  Seq Scan on pagg_tab1_p2 a_1
@@ -639,12 +669,13 @@ SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP
                            ->  Seq Scan on pagg_tab2_p2 b_1
          ->  HashAggregate
                Group Key: b_2.y
+               Buckets: 4
                ->  Hash Left Join
                      Hash Cond: (b_2.y = a_2.x)
                      ->  Seq Scan on pagg_tab2_p3 b_2
                      ->  Hash
                            ->  Seq Scan on pagg_tab1_p3 a_2
-(24 rows)
+(27 rows)
 
 SELECT b.y, sum(a.y) FROM pagg_tab1 a RIGHT JOIN pagg_tab2 b ON a.x = b.y GROUP BY b.y ORDER BY 1 NULLS LAST;
  y  | sum  
@@ -674,6 +705,7 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
          ->  Append
                ->  Partial HashAggregate
                      Group Key: a.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (a.x = b.y)
                            ->  Seq Scan on pagg_tab1_p1 a
@@ -681,6 +713,7 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
                                  ->  Seq Scan on pagg_tab2_p1 b
                ->  Partial HashAggregate
                      Group Key: a_1.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (a_1.x = b_1.y)
                            ->  Seq Scan on pagg_tab1_p2 a_1
@@ -688,12 +721,13 @@ SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y G
                                  ->  Seq Scan on pagg_tab2_p2 b_1
                ->  Partial HashAggregate
                      Group Key: a_2.x
+                     Buckets: 8
                      ->  Hash Full Join
                            Hash Cond: (b_2.y = a_2.x)
                            ->  Seq Scan on pagg_tab2_p3 b_2
                            ->  Hash
                                  ->  Seq Scan on pagg_tab1_p3 a_2
-(26 rows)
+(29 rows)
 
 SELECT a.x, sum(b.x) FROM pagg_tab1 a FULL OUTER JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x ORDER BY 1 NULLS LAST;
  x  | sum  
@@ -728,6 +762,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOI
    Sort Key: pagg_tab1.x, pagg_tab2.y
    ->  HashAggregate
          Group Key: pagg_tab1.x, pagg_tab2.y
+         Buckets: 256
          ->  Hash Left Join
                Hash Cond: (pagg_tab1.x = pagg_tab2.y)
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
@@ -742,7 +777,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOI
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
                                  Filter: (y > 10)
-(18 rows)
+(19 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a LEFT JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20  GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
@@ -768,6 +803,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
    Sort Key: pagg_tab1.x, pagg_tab2.y
    ->  HashAggregate
          Group Key: pagg_tab1.x, pagg_tab2.y
+         Buckets: 256
          ->  Hash Full Join
                Hash Cond: (pagg_tab1.x = pagg_tab2.y)
                Filter: ((pagg_tab1.x > 5) OR (pagg_tab2.y < 20))
@@ -782,7 +818,7 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI
                                  Filter: (y > 10)
                            ->  Seq Scan on pagg_tab2_p3 pagg_tab2_2
                                  Filter: (y > 10)
-(18 rows)
+(19 rows)
 
 SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOIN (SELECT * FROM pagg_tab2 WHERE y > 10) b ON a.x = b.y WHERE a.x > 5 or b.y < 20 GROUP BY a.x, b.y ORDER BY 1, 2;
  x  | y  | count 
@@ -831,18 +867,22 @@ SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a HAVING avg(c) < 22
    Sort Key: pagg_tab_m.a, (sum(pagg_tab_m.b)), (avg(pagg_tab_m.c))
    ->  Finalize HashAggregate
          Group Key: pagg_tab_m.a
+         Buckets: 64
          Filter: (avg(pagg_tab_m.c) < '22'::numeric)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m_1.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_m_2.a
+                     Buckets: 16
                      ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(19 rows)
 
 SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a HAVING avg(c) < 22 ORDER BY 1, 2, 3;
  a  | sum  |         avg         | count 
@@ -865,17 +905,20 @@ SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a, (a+b)/2 HAVING su
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_m.a, ((pagg_tab_m.a + pagg_tab_m.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m.b) < 50)
                ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
          ->  HashAggregate
                Group Key: pagg_tab_m_1.a, ((pagg_tab_m_1.a + pagg_tab_m_1.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m_1.b) < 50)
                ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
          ->  HashAggregate
                Group Key: pagg_tab_m_2.a, ((pagg_tab_m_2.a + pagg_tab_m_2.b) / 2)
+               Buckets: 128
                Filter: (sum(pagg_tab_m_2.b) < 50)
                ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(18 rows)
 
 SELECT a, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY a, (a+b)/2 HAVING sum(b) < 50 ORDER BY 1, 2, 3;
  a  | sum |         avg         | count 
@@ -898,17 +941,20 @@ SELECT a, c, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY (a+b)/2, 2, 1 HAV
    ->  Append
          ->  HashAggregate
                Group Key: ((pagg_tab_m.a + pagg_tab_m.b) / 2), pagg_tab_m.c, pagg_tab_m.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m.b) = 50) AND (avg(pagg_tab_m.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p1 pagg_tab_m
          ->  HashAggregate
                Group Key: ((pagg_tab_m_1.a + pagg_tab_m_1.b) / 2), pagg_tab_m_1.c, pagg_tab_m_1.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m_1.b) = 50) AND (avg(pagg_tab_m_1.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p2 pagg_tab_m_1
          ->  HashAggregate
                Group Key: ((pagg_tab_m_2.a + pagg_tab_m_2.b) / 2), pagg_tab_m_2.c, pagg_tab_m_2.a
+               Buckets: 128
                Filter: ((sum(pagg_tab_m_2.b) = 50) AND (avg(pagg_tab_m_2.c) > '25'::numeric))
                ->  Seq Scan on pagg_tab_m_p3 pagg_tab_m_2
-(15 rows)
+(18 rows)
 
 SELECT a, c, sum(b), avg(c), count(*) FROM pagg_tab_m GROUP BY (a+b)/2, 2, 1 HAVING sum(b) = 50 AND avg(c) > 25 ORDER BY 1, 2, 3;
  a  | c  | sum |         avg         | count 
@@ -1032,6 +1078,7 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_ml.a
+               Buckets: 16
                Filter: (avg(pagg_tab_ml.b) < '3'::numeric)
                ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  Finalize GroupAggregate
@@ -1042,9 +1089,11 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                      ->  Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_2.a
+                                 Buckets: 16
                                  ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_2
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_3.a
+                                 Buckets: 16
                                  ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_3
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_5.a
@@ -1054,11 +1103,13 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                      ->  Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_5.a
+                                 Buckets: 8
                                  ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_5
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_6.a
+                                 Buckets: 8
                                  ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_6
-(31 rows)
+(36 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1089,20 +1140,25 @@ SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b ORDER BY 1, 2, 3;
                ->  Append
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_1.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_2.b
+                           Buckets: 16
                            ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_3.b
+                           Buckets: 8
                            ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_ml_4.b
+                           Buckets: 8
                            ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(22 rows)
+(27 rows)
 
 SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b HAVING avg(a) < 15 ORDER BY 1, 2, 3;
  b |  sum  | count 
@@ -1124,25 +1180,30 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 O
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_ml.a, pagg_tab_ml.b, pagg_tab_ml.c
+               Buckets: 512
                Filter: (avg(pagg_tab_ml.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  HashAggregate
                Group Key: pagg_tab_ml_1.a, pagg_tab_ml_1.b, pagg_tab_ml_1.c
+               Buckets: 256
                Filter: (avg(pagg_tab_ml_1.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
          ->  HashAggregate
                Group Key: pagg_tab_ml_2.a, pagg_tab_ml_2.b, pagg_tab_ml_2.c
+               Buckets: 256
                Filter: (avg(pagg_tab_ml_2.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
          ->  HashAggregate
                Group Key: pagg_tab_ml_3.a, pagg_tab_ml_3.b, pagg_tab_ml_3.c
+               Buckets: 128
                Filter: (avg(pagg_tab_ml_3.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
          ->  HashAggregate
                Group Key: pagg_tab_ml_4.a, pagg_tab_ml_4.b, pagg_tab_ml_4.c
+               Buckets: 128
                Filter: (avg(pagg_tab_ml_4.b) > '7'::numeric)
                ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(23 rows)
+(28 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1183,6 +1244,7 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            Sort Key: pagg_tab_ml.a
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml.a
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_2.a
@@ -1194,9 +1256,11 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            ->  Parallel Append
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_2.a
+                                       Buckets: 16
                                        ->  Parallel Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_2
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_3.a
+                                       Buckets: 16
                                        ->  Parallel Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_3
          ->  Finalize GroupAggregate
                Group Key: pagg_tab_ml_5.a
@@ -1208,11 +1272,13 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER B
                            ->  Parallel Append
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_5.a
+                                       Buckets: 8
                                        ->  Parallel Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_5
                                  ->  Partial HashAggregate
                                        Group Key: pagg_tab_ml_6.a
+                                       Buckets: 8
                                        ->  Parallel Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_6
-(41 rows)
+(46 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1245,20 +1311,25 @@ SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b ORDER BY 1, 2, 3;
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_1.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_2.b
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_3.b
+                                 Buckets: 8
                                  ->  Parallel Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_ml_4.b
+                                 Buckets: 8
                                  ->  Parallel Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(24 rows)
+(29 rows)
 
 SELECT b, sum(a), count(*) FROM pagg_tab_ml GROUP BY b HAVING avg(a) < 15 ORDER BY 1, 2, 3;
  b |  sum  | count 
@@ -1282,25 +1353,30 @@ SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 O
          ->  Parallel Append
                ->  HashAggregate
                      Group Key: pagg_tab_ml.a, pagg_tab_ml.b, pagg_tab_ml.c
+                     Buckets: 512
                      Filter: (avg(pagg_tab_ml.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p1 pagg_tab_ml
                ->  HashAggregate
                      Group Key: pagg_tab_ml_1.a, pagg_tab_ml_1.b, pagg_tab_ml_1.c
+                     Buckets: 256
                      Filter: (avg(pagg_tab_ml_1.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p2_s1 pagg_tab_ml_1
                ->  HashAggregate
                      Group Key: pagg_tab_ml_2.a, pagg_tab_ml_2.b, pagg_tab_ml_2.c
+                     Buckets: 256
                      Filter: (avg(pagg_tab_ml_2.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p2_s2 pagg_tab_ml_2
                ->  HashAggregate
                      Group Key: pagg_tab_ml_3.a, pagg_tab_ml_3.b, pagg_tab_ml_3.c
+                     Buckets: 128
                      Filter: (avg(pagg_tab_ml_3.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p3_s1 pagg_tab_ml_3
                ->  HashAggregate
                      Group Key: pagg_tab_ml_4.a, pagg_tab_ml_4.b, pagg_tab_ml_4.c
+                     Buckets: 128
                      Filter: (avg(pagg_tab_ml_4.b) > '7'::numeric)
                      ->  Seq Scan on pagg_tab_ml_p3_s2 pagg_tab_ml_4
-(25 rows)
+(30 rows)
 
 SELECT a, sum(b), count(*) FROM pagg_tab_ml GROUP BY a, b, c HAVING avg(b) > 7 ORDER BY 1, 2, 3;
  a  | sum  | count 
@@ -1351,14 +1427,17 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_1.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_2.x
+                                 Buckets: 16
                                  ->  Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+(22 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1388,14 +1467,17 @@ SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) <
                      ->  Parallel Append
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_1.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
                            ->  Partial HashAggregate
                                  Group Key: pagg_tab_para_2.y
+                                 Buckets: 32
                                  ->  Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+(22 rows)
 
 SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) < 12 ORDER BY 1, 2, 3;
  y  |  sum  |         avg         | count 
@@ -1425,11 +1507,12 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      Sort Key: pagg_tab_para.x
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_para.x
+                           Buckets: 64
                            ->  Parallel Append
                                  ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para_1
                                  ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_3
                                  ->  Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_2
-(15 rows)
+(16 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1459,11 +1542,12 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
                      Sort Key: pagg_tab_para.x
                      ->  Partial HashAggregate
                            Group Key: pagg_tab_para.x
+                           Buckets: 64
                            ->  Parallel Append
                                  ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para_1
                                  ->  Seq Scan on pagg_tab_para_p2 pagg_tab_para_2
                                  ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_3
-(15 rows)
+(16 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
@@ -1488,17 +1572,20 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
    ->  Append
          ->  HashAggregate
                Group Key: pagg_tab_para.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p1 pagg_tab_para
          ->  HashAggregate
                Group Key: pagg_tab_para_1.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para_1.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
          ->  HashAggregate
                Group Key: pagg_tab_para_2.x
+               Buckets: 16
                Filter: (avg(pagg_tab_para_2.y) < '7'::numeric)
                ->  Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(15 rows)
+(18 rows)
 
 SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) < 7 ORDER BY 1, 2, 3;
  x  | sum  |        avg         | count 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index b3fbe47bde..b8309304c0 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -819,6 +819,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_2.a = t1_5.b)
                ->  HashAggregate
                      Group Key: t1_5.b
+                     Buckets: 4
                      ->  Hash Join
                            Hash Cond: (((t2_1.a + t2_1.b) / 2) = t1_5.b)
                            ->  Seq Scan on prt1_e_p1 t2_1
@@ -832,6 +833,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_3.a = t1_6.b)
                ->  HashAggregate
                      Group Key: t1_6.b
+                     Buckets: 4
                      ->  Hash Join
                            Hash Cond: (((t2_2.a + t2_2.b) / 2) = t1_6.b)
                            ->  Seq Scan on prt1_e_p2 t2_2
@@ -845,6 +847,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                Join Filter: (t1_4.a = t1_7.b)
                ->  HashAggregate
                      Group Key: t1_7.b
+                     Buckets: 2
                      ->  Nested Loop
                            ->  Seq Scan on prt2_p3 t1_7
                                  Filter: (a = 0)
@@ -853,7 +856,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHER
                ->  Index Scan using iprt1_p3_a on prt1_p3 t1_4
                      Index Cond: (a = ((t2_3.a + t2_3.b) / 2))
                      Filter: (b = 0)
-(41 rows)
+(44 rows)
 
 SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHERE t1.a = 0 AND t1.b = (t2.a + t2.b)/2) AND t1.b = 0 ORDER BY t1.a;
   a  | b |  c   
@@ -874,6 +877,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_6.b
+                     Buckets: 4
                      ->  Hash Semi Join
                            Hash Cond: (t1_6.b = ((t1_9.a + t1_9.b) / 2))
                            ->  Seq Scan on prt2_p1 t1_6
@@ -886,6 +890,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_7.b
+                     Buckets: 4
                      ->  Hash Semi Join
                            Hash Cond: (t1_7.b = ((t1_10.a + t1_10.b) / 2))
                            ->  Seq Scan on prt2_p2 t1_7
@@ -898,6 +903,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
          ->  Nested Loop
                ->  HashAggregate
                      Group Key: t1_8.b
+                     Buckets: 2
                      ->  Hash Semi Join
                            Hash Cond: (t1_8.b = ((t1_11.a + t1_11.b) / 2))
                            ->  Seq Scan on prt2_p3 t1_8
@@ -907,7 +913,7 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (
                ->  Index Scan using iprt1_p3_a on prt1_p3 t1_5
                      Index Cond: (a = t1_8.b)
                      Filter: (b = 0)
-(39 rows)
+(42 rows)
 
 SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a;
   a  | b |  c   
@@ -1466,6 +1472,7 @@ SELECT avg(t1.a), avg(t2.b), t1.c, t2.c FROM plt1 t1 RIGHT JOIN plt2 t2 ON t1.c
    Sort Key: t1.c
    ->  HashAggregate
          Group Key: t1.c, t2.c
+         Buckets: 4
          ->  Append
                ->  Hash Join
                      Hash Cond: (t2_1.c = t1_1.c)
@@ -1485,7 +1492,7 @@ SELECT avg(t1.a), avg(t2.b), t1.c, t2.c FROM plt1 t1 RIGHT JOIN plt2 t2 ON t1.c
                      ->  Hash
                            ->  Seq Scan on plt1_p3 t1_3
                                  Filter: ((a % 25) = 0)
-(23 rows)
+(24 rows)
 
 --
 -- multiple levels of partitioning
diff --git a/src/test/regress/expected/pg_lsn.out b/src/test/regress/expected/pg_lsn.out
index 64d41dfdad..c6a56254be 100644
--- a/src/test/regress/expected/pg_lsn.out
+++ b/src/test/regress/expected/pg_lsn.out
@@ -85,6 +85,7 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
    Sort Key: (((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn)
    ->  HashAggregate
          Group Key: ((((i.i)::text || '/'::text) || (j.j)::text))::pg_lsn
+         Buckets: 4
          ->  Nested Loop
                ->  Function Scan on generate_series k
                ->  Materialize
@@ -93,7 +94,7 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f
                                  Filter: ((j > 0) AND (j <= 10))
                            ->  Function Scan on generate_series i
                                  Filter: (i <= 10)
-(12 rows)
+(13 rows)
 
 SELECT DISTINCT (i || '/' || j)::pg_lsn f
   FROM generate_series(1, 10) i,
diff --git a/src/test/regress/expected/select_distinct.out b/src/test/regress/expected/select_distinct.out
index f3696c6d1d..8e2903a3ed 100644
--- a/src/test/regress/expected/select_distinct.out
+++ b/src/test/regress/expected/select_distinct.out
@@ -137,9 +137,10 @@ SELECT count(*) FROM
    ->  HashAggregate
          Output: tenk1.two, tenk1.four, tenk1.two
          Group Key: tenk1.two, tenk1.four, tenk1.two
+         Buckets: 8
          ->  Seq Scan on public.tenk1
                Output: tenk1.two, tenk1.four, tenk1.two
-(7 rows)
+(8 rows)
 
 SELECT count(*) FROM
   (SELECT DISTINCT two, four, two FROM tenk1) ss;
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 96dfb7c8dd..2b8a253c79 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -226,12 +226,14 @@ explain (costs off)
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 select length(stringu1) from tenk1 group by length(stringu1);
  length 
@@ -247,12 +249,14 @@ explain (costs off)
    Sort Key: stringu1
    ->  Finalize HashAggregate
          Group Key: stringu1
+         Buckets: 1024
          ->  Gather
                Workers Planned: 4
                ->  Partial HashAggregate
                      Group Key: stringu1
+                     Buckets: 1024
                      ->  Parallel Seq Scan on tenk1
-(9 rows)
+(11 rows)
 
 -- test that parallel plan for aggregates is not selected when
 -- target list contains parallel restricted clause.
@@ -263,10 +267,11 @@ explain (costs off)
 -------------------------------------------------------------------
  HashAggregate
    Group Key: sp_parallel_restricted(unique1)
+   Buckets: 16384
    ->  Gather
          Workers Planned: 4
          ->  Parallel Index Only Scan using tenk1_unique1 on tenk1
-(5 rows)
+(6 rows)
 
 -- test prepared statement
 prepare tenk1_count(integer) As select  count((unique1)) from tenk1 where hundred > $1;
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index 4c6cd5f146..e4a79170d8 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -932,6 +932,7 @@ where o.ten = 1;
                Filter: (ten = 1)
          ->  Subquery Scan on ss
                ->  HashSetOp Except
+                     Buckets: 2
                      ->  Append
                            ->  Subquery Scan on "*SELECT* 1"
                                  ->  Index Scan using onek_unique1 on onek i1
@@ -939,7 +940,7 @@ where o.ten = 1;
                            ->  Subquery Scan on "*SELECT* 2"
                                  ->  Index Scan using onek_unique1 on onek i2
                                        Index Cond: (unique1 = o.unique2)
-(13 rows)
+(14 rows)
 
 select count(*) from
   onek o cross join lateral (
@@ -976,10 +977,11 @@ where o.ten = 1;
          ->  CTE Scan on x
                CTE x
                  ->  Recursive Union
+                       Buckets: 64
                        ->  Result
                        ->  WorkTable Scan on x x_1
                              Filter: (a < 10)
-(10 rows)
+(11 rows)
 
 select sum(o.four), sum(ss.a) from
   onek o cross join lateral (
@@ -1130,9 +1132,10 @@ select * from int4_tbl o where (f1, f1) in
                            ->  HashAggregate
                                  Output: i.f1
                                  Group Key: i.f1
+                                 Buckets: 8
                                  ->  Seq Scan on public.int4_tbl i
                                        Output: i.f1
-(19 rows)
+(20 rows)
 
 select * from int4_tbl o where (f1, f1) in
   (select f1, generate_series(1,50) / 10 g from int4_tbl i group by f1);
diff --git a/src/test/regress/expected/tablesample.out b/src/test/regress/expected/tablesample.out
index 078358d226..fc41a81039 100644
--- a/src/test/regress/expected/tablesample.out
+++ b/src/test/regress/expected/tablesample.out
@@ -255,11 +255,12 @@ select pct, count(unique1) from
 --------------------------------------------------------
  HashAggregate
    Group Key: "*VALUES*".column1
+   Buckets: 2
    ->  Nested Loop
          ->  Values Scan on "*VALUES*"
          ->  Sample Scan on tenk1
                Sampling: bernoulli ("*VALUES*".column1)
-(6 rows)
+(7 rows)
 
 select pct, count(unique1) from
   (values (0),(100)) v(pct),
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92d80..2153974fa7 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -355,12 +355,13 @@ select count(*) from
  Aggregate
    ->  Subquery Scan on ss
          ->  HashSetOp Intersect
+               Buckets: 8192
                ->  Append
                      ->  Subquery Scan on "*SELECT* 2"
                            ->  Seq Scan on tenk1
                      ->  Subquery Scan on "*SELECT* 1"
                            ->  Index Only Scan using tenk1_unique1 on tenk1 tenk1_1
-(8 rows)
+(9 rows)
 
 select count(*) from
   ( select unique1 from tenk1 intersect select fivethous from tenk1 ) ss;
@@ -374,13 +375,14 @@ select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  HashSetOp Except
+   Buckets: 16384
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Index Only Scan using tenk1_unique1 on tenk1
          ->  Subquery Scan on "*SELECT* 2"
                ->  Index Only Scan using tenk1_unique2 on tenk1 tenk1_1
                      Filter: (unique2 <> 10)
-(7 rows)
+(8 rows)
 
 select unique1 from tenk1 except select unique2 from tenk1 where unique2 != 10;
  unique1 
@@ -585,12 +587,13 @@ select from generate_series(1,5) intersect select from generate_series(1,3);
                               QUERY PLAN                              
 ----------------------------------------------------------------------
  HashSetOp Intersect
+   Buckets: 2
    ->  Append
          ->  Subquery Scan on "*SELECT* 1"
                ->  Function Scan on generate_series
          ->  Subquery Scan on "*SELECT* 2"
                ->  Function Scan on generate_series generate_series_1
-(6 rows)
+(7 rows)
 
 select from generate_series(1,5) union select from generate_series(1,3);
 --
@@ -726,12 +729,13 @@ explain (costs off)
 ---------------------------------------------------
  HashAggregate
    Group Key: ((t1.a || t1.b))
+   Buckets: 8
    ->  Append
          ->  Index Scan using t1_ab_idx on t1
                Index Cond: ((a || b) = 'ab'::text)
          ->  Index Only Scan using t2_pkey on t2
                Index Cond: (ab = 'ab'::text)
-(7 rows)
+(8 rows)
 
 --
 -- Test that ORDER BY for UNION ALL can be pushed down to inheritance
@@ -864,11 +868,12 @@ ORDER BY x;
          Filter: (ss.x < 4)
          ->  HashAggregate
                Group Key: (1), (generate_series(1, 10))
+               Buckets: 16
                ->  Append
                      ->  ProjectSet
                            ->  Result
                      ->  Result
-(10 rows)
+(11 rows)
 
 SELECT * FROM
   (SELECT 1 AS t, generate_series(1,10) AS x
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index d5fd4045f9..ea34daffe4 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -624,8 +624,9 @@ select first_value(max(x)) over (), y
  WindowAgg
    ->  HashAggregate
          Group Key: (tenk1.ten + tenk1.four)
+         Buckets: 64
          ->  Seq Scan on tenk1
-(4 rows)
+(5 rows)
 
 -- test non-default frame specifications
 SELECT four, ten,
diff --git a/src/test/regress/expected/write_parallel.out b/src/test/regress/expected/write_parallel.out
index 0c4da2591a..25193a8b84 100644
--- a/src/test/regress/expected/write_parallel.out
+++ b/src/test/regress/expected/write_parallel.out
@@ -19,12 +19,14 @@ explain (costs off) create table parallel_write as
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create table parallel_write as
     select length(stringu1) from tenk1 group by length(stringu1);
@@ -35,12 +37,14 @@ explain (costs off) select length(stringu1) into parallel_write
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 select length(stringu1) into parallel_write
     from tenk1 group by length(stringu1);
@@ -51,12 +55,14 @@ explain (costs off) create materialized view parallel_mat_view as
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create materialized view parallel_mat_view as
     select length(stringu1) from tenk1 group by length(stringu1);
@@ -67,12 +73,14 @@ explain (costs off) create table parallel_write as execute prep_stmt;
 ---------------------------------------------------
  Finalize HashAggregate
    Group Key: (length((stringu1)::text))
+   Buckets: 1024
    ->  Gather
          Workers Planned: 4
          ->  Partial HashAggregate
                Group Key: length((stringu1)::text)
+               Buckets: 1024
                ->  Parallel Seq Scan on tenk1
-(7 rows)
+(9 rows)
 
 create table parallel_write as execute prep_stmt;
 drop table parallel_write;
-- 
2.17.0

v7-0002-refactor-show_grouping_set_keys.patchtext/x-diff; charset=us-asciiDownload
From 4104426bf48864b188f3157c26d73895efafa460 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 23 Feb 2020 23:13:07 -0600
Subject: [PATCH v7 2/9] refactor show_grouping_set_keys

---
 src/backend/commands/explain.c | 55 ++++++++++++++++++++--------------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 9e522f7971..e3f05da464 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -95,6 +95,8 @@ static void show_grouping_set_info(AggState *aggstate,
 								   List *ancestors,
 								   HashTableInstrumentation *inst,
 								   ExplainState *es);
+static void show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List
+		*context, bool useprefix, ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -2348,6 +2350,37 @@ show_grouping_set_info(AggState *aggstate,
 					   List *context, bool useprefix,
 					   List *ancestors, HashTableInstrumentation *inst,
 					   ExplainState *es)
+{
+	PlanState	*planstate = outerPlanState(aggstate);
+
+	ExplainOpenGroup("Grouping Set", NULL, true, es);
+
+	if (sortnode)
+	{
+		show_sort_group_keys(planstate, "Sort Key",
+							 sortnode->numCols, sortnode->sortColIdx,
+							 sortnode->sortOperators, sortnode->collations,
+							 sortnode->nullsFirst,
+							 ancestors, es);
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			es->indent++;
+	}
+
+	show_grouping_set_keys(aggstate, aggnode, context, useprefix, es);
+
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, es);
+
+	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
+		es->indent--;
+
+	ExplainCloseGroup("Grouping Set", NULL, true, es);
+}
+
+/* Show keys of a grouping set */
+static void
+show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List *context, bool useprefix, ExplainState *es)
 {
 	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
@@ -2369,19 +2402,6 @@ show_grouping_set_info(AggState *aggstate,
 		keysetname = "Group Keys";
 	}
 
-	ExplainOpenGroup("Grouping Set", NULL, true, es);
-
-	if (sortnode)
-	{
-		show_sort_group_keys(planstate, "Sort Key",
-							 sortnode->numCols, sortnode->sortColIdx,
-							 sortnode->sortOperators, sortnode->collations,
-							 sortnode->nullsFirst,
-							 ancestors, es);
-		if (es->format == EXPLAIN_FORMAT_TEXT)
-			es->indent++;
-	}
-
 	ExplainOpenGroup(keysetname, keysetname, false, es);
 
 	foreach(lc, gsets)
@@ -2412,15 +2432,6 @@ show_grouping_set_info(AggState *aggstate,
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
-
-	if (aggnode->aggstrategy == AGG_HASHED ||
-			aggnode->aggstrategy == AGG_MIXED)
-		show_tuplehash_info(inst, es);
-
-	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
-		es->indent--;
-
-	ExplainCloseGroup("Grouping Set", NULL, true, es);
 }
 
 /*
-- 
2.17.0

v7-0003-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From 1d1671b2fffd3e3a8050364aec5a7f5d5add8d73 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v7 3/9] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c | 70 ++++++++++++++++++----------------
 1 file changed, 38 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e3f05da464..4c92256646 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						SubPlanState *subplanstate, ExplainState *es);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -721,7 +721,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, NULL, es);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1083,7 +1083,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			SubPlanState *subplanstate, ExplainState *es)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1340,6 +1340,21 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			}
+			if (subplanstate->hashnulls)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1368,6 +1383,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+
+		if (subplanstate && subplanstate->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (subplanstate && subplanstate->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2040,12 +2069,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, NULL, es);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, NULL, es);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2077,7 +2106,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, NULL, es);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3480,7 +3509,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, NULL, es);
 }
 
 /*
@@ -3538,30 +3567,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-		{
-			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashtable->instrument, es);
-			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
-		}
-
-		if (sps->hashnulls)
-		{
-			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, es);
-			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
-		}
+					relationship, sp->plan_name, sps, es);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3578,7 +3584,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, NULL, es);
 }
 
 /*
-- 
2.17.0

v7-0004-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From b7cbb6c0236934bdbd5e84077dc4d6f752e5e3c2 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v7 4/9] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.

Note, this doesn't affect any regression tests, since hashtable isn't allocated
during "explain".  Note that "explain analyze" would show memory stats, which
we'd have to filter.
---
 src/backend/commands/explain.c            |  5 ++--
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 29 +++++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 4c92256646..e0dc5a092a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1734,8 +1734,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
-			if (es->analyze)
-				show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
+			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
 			break;
 		case T_SampleScan:
 			show_tablesample(((SampleScan *) plan)->tablesample,
@@ -2927,6 +2926,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index ae8a11da30..9ae99a326e 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -744,6 +746,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index ad4e071ca3..ab81cce3b2 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -44,6 +44,7 @@
 #include "common/hashfn.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	HashTableInstrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1147,6 +1150,32 @@ tbm_end_iterate(TBMIterator *iterator)
 	pfree(iterator);
 }
 
+/*
+ * tbm_instrumentation - update stored stats and return pointer to
+ * instrumentation structure
+ *
+ * This updates stats when called.
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+HashTableInstrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable)
+	{
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+
+		/*
+		 * If there are lossy pages, then at one point, we filled maxentries;
+		 * otherwise, number of pages is "->members".
+		 */
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) *
+			(tbm->nchunks>0 ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
 /*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cfeada5f1d..b1e2d1f1ae 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1609,6 +1609,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	HashTableInstrumentation	instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fcae34..de0cdfb91f 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct HashTableInstrumentation HashTableInstrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern HashTableInstrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.17.0

v7-0005-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From 78de819e4c19984d9ad124810da4682890ba796a Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v7 5/9] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.

This might be unimportant and maybe clearer left in execGrouping.c.
---
 src/backend/commands/explain.c            | 18 +++++++--------
 src/backend/executor/execGrouping.c       | 28 -----------------------
 src/backend/executor/nodeAgg.c            | 14 ++++++++----
 src/backend/executor/nodeRecursiveunion.c |  3 ++-
 src/backend/executor/nodeSetOp.c          |  6 ++++-
 src/backend/executor/nodeSubplan.c        | 10 ++++++--
 src/include/executor/executor.h           |  1 -
 src/include/executor/nodeAgg.h            |  1 +
 src/include/nodes/execnodes.h             | 24 +++++++++++++++++--
 9 files changed, 57 insertions(+), 48 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index e0dc5a092a..cef19c684b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1347,13 +1347,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument, es);
 			}
 			if (subplanstate->hashnulls)
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			}
 		}
 		if (es->indent)
@@ -1387,14 +1387,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (subplanstate && subplanstate->hashtable)
 		{
 			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashtable->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument, es);
 			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
 		}
 
 		if (subplanstate && subplanstate->hashnulls)
 		{
 			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, es);
 			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
 		}
 	}
@@ -1925,14 +1925,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, es);
+					show_tuplehash_info(&sos->instrument, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, es);
+					show_tuplehash_info(&rus->instrument, es);
 				break;
 			}
 		case T_Group:
@@ -2320,7 +2320,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes <= 1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2347,7 +2347,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 
 	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
 			aggstate->num_hashes ?
-			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			&aggstate->perhash[setno++].instrument : NULL,
 			es);
 
 	foreach(lc, agg->chain)
@@ -2360,7 +2360,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED)
 		{
 			Assert(setno < aggstate->num_hashes);
-			inst = &aggstate->perhash[setno++].hashtable->instrument;
+			inst = &aggstate->perhash[setno++].instrument;
 		}
 
 		show_grouping_set_info(aggstate, aggnode, sortnode,
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 844dd3ba86..009d27b9a8 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,7 +188,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -204,7 +203,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -283,35 +281,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial)
-	{
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
-			sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-		/* hashtable->entrysize includes additionalsize */
-		hashtable->instrument.space_peak_hash = Max(
-			hashtable->instrument.space_peak_hash,
-			hashtable->hashtab->size * sizeof(TupleHashEntryData));
-
-		hashtable->instrument.space_peak_tuples = Max(
-			hashtable->instrument.space_peak_tuples,
-				hashtable->hashtab->members * hashtable->entrysize);
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index e5aa0629a7..867da6eebf 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1334,6 +1334,9 @@ build_hash_table(AggState *aggstate, int setno, long nbuckets)
 		hashcxt,
 		tmpcxt,
 		DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+
+	InitTupleHashTableStats(perhash->instrument,
+			perhash->hashtable->hashtab, additionalsize);
 }
 
 /*
@@ -1710,9 +1713,10 @@ agg_retrieve_direct(AggState *aggstate)
 				 */
 				initialize_phase(aggstate, 0);
 				aggstate->table_filled = true;
-				UpdateTupleHashTableStats(aggstate->perhash[0].hashtable, false);
+				UpdateTupleHashTableStats(aggstate->perhash[0].instrument,
+						aggstate->perhash[0].hashtable->hashtab);
 				ResetTupleHashIterator(aggstate->perhash[0].hashtable,
-									   &aggstate->perhash[0].hashiter);
+						&aggstate->perhash[0].hashiter);
 				select_current_set(aggstate, 0, true);
 				return agg_retrieve_hash_table(aggstate);
 			}
@@ -1913,7 +1917,8 @@ agg_retrieve_direct(AggState *aggstate)
 						aggstate->current_phase == 1)
 				{
 					for (int i = 0; i < aggstate->num_hashes; i++)
-						UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+						UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+								aggstate->perhash[i].hashtable->hashtab);
 				}
 			}
 
@@ -1990,7 +1995,8 @@ agg_fill_hash_table(AggState *aggstate)
 
 	aggstate->table_filled = true;
 	for (int i = 0; i < aggstate->num_hashes; i++)
-		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+		UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+				aggstate->perhash[i].hashtable->hashtab);
 
 	/* Initialize to walk the first hash table */
 	select_current_set(aggstate, 0, true);
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c28b1..594abdbdf0 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,7 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, 0);
 }
 
 
@@ -157,7 +158,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab96e..a3860758eb 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,9 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+
+	InitTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab, 0);
 }
 
 /*
@@ -415,7 +418,8 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 22c32612ba..03533538f6 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -505,6 +505,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -518,6 +519,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -533,6 +536,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashnulls);
 		else
+		{
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -546,6 +550,8 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab, 0);
+		}
 	}
 	else
 		node->hashnulls = NULL;
@@ -621,9 +627,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index f4f2ede207..94890512dc 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 264916f9a9..2072c18b54 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -302,6 +302,7 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	HashTableInstrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index b1e2d1f1ae..11fe866c1c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,8 +691,25 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, addsize) \
+	do{\
+	instr.entrysize = sizeof(MinimalTuple) + addsize; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = htable->size * sizeof(TupleHashEntryData); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, htable->size*sizeof(TupleHashEntryData)); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, htable->members*instr.entrysize );\
+	}while(0)
+
 typedef struct HashTableInstrumentation
 {
+	size_t	entrysize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;	/* peak memory usage in bytes */
@@ -717,7 +734,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -883,6 +899,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	HashTableInstrumentation instrument;
+	HashTableInstrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1291,6 +1309,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	HashTableInstrumentation instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -1609,7 +1628,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
-	HashTableInstrumentation	instrument;
+	HashTableInstrumentation instrument;
 } BitmapHeapScanState;
 
 /* ----------------
@@ -2324,6 +2343,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	HashTableInstrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.17.0

v7-0006-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From 7032c3b1c21217dc0a6dfdbdf36359adc28c7bf2 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v7 6/9] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..98fd4bf8bd 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -183,7 +183,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 11fe866c1c..3a335d87a0 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -726,7 +726,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.17.0

v7-0009-Add-explain-REGRESS.patchtext/x-diff; charset=us-asciiDownload
From 5c0f192df38917d21e267439b1b61b1b98fcb0b3 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 22 Feb 2020 21:17:10 -0600
Subject: [PATCH v7 9/9] Add explain(REGRESS)...

shorthand for: costs off, timing off, summary off, memory off
---
 src/backend/commands/explain.c                | 24 ++++++++++++++++---
 src/test/regress/expected/select.out          |  2 +-
 src/test/regress/expected/select_parallel.out |  2 +-
 src/test/regress/sql/select.sql               |  2 +-
 src/test/regress/sql/select_parallel.sql      |  2 +-
 5 files changed, 25 insertions(+), 7 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 9c7a243a78..106b0cdcf6 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -167,6 +167,8 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 	bool		timing_set = false;
 	bool		summary_set = false;
 	bool		machine_set = false;
+	bool		costs_set = false;
+	bool		regress = false;
 
 	/* Parse options list. */
 	foreach(lc, stmt->options)
@@ -178,11 +180,18 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 		else if (strcmp(opt->defname, "verbose") == 0)
 			es->verbose = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "costs") == 0)
+		{
+			/* Need to keep track if it explicitly set to ON */
+			costs_set = true;
 			es->costs = defGetBoolean(opt);
+		}
 		else if (strcmp(opt->defname, "buffers") == 0)
 			es->buffers = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "settings") == 0)
 			es->settings = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "regress") == 0)
+			/* This is convenience shorthand, only */
+			regress = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "timing") == 0)
 		{
 			timing_set = true;
@@ -225,13 +234,22 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 					 parser_errposition(pstate, opt->location)));
 	}
 
+	if (regress && (es->timing || es->machine || es->summary ||
+			(costs_set && es->costs)))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN option REGRESS cannot be specified with any of: TIMING, MEMORY, SUMMARY, COSTS")));
+
+	/* if the costs option was not set explicitly, set default value */
+	es->costs = (costs_set) ? es->costs : es->costs && !regress;
+
 	if (es->buffers && !es->analyze)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("EXPLAIN option BUFFERS requires ANALYZE")));
 
 	/* if the timing was not set explicitly, set default value */
-	es->timing = (timing_set) ? es->timing : es->analyze;
+	es->timing = (timing_set) ? es->timing : es->analyze && !regress;
 
 	/* check that timing is used with EXPLAIN ANALYZE */
 	if (es->timing && !es->analyze)
@@ -246,10 +264,10 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 				 errmsg("EXPLAIN option MEMORY requires ANALYZE")));
 
 	/* if the summary was not set explicitly, set default value */
-	es->summary = (summary_set) ? es->summary : es->analyze;
+	es->summary = (summary_set) ? es->summary : es->analyze && !regress;
 
 	/* if the memory option was not set explicitly, set default value */
-	es->machine = (machine_set) ? es->machine : es->analyze;
+	es->machine = (machine_set) ? es->machine : es->analyze && !regress;
 
 	/*
 	 * Parse analysis was done already, but we still have to run the rule
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index c441049f41..2a94111282 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -753,7 +753,7 @@ select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 (1 row)
 
 -- actually run the query with an analyze to use the partial index
-explain (costs off, analyze on, timing off, summary off)
+explain (analyze, regress)
 select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
                            QUERY PLAN                            
 -----------------------------------------------------------------
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index dcbf4b385f..51036289ba 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -549,7 +549,7 @@ select count(*) from bmscantest where a>1;
 -- test accumulation of stats for parallel nodes
 reset enable_seqscan;
 alter table tenk2 set (parallel_workers = 0);
-explain (analyze, timing off, summary off, costs off)
+explain (analyze, regress)
    select count(*) from tenk1, tenk2 where tenk1.hundred > 1
         and tenk2.thousand=0;
                                 QUERY PLAN                                
diff --git a/src/test/regress/sql/select.sql b/src/test/regress/sql/select.sql
index b5929b2eca..1c757fe0bf 100644
--- a/src/test/regress/sql/select.sql
+++ b/src/test/regress/sql/select.sql
@@ -196,7 +196,7 @@ explain (costs off)
 select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 -- actually run the query with an analyze to use the partial index
-explain (costs off, analyze on, timing off, summary off)
+explain (analyze, regress)
 select * from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
 explain (costs off)
 select unique2 from onek2 where unique2 = 11 and stringu1 = 'ATAAAA';
diff --git a/src/test/regress/sql/select_parallel.sql b/src/test/regress/sql/select_parallel.sql
index 5be2fef2d3..7afa55f345 100644
--- a/src/test/regress/sql/select_parallel.sql
+++ b/src/test/regress/sql/select_parallel.sql
@@ -217,7 +217,7 @@ select count(*) from bmscantest where a>1;
 -- test accumulation of stats for parallel nodes
 reset enable_seqscan;
 alter table tenk2 set (parallel_workers = 0);
-explain (analyze, timing off, summary off, costs off)
+explain (analyze, regress)
    select count(*) from tenk1, tenk2 where tenk1.hundred > 1
         and tenk2.thousand=0;
 alter table tenk2 reset (parallel_workers);
-- 
2.17.0

#15Andres Freund
andres@anarazel.de
In reply to: Justin Pryzby (#14)
Re: explain HashAggregate to report bucket and memory stats

Hi,

On 2020-03-06 15:33:10 -0600, Justin Pryzby wrote:

On Fri, Mar 06, 2020 at 09:58:59AM -0800, Andres Freund wrote:

+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %ld (originally %ld)",
+						inst->nbuckets,
+						inst->nbuckets_original);

I'm not sure I like the alternative output formats here. All the other
fields are separated with a comma, but the original size is in
parens. I'd probably just format it as "Buckets: %lld " and then add
", Original Buckets: %lld" when differing.

It's done that way for consistency with hashJoin in show_hash_info().

Fair. I don't like it, but it's not this patch's fault.

diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index f457b5b150..b173b32cab 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -517,10 +517,11 @@ order by 1, 2;
->  HashAggregate
Output: s2.s2, sum((s1.s1 + s2.s2))
Group Key: s2.s2
+               Buckets: 4
->  Function Scan on pg_catalog.generate_series s2
Output: s2.s2
Function Call: generate_series(1, 3)
-(14 rows)
+(15 rows)

These tests probably won't be portable. The number of hash buckets
calculated will e.g. depend onthe size of the contained elements. And
that'll e.g. will depend on whether pointers are 4 or 8 bytes.

I was aware and afraid of that. Previously, I added this output only to
"explain analyze", and (as an quick, interim implementation) changed various
tests to use analyze, and memory only shown in "verbose" mode. But as Tomas
pointed out, that's consistent with what's done elsewhere.

So is the solution to show stats only during explain ANALYZE ?

Or ... I have a patch to create a new explain(MACHINE) option to allow more
stable output, by avoiding Memory/Disk. That doesn't attempt to make all
"explain analyze" output stable - there's other issues, I think mostly related
to parallel workers (see 4ea03f3f, 13e8b2ee). But does allow retiring
explain_sq_limit and explain_parallel_sort_stats. I'm including my patch to
show what I mean, but I didn't enable it for hashtable "Buckets:". I guess in
either case, the tests shouldn't be included.

Yea, there's been recent discussion about an argument like that. See
e.g.
/messages/by-id/18494.1580079189@sss.pgh.pa.us

Greetings,

Andres Freund

#16Jeff Davis
pgsql@j-davis.com
In reply to: Justin Pryzby (#14)
Re: explain HashAggregate to report bucket and memory stats
+		/* hashtable->entrysize includes additionalsize */
+		hashtable->instrument.space_peak_hash = Max(
+			hashtable->instrument.space_peak_hash,
+			hashtable->hashtab->size *
sizeof(TupleHashEntryData));
+
+		hashtable->instrument.space_peak_tuples = Max(
+			hashtable->instrument.space_peak_tuples,
+				hashtable->hashtab->members *
hashtable->entrysize);

I think, in general, we should avoid estimates/projections for
reporting and try to get at a real number, like
MemoryContextMemAllocated(). (Aside: I may want to tweak exactly what
that function reports so that it doesn't count the unused portion of
the last block.)

For instance, the report is still not accurate, because it doesn't
account for pass-by-ref transition state values.

To use memory-context-based reporting, it's hard to make the stats a
part of the tuple hash table, because the tuple hash table doesn't own
the memory contexts (they are passed in). It's also hard to make it
per-hashtable (e.g. for grouping sets), unless we put each grouping set
in its own memory context.

Also, is there a reason you report two different memory values
(hashtable and tuples)? I don't object, but it seems like a little too
much detail.

Regards,
Jeff Davis

#17Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#16)
Re: explain HashAggregate to report bucket and memory stats

Hi,

On 2020-03-13 10:15:46 -0700, Jeff Davis wrote:

Also, is there a reason you report two different memory values
(hashtable and tuples)? I don't object, but it seems like a little too
much detail.

Seems useful to me - the hashtable is pre-allocated based on estimates,
whereas the tuples are allocated "on demand". So seeing the difference
will allow to investigate the more crucial issue...

Greetings,

Andres Freund

#18Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#17)
Re: explain HashAggregate to report bucket and memory stats

On Fri, 2020-03-13 at 10:27 -0700, Andres Freund wrote:

On 2020-03-13 10:15:46 -0700, Jeff Davis wrote:

Also, is there a reason you report two different memory values
(hashtable and tuples)? I don't object, but it seems like a little
too
much detail.

Seems useful to me - the hashtable is pre-allocated based on
estimates,
whereas the tuples are allocated "on demand". So seeing the
difference
will allow to investigate the more crucial issue...

Then do we also want to report separately on the by-ref transition
values? That could be useful if you are using ARRAY_AGG and the states
grow larger than you might expect.

Regards,
Jeff Davis

#19Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#18)
Re: explain HashAggregate to report bucket and memory stats

Hi,

On 2020-03-13 10:53:17 -0700, Jeff Davis wrote:

On Fri, 2020-03-13 at 10:27 -0700, Andres Freund wrote:

On 2020-03-13 10:15:46 -0700, Jeff Davis wrote:

Also, is there a reason you report two different memory values
(hashtable and tuples)? I don't object, but it seems like a little
too
much detail.

Seems useful to me - the hashtable is pre-allocated based on
estimates,
whereas the tuples are allocated "on demand". So seeing the
difference
will allow to investigate the more crucial issue...

Then do we also want to report separately on the by-ref transition
values? That could be useful if you are using ARRAY_AGG and the states
grow larger than you might expect.

I can see that being valuable - I've had to debug cases with too much
memory being used due to aggregate transitions before. Right now it'd be
mixed in with tuples, I believe - and we'd need a separate context for
tracking the transition values? Due to that I'm inclined to not report
separately for now.

Greetings,

Andres Freund

#20Justin Pryzby
pryzby@telsasoft.com
In reply to: Andres Freund (#19)
8 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

On Fri, Mar 13, 2020 at 10:57:43AM -0700, Andres Freund wrote:

On 2020-03-13 10:53:17 -0700, Jeff Davis wrote:

On Fri, 2020-03-13 at 10:27 -0700, Andres Freund wrote:

On 2020-03-13 10:15:46 -0700, Jeff Davis wrote:

Also, is there a reason you report two different memory values
(hashtable and tuples)? I don't object, but it seems like a little too
much detail.

Seems useful to me - the hashtable is pre-allocated based on estimates,
whereas the tuples are allocated "on demand". So seeing the difference
will allow to investigate the more crucial issue...

Then do we also want to report separately on the by-ref transition
values? That could be useful if you are using ARRAY_AGG and the states
grow larger than you might expect.

I can see that being valuable - I've had to debug cases with too much
memory being used due to aggregate transitions before. Right now it'd be
mixed in with tuples, I believe - and we'd need a separate context for
tracking the transition values? Due to that I'm inclined to not report
separately for now.

I think that's already in a separate context indexed by grouping set:
src/include/nodes/execnodes.h: ExprContext **aggcontexts; /* econtexts for long-lived data (per GS) */

But the hashtable and tuples are combined. I put them in separate contexts and
rebased on top of 1f39bce021540fde00990af55b4432c55ef4b3c7.

But didn't do anything yet with the aggcontexts.

Now I can get output like:

|template1=# explain analyze SELECT i,COUNT(1) FROM t GROUP BY 1;
| HashAggregate (cost=4769.99..6769.98 rows=199999 width=12) (actual time=266.465..27020.333 rows=199999 loops=1)
| Group Key: i
| Buckets: 524288 (originally 262144)
| Peak Memory Usage: hashtable: 12297kB, tuples: 24576kB
| Disk Usage: 192 kB
| HashAgg Batches: 3874
| -> Seq Scan on t (cost=0.00..3769.99 rows=199999 width=4) (actual time=13.043..64.017 rows=199999 loops=1)

It looks somewhat funny next to hash join, which puts everything on one line:

|template1=# explain analyze SELECT i,COUNT(1) FROM t a JOIN t b USING(i) GROUP BY 1;
| HashAggregate (cost=13789.95..15789.94 rows=199999 width=12) (actual time=657.733..27129.873 rows=199999 loops=1)
| Group Key: a.i
| Buckets: 524288 (originally 262144)
| Peak Memory Usage: hashtable: 12297kB, tuples: 24576kB
| Disk Usage: 192 kB
| HashAgg Batches: 3874
| -> Hash Join (cost=6269.98..12789.95 rows=199999 width=4) (actual time=135.932..426.071 rows=199999 loops=1)
| Hash Cond: (a.i = b.i)
| -> Seq Scan on t a (cost=0.00..3769.99 rows=199999 width=4) (actual time=3.265..47.598 rows=199999 loops=1)
| -> Hash (cost=3769.99..3769.99 rows=199999 width=4) (actual time=131.881..131.882 rows=199999 loops=1)
| Buckets: 262144 Batches: 1 Memory Usage: 9080kB
| -> Seq Scan on t b (cost=0.00..3769.99 rows=199999 width=4) (actual time=3.273..40.163 rows=199999 loops=1)

--
Justin

Attachments:

v8-0001-nodeAgg-separate-context-for-each-hashtable.patchtext/x-diff; charset=us-asciiDownload
From e593c119c97ea31edac4c9f08a39eee451964a16 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Thu, 19 Mar 2020 23:03:25 -0500
Subject: [PATCH v8 1/8] nodeAgg: separate context for each hashtable

---
 src/backend/executor/execExpr.c |  2 +-
 src/backend/executor/nodeAgg.c  | 82 +++++++++++++++++++--------------
 src/include/executor/nodeAgg.h  |  2 +
 src/include/nodes/execnodes.h   |  2 -
 4 files changed, 51 insertions(+), 37 deletions(-)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 1370ffec50..039c5a8b5f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -3241,7 +3241,7 @@ ExecBuildAggTransCall(ExprState *state, AggState *aggstate,
 	int adjust_jumpnull = -1;
 
 	if (ishash)
-		aggcontext = aggstate->hashcontext;
+		aggcontext = aggstate->perhash[setno].hashcontext;
 	else
 		aggcontext = aggstate->aggcontexts[setno];
 
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 44c159ab2a..1d319f49d0 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -191,8 +191,7 @@
  *	  So we create an array, aggcontexts, with an ExprContext for each grouping
  *	  set in the largest rollup that we're going to process, and use the
  *	  per-tuple memory context of those ExprContexts to store the aggregate
- *	  transition values.  hashcontext is the single context created to support
- *	  all hash tables.
+ *	  transition values.
  *
  *	  Spilling To Disk
  *
@@ -457,7 +456,7 @@ select_current_set(AggState *aggstate, int setno, bool is_hash)
 	 * ExecAggPlainTransByRef().
 	 */
 	if (is_hash)
-		aggstate->curaggcontext = aggstate->hashcontext;
+		aggstate->curaggcontext = aggstate->perhash[setno].hashcontext;
 	else
 		aggstate->curaggcontext = aggstate->aggcontexts[setno];
 
@@ -1424,8 +1423,7 @@ find_unaggregated_cols_walker(Node *node, Bitmapset **colnos)
  * grouping set for which we're doing hashing.
  *
  * The contents of the hash tables always live in the hashcontext's per-tuple
- * memory context (there is only one of these for all tables together, since
- * they are all reset at the same time).
+ * memory context.
  */
 static void
 build_hash_tables(AggState *aggstate)
@@ -1465,8 +1463,8 @@ static void
 build_hash_table(AggState *aggstate, int setno, long nbuckets)
 {
 	AggStatePerHash perhash = &aggstate->perhash[setno];
-	MemoryContext	metacxt = aggstate->hash_metacxt;
-	MemoryContext	hashcxt = aggstate->hashcontext->ecxt_per_tuple_memory;
+	MemoryContext	metacxt = perhash->hash_metacxt;
+	MemoryContext	hashcxt = perhash->hashcontext->ecxt_per_tuple_memory;
 	MemoryContext	tmpcxt	= aggstate->tmpcontext->ecxt_per_tuple_memory;
 	Size            additionalsize;
 
@@ -1776,10 +1774,15 @@ static void
 hash_agg_check_limits(AggState *aggstate)
 {
 	uint64 ngroups = aggstate->hash_ngroups_current;
-	Size meta_mem = MemoryContextMemAllocated(
-		aggstate->hash_metacxt, true);
-	Size hash_mem = MemoryContextMemAllocated(
-		aggstate->hashcontext->ecxt_per_tuple_memory, true);
+	Size meta_mem = 0;
+	Size hash_mem = 0;
+
+	for (int i = 0; i < aggstate->num_hashes; ++i)
+		meta_mem = MemoryContextMemAllocated(
+		aggstate->perhash[i].hash_metacxt, true);
+	for (int i = 0; i < aggstate->num_hashes; ++i)
+		hash_mem += MemoryContextMemAllocated(
+			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
 
 	/*
 	 * Don't spill unless there's at least one group in the hash table so we
@@ -1837,8 +1840,8 @@ hash_agg_enter_spill_mode(AggState *aggstate)
 static void
 hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 {
-	Size	meta_mem;
-	Size	hash_mem;
+	Size	meta_mem = 0;
+	Size	hash_mem = 0;
 	Size	buffer_mem;
 	Size	total_mem;
 
@@ -1846,12 +1849,16 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 		aggstate->aggstrategy != AGG_HASHED)
 		return;
 
-	/* memory for the hash table itself */
-	meta_mem = MemoryContextMemAllocated(aggstate->hash_metacxt, true);
 
-	/* memory for the group keys and transition states */
-	hash_mem = MemoryContextMemAllocated(
-		aggstate->hashcontext->ecxt_per_tuple_memory, true);
+	for (int i = 0; i < aggstate->num_hashes; ++i)
+	{
+		/* memory for the hash table itself */
+		meta_mem += MemoryContextMemAllocated(
+			aggstate->perhash[i].hash_metacxt, true);
+		/* memory for the group keys and transition states */
+		hash_mem += MemoryContextMemAllocated(
+			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
+	}
 
 	/* memory for read/write tape buffers, if spilled */
 	buffer_mem = npartitions * HASHAGG_WRITE_BUFFER_SIZE;
@@ -2557,9 +2564,11 @@ agg_refill_hash_table(AggState *aggstate)
 		aggstate->all_pergroups[setoff] = NULL;
 
 	/* free memory and reset hash tables */
-	ReScanExprContext(aggstate->hashcontext);
 	for (int setno = 0; setno < aggstate->num_hashes; setno++)
+	{
+		ReScanExprContext(aggstate->perhash[setno].hashcontext);
 		ResetTupleHashTable(aggstate->perhash[setno].hashtable);
+	}
 
 	aggstate->hash_ngroups_current = 0;
 
@@ -3217,6 +3226,10 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 	aggstate->aggcontexts = (ExprContext **)
 		palloc0(sizeof(ExprContext *) * numGroupingSets);
 
+	aggstate->num_hashes = numHashes;
+	if (numHashes)
+		aggstate->perhash = palloc0(sizeof(AggStatePerHashData) * numHashes);
+
 	/*
 	 * Create expression contexts.  We need three or more, one for
 	 * per-input-tuple processing, one for per-output-tuple processing, one
@@ -3240,10 +3253,10 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 		aggstate->aggcontexts[i] = aggstate->ss.ps.ps_ExprContext;
 	}
 
-	if (use_hashing)
+	for (i = 0 ; i < numHashes; ++i)
 	{
 		ExecAssignExprContext(estate, &aggstate->ss.ps);
-		aggstate->hashcontext = aggstate->ss.ps.ps_ExprContext;
+		aggstate->perhash[i].hashcontext = aggstate->ss.ps.ps_ExprContext;
 	}
 
 	ExecAssignExprContext(estate, &aggstate->ss.ps);
@@ -3332,11 +3345,8 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 	 * compare functions.  Accumulate all_grouped_cols in passing.
 	 */
 	aggstate->phases = palloc0(numPhases * sizeof(AggStatePerPhaseData));
-
-	aggstate->num_hashes = numHashes;
 	if (numHashes)
 	{
-		aggstate->perhash = palloc0(sizeof(AggStatePerHashData) * numHashes);
 		aggstate->phases[0].numsets = 0;
 		aggstate->phases[0].gset_lengths = palloc(numHashes * sizeof(int));
 		aggstate->phases[0].grouped_cols = palloc(numHashes * sizeof(Bitmapset *));
@@ -3528,10 +3538,12 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 		uint64	totalGroups = 0;
 		int 	i;
 
-		aggstate->hash_metacxt = AllocSetContextCreate(
-			aggstate->ss.ps.state->es_query_cxt,
-			"HashAgg meta context",
-			ALLOCSET_DEFAULT_SIZES);
+		for (i = 0; i < aggstate->num_hashes; i++)
+			aggstate->perhash[i].hash_metacxt = AllocSetContextCreate(
+				aggstate->ss.ps.state->es_query_cxt,
+				"HashAgg meta context",
+				ALLOCSET_DEFAULT_SIZES);
+
 		aggstate->hash_spill_slot = ExecInitExtraTupleSlot(
 			estate, scanDesc, &TTSOpsMinimalTuple);
 
@@ -4461,10 +4473,11 @@ ExecEndAgg(AggState *node)
 
 	hashagg_reset_spill_state(node);
 
-	if (node->hash_metacxt != NULL)
+	for (setno = 0; setno < node->num_hashes; setno++)
 	{
-		MemoryContextDelete(node->hash_metacxt);
-		node->hash_metacxt = NULL;
+		MemoryContext *metacxt = &node->perhash[setno].hash_metacxt;
+		MemoryContextDelete(*metacxt);
+		*metacxt = NULL;
 	}
 
 	for (transno = 0; transno < node->numtrans; transno++)
@@ -4481,8 +4494,8 @@ ExecEndAgg(AggState *node)
 	/* And ensure any agg shutdown callbacks have been called */
 	for (setno = 0; setno < numGroupingSets; setno++)
 		ReScanExprContext(node->aggcontexts[setno]);
-	if (node->hashcontext)
-		ReScanExprContext(node->hashcontext);
+	for (setno = 0; setno < node->num_hashes; setno++)
+		ReScanExprContext(node->perhash[setno].hashcontext);
 
 	/*
 	 * We don't actually free any ExprContexts here (see comment in
@@ -4591,7 +4604,8 @@ ExecReScanAgg(AggState *node)
 		node->hash_spill_mode = false;
 		node->hash_ngroups_current = 0;
 
-		ReScanExprContext(node->hashcontext);
+		for (setno = 0; setno < node->num_hashes; setno++)
+			ReScanExprContext(node->perhash[setno].hashcontext);
 		/* Rebuild an empty hash table */
 		build_hash_tables(node);
 		node->table_filled = false;
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index a5b8a004d1..247bb65bd6 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -307,6 +307,8 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	MemoryContext	hash_metacxt;	/* memory for hash table itself */
+	ExprContext	*hashcontext;	/* context for hash table data */
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 3d27d50f09..ddf0b43916 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2051,7 +2051,6 @@ typedef struct AggState
 	int			current_phase;	/* current phase number */
 	AggStatePerAgg peragg;		/* per-Aggref information */
 	AggStatePerTrans pertrans;	/* per-Trans state information */
-	ExprContext *hashcontext;	/* econtexts for long-lived data (hashtable) */
 	ExprContext **aggcontexts;	/* econtexts for long-lived data (per GS) */
 	ExprContext *tmpcontext;	/* econtext for input expressions */
 #define FIELDNO_AGGSTATE_CURAGGCONTEXT 14
@@ -2079,7 +2078,6 @@ typedef struct AggState
 	/* these fields are used in AGG_HASHED and AGG_MIXED modes: */
 	bool		table_filled;	/* hash table filled yet? */
 	int			num_hashes;
-	MemoryContext	hash_metacxt;	/* memory for hash table itself */
 	struct HashTapeInfo *hash_tapeinfo; /* metadata for spill tapes */
 	struct HashAggSpill *hash_spills; /* HashAggSpill for each grouping set,
 										 exists only during first pass */
-- 
2.17.0

v8-0002-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From 8fb9202fdfa04cf0c13ba3637b2c0a5365380eac Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v8 2/8] explain to show tuplehash bucket and memory stats..

Note that hashed SubPlan and recursiveUnion aren't affected in explain output,
probably since hashtables aren't allocated at that point.

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 .../postgres_fdw/expected/postgres_fdw.out    |  56 ++++--
 src/backend/commands/explain.c                | 168 ++++++++++++++----
 src/backend/executor/execGrouping.c           |  33 ++++
 src/backend/executor/nodeAgg.c                |  15 +-
 src/backend/executor/nodeRecursiveunion.c     |   3 +
 src/backend/executor/nodeSetOp.c              |   1 +
 src/backend/executor/nodeSubplan.c            |   3 +
 src/include/executor/executor.h               |   1 +
 src/include/nodes/execnodes.h                 |  10 +-
 9 files changed, 227 insertions(+), 63 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62c2697920..2ddae83178 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2086,9 +2086,11 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
          ->  HashAggregate
                Output: t1.c1, avg((t1.c1 + t2.c1))
                Group Key: t1.c1
+               Buckets: 256
                ->  HashAggregate
                      Output: t1.c1, t2.c1
                      Group Key: t1.c1, t2.c1
+                     Buckets: 4096
                      ->  Append
                            ->  Foreign Scan
                                  Output: t1.c1, t2.c1
@@ -2098,7 +2100,7 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                                  Output: t1_1.c1, t2_1.c1
                                  Relations: (public.ft1 t1_1) INNER JOIN (public.ft2 t2_1)
                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
-(20 rows)
+(22 rows)
 
 SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
  t1c1 |         avg          
@@ -2129,11 +2131,12 @@ SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
          ->  HashAggregate
                Output: t2.c1, t3.c1
                Group Key: t2.c1, t3.c1
+               Buckets: 2
                ->  Foreign Scan
                      Output: t2.c1, t3.c1
                      Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
-(13 rows)
+(14 rows)
 
 SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  C 1 
@@ -2610,10 +2613,11 @@ select c2 * (random() <= 1)::int as c2 from ft2 group by c2 * (random() <= 1)::i
    ->  HashAggregate
          Output: ((c2 * ((random() <= '1'::double precision))::integer))
          Group Key: (ft2.c2 * ((random() <= '1'::double precision))::integer)
+         Buckets: 2
          ->  Foreign Scan on public.ft2
                Output: (c2 * ((random() <= '1'::double precision))::integer)
                Remote SQL: SELECT c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 -- GROUP BY clause in various forms, cardinal, alias and constant expression
 explain (verbose, costs off)
@@ -2713,11 +2717,12 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
    ->  HashAggregate
          Output: sum(c1), c2
          Group Key: ft1.c2
+         Buckets: 16
          Filter: (avg((ft1.c1 * ((random() <= '1'::double precision))::integer)) > '100'::numeric)
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(10 rows)
+(11 rows)
 
 -- Remote aggregate in combination with a local Param (for the output
 -- of an initplan) can be trouble, per bug #15781
@@ -2963,10 +2968,11 @@ select sum(c1) filter (where (c1 / c1) * random() <= 1) from ft1 group by c2 ord
    ->  HashAggregate
          Output: sum(c1) FILTER (WHERE ((((c1 / c1))::double precision * random()) <= '1'::double precision)), c2
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 explain (verbose, costs off)
 select sum(c2) filter (where c2 in (select c2 from ft1 where c2 < 5)) from ft1;
@@ -3229,6 +3235,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
    ->  HashAggregate
          Output: count(*), x.b
          Group Key: x.b
+         Buckets: 16
          ->  Hash Join
                Output: x.b
                Inner Unique: true
@@ -3244,7 +3251,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
                                  Output: ft1_1.c2, (sum(ft1_1.c1))
                                  Relations: Aggregate on (public.ft1 ft1_1)
                                  Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1
-(21 rows)
+(22 rows)
 
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
  count |   b   
@@ -3449,11 +3456,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls la
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3474,11 +3482,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3499,11 +3508,13 @@ select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) orde
    ->  HashAggregate
          Output: c2, c6, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Hash Key: ft1.c6
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c6, c1
                Remote SQL: SELECT "C 1", c2, c6 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(12 rows)
 
 select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) order by 1 nulls last, 2 nulls last;
  c2 | c6 |  sum  
@@ -3526,10 +3537,11 @@ select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nu
    ->  HashAggregate
          Output: c2, sum(c1), GROUPING(c2)
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(9 rows)
+(10 rows)
 
 select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nulls last;
  c2 |  sum  | grouping 
@@ -7147,13 +7159,14 @@ select * from bar where f1 in (select f1 from foo) for update;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for update;
  f1 | f2 
@@ -7185,13 +7198,14 @@ select * from bar where f1 in (select f1 from foo) for share;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for share;
  f1 | f2 
@@ -7222,6 +7236,7 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
@@ -7240,13 +7255,14 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(39 rows)
+(41 rows)
 
 update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
 select tableoid::regclass, * from bar order by 1,2;
@@ -8751,12 +8767,13 @@ SELECT a, sum(b), min(b), count(*) FROM pagg_tab GROUP BY a HAVING avg(b) < 22 O
    Sort Key: pagg_tab.a
    ->  HashAggregate
          Group Key: pagg_tab.a
+         Buckets: 64
          Filter: (avg(pagg_tab.b) < '22'::numeric)
          ->  Append
                ->  Foreign Scan on fpagg_tab_p1 pagg_tab_1
                ->  Foreign Scan on fpagg_tab_p2 pagg_tab_2
                ->  Foreign Scan on fpagg_tab_p3 pagg_tab_3
-(9 rows)
+(10 rows)
 
 -- Plan with partitionwise aggregates is enabled
 SET enable_partitionwise_aggregate TO true;
@@ -8799,6 +8816,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1.a, count(((t1.*)::pagg_tab))
                Group Key: t1.a
+               Buckets: 16
                Filter: (avg(t1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p1 t1
                      Output: t1.a, t1.*, t1.b
@@ -8806,6 +8824,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_1.a, count(((t1_1.*)::pagg_tab))
                Group Key: t1_1.a
+               Buckets: 16
                Filter: (avg(t1_1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p2 t1_1
                      Output: t1_1.a, t1_1.*, t1_1.b
@@ -8813,11 +8832,12 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_2.a, count(((t1_2.*)::pagg_tab))
                Group Key: t1_2.a
+               Buckets: 16
                Filter: (avg(t1_2.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p3 t1_2
                      Output: t1_2.a, t1_2.*, t1_2.b
                      Remote SQL: SELECT a, b, c FROM public.pagg_tab_p3
-(25 rows)
+(28 rows)
 
 SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
  a  | count 
@@ -8839,18 +8859,22 @@ SELECT b, avg(a), max(a), count(*) FROM pagg_tab GROUP BY b HAVING sum(a) < 700
    Sort Key: pagg_tab.b
    ->  Finalize HashAggregate
          Group Key: pagg_tab.b
+         Buckets: 64
          Filter: (sum(pagg_tab.a) < 700)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p1 pagg_tab
                ->  Partial HashAggregate
                      Group Key: pagg_tab_1.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p2 pagg_tab_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_2.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p3 pagg_tab_2
-(15 rows)
+(19 rows)
 
 -- ===================================================================
 -- access rights and superuser
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 58141d8393..1d9623619b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -86,12 +87,14 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *aggstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_info(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors,
+								   HashTableInstrumentation *inst,
+								   ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -104,7 +107,8 @@ static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
-static void show_hashagg_info(AggState *hashstate, ExplainState *es);
+static void show_tuplehash_info(HashTableInstrumentation *inst, AggState *as,
+		ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1490,6 +1494,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1883,11 +1888,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_Agg:
 			show_agg_keys(castNode(AggState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
-			show_hashagg_info((AggState *) planstate, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, NULL, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, NULL, es);
+			}
+			break;
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2264,24 +2282,31 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
+			show_grouping_sets(astate, plan, ancestors, es);
 		else
+		{
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes <= 1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, astate, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
+	int			setno = 0;
 
 	/* Set up deparsing context */
 	context = set_deparse_context_plan(es->deparse_cxt,
@@ -2291,27 +2316,41 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
+			aggstate->num_hashes ?
+			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			es);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		HashTableInstrumentation *inst = NULL;
+
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+		{
+			Assert(setno < aggstate->num_hashes);
+			inst = &aggstate->perhash[setno++].hashtable->instrument;
+		}
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		show_grouping_set_info(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors,
+							   inst, es);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
+/* Show keys and any hash instrumentation for a grouping set */
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_info(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, HashTableInstrumentation *inst,
+					   ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2375,6 +2414,10 @@ show_grouping_set_keys(PlanState *planstate,
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
 
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, NULL, es);
+
 	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
 		es->indent--;
 
@@ -2772,37 +2815,73 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 }
 
 /*
- * Show information on hash aggregate memory usage and batches.
+ * Show hash bucket stats and (optionally) memory.
  */
 static void
-show_hashagg_info(AggState *aggstate, ExplainState *es)
+show_tuplehash_info(HashTableInstrumentation *inst, AggState *aggstate, ExplainState *es)
 {
-	Agg		*agg	   = (Agg *)aggstate->ss.ps.plan;
-	long	 memPeakKb = (aggstate->hash_mem_peak + 1023) / 1024;
+	size_t	spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
 
-	Assert(IsA(aggstate, AggState));
-
-	if (agg->aggstrategy != AGG_HASHED &&
-		agg->aggstrategy != AGG_MIXED)
-		return;
-
-	if (es->costs && aggstate->hash_planned_partitions > 0)
-	{
+	if (es->costs && aggstate!=NULL && aggstate->hash_planned_partitions > 0)
 		ExplainPropertyInteger("Planned Partitions", NULL,
 							   aggstate->hash_planned_partitions, es);
-	}
 
 	if (!es->analyze)
 		return;
 
-	/* EXPLAIN ANALYZE */
-	ExplainPropertyInteger("Peak Memory Usage", "kB", memPeakKb, es);
-	if (aggstate->hash_batches_used > 0)
+	if (es->format != EXPLAIN_FORMAT_TEXT)
 	{
-		ExplainPropertyInteger("Disk Usage", "kB",
-							   aggstate->hash_disk_used, es);
-		ExplainPropertyInteger("HashAgg Batches", NULL,
-							   aggstate->hash_batches_used, es);
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+		if (aggstate != NULL)
+		{
+			ExplainPropertyInteger("Disk Usage", "kB",
+								   aggstate->hash_disk_used, es);
+			ExplainPropertyInteger("HashAgg Batches", NULL,
+								   aggstate->hash_batches_used, es);
+		}
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
+	{
+		if (inst->nbuckets_original != inst->nbuckets)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %lld (originally %lld)",
+						(long long)inst->nbuckets,
+						(long long)inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %lld",
+						(long long)inst->nbuckets);
+		}
+
+		appendStringInfoChar(es->str, '\n');
+		ExplainIndentText(es);
+		appendStringInfo(es->str,
+				"Peak Memory Usage: hashtable: %lldkB, tuples: %lldkB",
+				(long long)spacePeakKb_hash, (long long)spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+
+		if (aggstate!=NULL && aggstate->hash_batches_used > 0)
+		{
+			ExplainPropertyInteger("Disk Usage", "kB",
+								   aggstate->hash_disk_used, es);
+			ExplainPropertyInteger("HashAgg Batches", NULL,
+								   aggstate->hash_batches_used, es);
+		}
 	}
 }
 
@@ -3473,6 +3552,29 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashtable->instrument, NULL, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (sps->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, NULL, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..10276d3f58 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,6 +188,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -203,6 +204,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -281,9 +283,40 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial)
+	{
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		// hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
+			// sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_hash =
+			MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+		/* hashtable->entrysize includes additionalsize */
+		size_t hash_size = MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
+		size_t tuple_size = MemoryContextMemAllocated(hashtable->tablecxt, true);
+
+		hashtable->instrument.space_peak_hash = Max(
+			hashtable->instrument.space_peak_hash,
+			hash_size);
+
+		hashtable->instrument.space_peak_tuples = Max(
+			hashtable->instrument.space_peak_tuples, tuple_size);
+				// hashtable->hashtab->members * hashtable->entrysize);
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 1d319f49d0..daa82cdee2 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1840,36 +1840,25 @@ hash_agg_enter_spill_mode(AggState *aggstate)
 static void
 hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 {
-	Size	meta_mem = 0;
 	Size	hash_mem = 0;
 	Size	buffer_mem;
-	Size	total_mem;
 
 	if (aggstate->aggstrategy != AGG_MIXED &&
 		aggstate->aggstrategy != AGG_HASHED)
 		return;
 
-
 	for (int i = 0; i < aggstate->num_hashes; ++i)
 	{
-		/* memory for the hash table itself */
-		meta_mem += MemoryContextMemAllocated(
-			aggstate->perhash[i].hash_metacxt, true);
-		/* memory for the group keys and transition states */
 		hash_mem += MemoryContextMemAllocated(
 			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
+		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
 	}
 
-	/* memory for read/write tape buffers, if spilled */
+	/* memory for read/write tape buffers, if spilled XXX */
 	buffer_mem = npartitions * HASHAGG_WRITE_BUFFER_SIZE;
 	if (from_tape)
 		buffer_mem += HASHAGG_READ_BUFFER_SIZE;
 
-	/* update peak mem */
-	total_mem = meta_mem + hash_mem + buffer_mem;
-	if (total_mem > aggstate->hash_mem_peak)
-		aggstate->hash_mem_peak = total_mem;
-
 	/* update disk usage */
 	if (aggstate->hash_tapeinfo != NULL)
 	{
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a1ed..93272c28b1 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a41a..9c0e0ab96e 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 298b7757f5..22c32612ba 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 94890512dc..f4f2ede207 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ddf0b43916..63bbf22955 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,6 +691,14 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+typedef struct HashTableInstrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;		/* peak memory usage in bytes */
+	size_t	space_peak_tuples;		/* peak memory usage in bytes */
+} HashTableInstrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -709,6 +717,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -2091,7 +2100,6 @@ typedef struct AggState
 	int			hash_planned_partitions; /* number of partitions planned
 											for first pass */
 	double		hashentrysize;	/* estimate revised during execution */
-	Size		hash_mem_peak;	/* peak hash table memory usage */
 	uint64		hash_ngroups_current;	/* number of groups currently in
 										   memory in all hash tables */
 	uint64		hash_disk_used; /* kB of disk space used */
-- 
2.17.0

v8-0003-refactor-show_grouping_set_keys.patchtext/x-diff; charset=us-asciiDownload
From af059e7435f0f8543cfb1dd6a297811e6bb201c4 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 23 Feb 2020 23:13:07 -0600
Subject: [PATCH v8 3/8] refactor show_grouping_set_keys

---
 src/backend/commands/explain.c | 55 ++++++++++++++++++++--------------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1d9623619b..9482f68c4b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -95,6 +95,8 @@ static void show_grouping_set_info(AggState *aggstate,
 								   List *ancestors,
 								   HashTableInstrumentation *inst,
 								   ExplainState *es);
+static void show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List
+		*context, bool useprefix, ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -2349,6 +2351,37 @@ show_grouping_set_info(AggState *aggstate,
 					   List *context, bool useprefix,
 					   List *ancestors, HashTableInstrumentation *inst,
 					   ExplainState *es)
+{
+	PlanState	*planstate = outerPlanState(aggstate);
+
+	ExplainOpenGroup("Grouping Set", NULL, true, es);
+
+	if (sortnode)
+	{
+		show_sort_group_keys(planstate, "Sort Key",
+							 sortnode->numCols, sortnode->sortColIdx,
+							 sortnode->sortOperators, sortnode->collations,
+							 sortnode->nullsFirst,
+							 ancestors, es);
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			es->indent++;
+	}
+
+	show_grouping_set_keys(aggstate, aggnode, context, useprefix, es);
+
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, NULL, es);
+
+	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
+		es->indent--;
+
+	ExplainCloseGroup("Grouping Set", NULL, true, es);
+}
+
+/* Show keys of a grouping set */
+static void
+show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List *context, bool useprefix, ExplainState *es)
 {
 	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
@@ -2370,19 +2403,6 @@ show_grouping_set_info(AggState *aggstate,
 		keysetname = "Group Keys";
 	}
 
-	ExplainOpenGroup("Grouping Set", NULL, true, es);
-
-	if (sortnode)
-	{
-		show_sort_group_keys(planstate, "Sort Key",
-							 sortnode->numCols, sortnode->sortColIdx,
-							 sortnode->sortOperators, sortnode->collations,
-							 sortnode->nullsFirst,
-							 ancestors, es);
-		if (es->format == EXPLAIN_FORMAT_TEXT)
-			es->indent++;
-	}
-
 	ExplainOpenGroup(keysetname, keysetname, false, es);
 
 	foreach(lc, gsets)
@@ -2413,15 +2433,6 @@ show_grouping_set_info(AggState *aggstate,
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
-
-	if (aggnode->aggstrategy == AGG_HASHED ||
-			aggnode->aggstrategy == AGG_MIXED)
-		show_tuplehash_info(inst, NULL, es);
-
-	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
-		es->indent--;
-
-	ExplainCloseGroup("Grouping Set", NULL, true, es);
 }
 
 /*
-- 
2.17.0

v8-0004-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From c77d2d07f58b52721c889d99c859594c866fbaa4 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v8 4/8] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c | 70 ++++++++++++++++++----------------
 1 file changed, 38 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 9482f68c4b..786d3ded16 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						SubPlanState *subplanstate, ExplainState *es);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -722,7 +722,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, NULL, es);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1084,7 +1084,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			SubPlanState *subplanstate, ExplainState *es)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1341,6 +1341,21 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+			}
+			if (subplanstate->hashnulls)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1369,6 +1384,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+
+		if (subplanstate && subplanstate->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (subplanstate && subplanstate->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2041,12 +2070,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, NULL, es);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, NULL, es);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2078,7 +2107,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, NULL, es);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3504,7 +3533,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, NULL, es);
 }
 
 /*
@@ -3562,30 +3591,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-		{
-			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashtable->instrument, NULL, es);
-			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
-		}
-
-		if (sps->hashnulls)
-		{
-			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, NULL, es);
-			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
-		}
+					relationship, sp->plan_name, sps, es);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3602,7 +3608,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, NULL, es);
 }
 
 /*
-- 
2.17.0

v8-0005-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From 54bdb638365e810c0a7ab53253086e380824c177 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v8 5/8] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.

Note, this doesn't affect any regression tests, since hashtable isn't allocated
during "explain".  Note that "explain analyze" would show memory stats, which
we'd have to filter.
---
 src/backend/commands/explain.c            |  5 ++--
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 29 +++++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 786d3ded16..ba926bc216 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1735,8 +1735,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
-			if (es->analyze)
-				show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
+			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
 			break;
 		case T_SampleScan:
 			show_tablesample(((SampleScan *) plan)->tablesample,
@@ -2951,6 +2950,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es, NULL);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index 726d3a2d9a..1785c78091 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -741,6 +743,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index ad4e071ca3..ab81cce3b2 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -44,6 +44,7 @@
 #include "common/hashfn.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	HashTableInstrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1147,6 +1150,32 @@ tbm_end_iterate(TBMIterator *iterator)
 	pfree(iterator);
 }
 
+/*
+ * tbm_instrumentation - update stored stats and return pointer to
+ * instrumentation structure
+ *
+ * This updates stats when called.
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+HashTableInstrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable)
+	{
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+
+		/*
+		 * If there are lossy pages, then at one point, we filled maxentries;
+		 * otherwise, number of pages is "->members".
+		 */
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) *
+			(tbm->nchunks>0 ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
 /*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 63bbf22955..972c7a00cf 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1609,6 +1609,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	HashTableInstrumentation instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fcae34..de0cdfb91f 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct HashTableInstrumentation HashTableInstrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern HashTableInstrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.17.0

v8-0006-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From 1b99dba4d2116b4a2230dfbf3718e3c9a66c78cd Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v8 6/8] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.

This might be unimportant and maybe clearer left in execGrouping.c.
---
 src/backend/commands/explain.c            | 20 +++++++-------
 src/backend/executor/execGrouping.c       | 33 -----------------------
 src/backend/executor/nodeAgg.c            |  9 +++++--
 src/backend/executor/nodeRecursiveunion.c |  4 ++-
 src/backend/executor/nodeSetOp.c          |  6 ++++-
 src/backend/executor/nodeSubplan.c        | 12 +++++++--
 src/include/executor/executor.h           |  1 -
 src/include/executor/nodeAgg.h            |  1 +
 src/include/nodes/execnodes.h             | 24 ++++++++++++++++-
 9 files changed, 59 insertions(+), 51 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ba926bc216..309e7259fc 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1348,13 +1348,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+				show_tuplehash_info(&subplanstate->instrument, NULL, es);
 			}
 			if (subplanstate->hashnulls)
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, NULL, es);
 			}
 		}
 		if (es->indent)
@@ -1388,14 +1388,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (subplanstate && subplanstate->hashtable)
 		{
 			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+			show_tuplehash_info(&subplanstate->instrument, NULL, es);
 			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
 		}
 
 		if (subplanstate && subplanstate->hashnulls)
 		{
 			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, NULL, es);
 			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
 		}
 	}
@@ -1926,14 +1926,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, NULL, es);
+					show_tuplehash_info(&sos->instrument, NULL, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, NULL, es);
+					show_tuplehash_info(&rus->instrument, NULL, es);
 			}
 			break;
 		case T_Group:
@@ -2321,7 +2321,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes <= 1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, astate, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, astate, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2348,7 +2348,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 
 	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
 			aggstate->num_hashes ?
-			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			&aggstate->perhash[setno++].instrument : NULL,
 			es);
 
 	foreach(lc, agg->chain)
@@ -2361,7 +2361,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED)
 		{
 			Assert(setno < aggstate->num_hashes);
-			inst = &aggstate->perhash[setno++].hashtable->instrument;
+			inst = &aggstate->perhash[setno++].instrument;
 		}
 
 		show_grouping_set_info(aggstate, aggnode, sortnode,
@@ -2951,7 +2951,7 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 		}
 	}
 
-	show_tuplehash_info(&planstate->instrument, es, NULL);
+	show_tuplehash_info(&planstate->instrument, NULL, es);
 }
 
 /*
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 10276d3f58..009d27b9a8 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,7 +188,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -204,7 +203,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -283,40 +281,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial)
-	{
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		// hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
-			// sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_hash =
-			MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-		/* hashtable->entrysize includes additionalsize */
-		size_t hash_size = MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
-		size_t tuple_size = MemoryContextMemAllocated(hashtable->tablecxt, true);
-
-		hashtable->instrument.space_peak_hash = Max(
-			hashtable->instrument.space_peak_hash,
-			hash_size);
-
-		hashtable->instrument.space_peak_tuples = Max(
-			hashtable->instrument.space_peak_tuples, tuple_size);
-				// hashtable->hashtab->members * hashtable->entrysize);
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index daa82cdee2..4cd09e2291 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1493,6 +1493,10 @@ build_hash_table(AggState *aggstate, int setno, long nbuckets)
 		hashcxt,
 		tmpcxt,
 		DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+
+	InitTupleHashTableStats(perhash->instrument,
+			perhash->hashtable->hashtab,
+			hashcxt, additionalsize);
 }
 
 /*
@@ -1851,7 +1855,8 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 	{
 		hash_mem += MemoryContextMemAllocated(
 			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
-		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+		UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+				aggstate->perhash[i].hashtable->hashtab);
 	}
 
 	/* memory for read/write tape buffers, if spilled XXX */
@@ -1879,7 +1884,7 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 	if (aggstate->hash_ngroups_current > 0)
 	{
 		aggstate->hashentrysize =
-			hash_mem / (double)aggstate->hash_ngroups_current;
+			0 / (double)aggstate->hash_ngroups_current; // XXX
 	}
 }
 
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c28b1..5e70e008e5 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,8 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, rustate->tableContext, 0);
 }
 
 
@@ -157,7 +159,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab96e..5eca128183 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,9 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+
+	InitTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab, setopstate->tableContext, 0);
 }
 
 /*
@@ -415,7 +418,8 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 22c32612ba..0de6be40e4 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -505,6 +505,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -518,6 +519,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab,
+				node->hashtablecxt, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -533,6 +537,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashnulls);
 		else
+		{
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -546,6 +551,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls,
+					node->hashnulls->hashtab, node->hashtablecxt, 0);
+		}
 	}
 	else
 		node->hashnulls = NULL;
@@ -621,9 +629,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index f4f2ede207..94890512dc 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 247bb65bd6..71ba2011cd 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -309,6 +309,7 @@ typedef struct AggStatePerHashData
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
 	MemoryContext	hash_metacxt;	/* memory for hash table itself */
 	ExprContext	*hashcontext;	/* context for hash table data */
+	HashTableInstrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 972c7a00cf..e2cfce8bad 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -691,12 +691,31 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, tupctx, addsize) \
+	do{\
+	instr.entrysize = sizeof(MinimalTuple) + addsize; \
+	instr.tuplectx = tupctx; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = MemoryContextMemAllocated(htable->ctx, false); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, MemoryContextMemAllocated(htable->ctx, false)); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, MemoryContextMemAllocated(instr.tuplectx, false)); \
+	}while(0)
+
 typedef struct HashTableInstrumentation
 {
+	size_t	entrysize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;		/* peak memory usage in bytes */
 	size_t	space_peak_tuples;		/* peak memory usage in bytes */
+	MemoryContext	tuplectx;		/* Context where tuples are stored */
 } HashTableInstrumentation;
 
 typedef struct TupleHashTableData
@@ -717,7 +736,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -883,6 +901,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	HashTableInstrumentation instrument;
+	HashTableInstrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1291,6 +1311,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	HashTableInstrumentation instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -2341,6 +2362,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	HashTableInstrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.17.0

v8-0007-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From 64d80fa1a10c1d8961295328c1461e33960b6eab Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v8 7/8] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..98fd4bf8bd 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -183,7 +183,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e2cfce8bad..82b2f510f4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -728,7 +728,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.17.0

v8-0008-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From aeb814f18051ed47f18c1c2502a5ba06187eb853 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v8 8/8] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 4cd09e2291..68fe4d3591 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -2025,8 +2025,7 @@ lookup_hash_entry(AggState *aggstate, uint32 hash)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  *
-- 
2.17.0

#21Justin Pryzby
pryzby@telsasoft.com
In reply to: Justin Pryzby (#20)
8 attachment(s)
Re: explain HashAggregate to report bucket and memory stats

On Fri, Mar 20, 2020 at 03:44:42AM -0500, Justin Pryzby wrote:

On Fri, Mar 13, 2020 at 10:57:43AM -0700, Andres Freund wrote:

On 2020-03-13 10:53:17 -0700, Jeff Davis wrote:

On Fri, 2020-03-13 at 10:27 -0700, Andres Freund wrote:

On 2020-03-13 10:15:46 -0700, Jeff Davis wrote:

Also, is there a reason you report two different memory values
(hashtable and tuples)? I don't object, but it seems like a little too
much detail.

Seems useful to me - the hashtable is pre-allocated based on estimates,
whereas the tuples are allocated "on demand". So seeing the difference
will allow to investigate the more crucial issue...

Then do we also want to report separately on the by-ref transition
values? That could be useful if you are using ARRAY_AGG and the states
grow larger than you might expect.

I can see that being valuable - I've had to debug cases with too much
memory being used due to aggregate transitions before. Right now it'd be
mixed in with tuples, I believe - and we'd need a separate context for
tracking the transition values? Due to that I'm inclined to not report
separately for now.

I think that's already in a separate context indexed by grouping set:
src/include/nodes/execnodes.h: ExprContext **aggcontexts; /* econtexts for long-lived data (per GS) */

But the hashtable and tuples are combined. I put them in separate contexts and
rebased on top of 1f39bce021540fde00990af55b4432c55ef4b3c7.

I forgot to say that I'd also switched started using memory context based
accounting.

90% of the initial goal of this patch was handled by instrumentation added by
"hash spill to disk" (1f39bce02), but this *also* adds:

- separate accounting for tuples vs hashtable;
- number of hash buckets;
- handles other agg nodes, and bitmap scan;

Should I continue pursuing this patch?
Does it still serve any significant purpose?

template1=# explain (analyze, costs off, summary off) SELECT a, COUNT(1) FROM generate_series(1,999999) a GROUP BY 1 ;
HashAggregate (actual time=1070.713..2287.011 rows=999999 loops=1)
Group Key: a
Buckets: 32768 (originally 512)
Peak Memory Usage: hashtable: 777kB, tuples: 4096kB
Disk Usage: 22888 kB
HashAgg Batches: 84
-> Function Scan on generate_series a (actual time=238.270..519.832 rows=999999 loops=1)

template1=# explain analyze SELECT * FROM t WHERE a BETWEEN 999 AND 99999;
Bitmap Heap Scan on t (cost=4213.01..8066.67 rows=197911 width=4) (actual time=26.803..84.693 rows=198002 loops=1)
Recheck Cond: ((a >= 999) AND (a <= 99999))
Heap Blocks: exact=878
Buckets: 1024 (originally 256)
Peak Memory Usage: hashtable: 48kB, tuples: 4kB

template1=# explain analyze SELECT generate_series(1,99999) EXCEPT SELECT generate_series(1,999);
HashSetOp Except (cost=0.00..2272.49 rows=99999 width=8) (actual time=135.986..174.656 rows=99000 loops=1)
Buckets: 262144 (originally 131072)
Peak Memory Usage: hashtable: 6177kB, tuples: 8192kB

@cfbot: rebased

Attachments:

v9-0001-nodeAgg-separate-context-for-each-hashtable.patchtext/x-diff; charset=us-asciiDownload
From babe9bad1a310efdd42b54b0dfcaf76da123dbdf Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Thu, 19 Mar 2020 23:03:25 -0500
Subject: [PATCH v9 1/8] nodeAgg: separate context for each hashtable

---
 src/backend/executor/execExpr.c |  2 +-
 src/backend/executor/nodeAgg.c  | 85 ++++++++++++++++++++-------------
 src/include/executor/nodeAgg.h  |  2 +
 src/include/nodes/execnodes.h   |  2 -
 4 files changed, 54 insertions(+), 37 deletions(-)

diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 1370ffec50..039c5a8b5f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -3241,7 +3241,7 @@ ExecBuildAggTransCall(ExprState *state, AggState *aggstate,
 	int adjust_jumpnull = -1;
 
 	if (ishash)
-		aggcontext = aggstate->hashcontext;
+		aggcontext = aggstate->perhash[setno].hashcontext;
 	else
 		aggcontext = aggstate->aggcontexts[setno];
 
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 44587a84ba..48b0274b2e 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -191,8 +191,7 @@
  *	  So we create an array, aggcontexts, with an ExprContext for each grouping
  *	  set in the largest rollup that we're going to process, and use the
  *	  per-tuple memory context of those ExprContexts to store the aggregate
- *	  transition values.  hashcontext is the single context created to support
- *	  all hash tables.
+ *	  transition values.
  *
  *	  Spilling To Disk
  *
@@ -464,7 +463,7 @@ select_current_set(AggState *aggstate, int setno, bool is_hash)
 	 * ExecAggPlainTransByRef().
 	 */
 	if (is_hash)
-		aggstate->curaggcontext = aggstate->hashcontext;
+		aggstate->curaggcontext = aggstate->perhash[setno].hashcontext;
 	else
 		aggstate->curaggcontext = aggstate->aggcontexts[setno];
 
@@ -1431,8 +1430,7 @@ find_unaggregated_cols_walker(Node *node, Bitmapset **colnos)
  * grouping set for which we're doing hashing.
  *
  * The contents of the hash tables always live in the hashcontext's per-tuple
- * memory context (there is only one of these for all tables together, since
- * they are all reset at the same time).
+ * memory context.
  */
 static void
 build_hash_tables(AggState *aggstate)
@@ -1472,8 +1470,8 @@ static void
 build_hash_table(AggState *aggstate, int setno, long nbuckets)
 {
 	AggStatePerHash perhash = &aggstate->perhash[setno];
-	MemoryContext	metacxt = aggstate->hash_metacxt;
-	MemoryContext	hashcxt = aggstate->hashcontext->ecxt_per_tuple_memory;
+	MemoryContext	metacxt = perhash->hash_metacxt;
+	MemoryContext	hashcxt = perhash->hashcontext->ecxt_per_tuple_memory;
 	MemoryContext	tmpcxt	= aggstate->tmpcontext->ecxt_per_tuple_memory;
 	Size            additionalsize;
 
@@ -1803,10 +1801,15 @@ static void
 hash_agg_check_limits(AggState *aggstate)
 {
 	uint64 ngroups = aggstate->hash_ngroups_current;
-	Size meta_mem = MemoryContextMemAllocated(
-		aggstate->hash_metacxt, true);
-	Size hash_mem = MemoryContextMemAllocated(
-		aggstate->hashcontext->ecxt_per_tuple_memory, true);
+	Size meta_mem = 0;
+	Size hash_mem = 0;
+
+	for (int i = 0; i < aggstate->num_hashes; ++i)
+		meta_mem = MemoryContextMemAllocated(
+		aggstate->perhash[i].hash_metacxt, true);
+	for (int i = 0; i < aggstate->num_hashes; ++i)
+		hash_mem += MemoryContextMemAllocated(
+			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
 
 	/*
 	 * Don't spill unless there's at least one group in the hash table so we
@@ -1864,8 +1867,8 @@ hash_agg_enter_spill_mode(AggState *aggstate)
 static void
 hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 {
-	Size	meta_mem;
-	Size	hash_mem;
+	Size	meta_mem = 0;
+	Size	hash_mem = 0;
 	Size	buffer_mem;
 	Size	total_mem;
 
@@ -1873,12 +1876,16 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 		aggstate->aggstrategy != AGG_HASHED)
 		return;
 
-	/* memory for the hash table itself */
-	meta_mem = MemoryContextMemAllocated(aggstate->hash_metacxt, true);
 
-	/* memory for the group keys and transition states */
-	hash_mem = MemoryContextMemAllocated(
-		aggstate->hashcontext->ecxt_per_tuple_memory, true);
+	for (int i = 0; i < aggstate->num_hashes; ++i)
+	{
+		/* memory for the hash table itself */
+		meta_mem += MemoryContextMemAllocated(
+			aggstate->perhash[i].hash_metacxt, true);
+		/* memory for the group keys and transition states */
+		hash_mem += MemoryContextMemAllocated(
+			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
+	}
 
 	/* memory for read/write tape buffers, if spilled */
 	buffer_mem = npartitions * HASHAGG_WRITE_BUFFER_SIZE;
@@ -2591,9 +2598,11 @@ agg_refill_hash_table(AggState *aggstate)
 		aggstate->all_pergroups[setoff] = NULL;
 
 	/* free memory and reset hash tables */
-	ReScanExprContext(aggstate->hashcontext);
 	for (int setno = 0; setno < aggstate->num_hashes; setno++)
+	{
+		ReScanExprContext(aggstate->perhash[setno].hashcontext);
 		ResetTupleHashTable(aggstate->perhash[setno].hashtable);
+	}
 
 	aggstate->hash_ngroups_current = 0;
 
@@ -3253,6 +3262,10 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 	aggstate->aggcontexts = (ExprContext **)
 		palloc0(sizeof(ExprContext *) * numGroupingSets);
 
+	aggstate->num_hashes = numHashes;
+	if (numHashes)
+		aggstate->perhash = palloc0(sizeof(AggStatePerHashData) * numHashes);
+
 	/*
 	 * Create expression contexts.  We need three or more, one for
 	 * per-input-tuple processing, one for per-output-tuple processing, one
@@ -3276,8 +3289,11 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 		aggstate->aggcontexts[i] = aggstate->ss.ps.ps_ExprContext;
 	}
 
-	if (use_hashing)
-		aggstate->hashcontext = CreateWorkExprContext(estate);
+	for (i = 0 ; i < numHashes; ++i)
+	{
+		ExecAssignExprContext(estate, &aggstate->ss.ps);
+		aggstate->perhash[i].hashcontext = CreateWorkExprContext(estate);
+	}
 
 	ExecAssignExprContext(estate, &aggstate->ss.ps);
 
@@ -3365,11 +3381,8 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 	 * compare functions.  Accumulate all_grouped_cols in passing.
 	 */
 	aggstate->phases = palloc0(numPhases * sizeof(AggStatePerPhaseData));
-
-	aggstate->num_hashes = numHashes;
 	if (numHashes)
 	{
-		aggstate->perhash = palloc0(sizeof(AggStatePerHashData) * numHashes);
 		aggstate->phases[0].numsets = 0;
 		aggstate->phases[0].gset_lengths = palloc(numHashes * sizeof(int));
 		aggstate->phases[0].grouped_cols = palloc(numHashes * sizeof(Bitmapset *));
@@ -3561,10 +3574,12 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 		uint64	totalGroups = 0;
 		int 	i;
 
-		aggstate->hash_metacxt = AllocSetContextCreate(
-			aggstate->ss.ps.state->es_query_cxt,
-			"HashAgg meta context",
-			ALLOCSET_DEFAULT_SIZES);
+		for (i = 0; i < aggstate->num_hashes; i++)
+			aggstate->perhash[i].hash_metacxt = AllocSetContextCreate(
+				aggstate->ss.ps.state->es_query_cxt,
+				"HashAgg meta context",
+				ALLOCSET_DEFAULT_SIZES);
+
 		aggstate->hash_spill_slot = ExecInitExtraTupleSlot(
 			estate, scanDesc, &TTSOpsMinimalTuple);
 
@@ -4494,10 +4509,11 @@ ExecEndAgg(AggState *node)
 
 	hashagg_reset_spill_state(node);
 
-	if (node->hash_metacxt != NULL)
+	for (setno = 0; setno < node->num_hashes; setno++)
 	{
-		MemoryContextDelete(node->hash_metacxt);
-		node->hash_metacxt = NULL;
+		MemoryContext *metacxt = &node->perhash[setno].hash_metacxt;
+		MemoryContextDelete(*metacxt);
+		*metacxt = NULL;
 	}
 
 	for (transno = 0; transno < node->numtrans; transno++)
@@ -4514,8 +4530,8 @@ ExecEndAgg(AggState *node)
 	/* And ensure any agg shutdown callbacks have been called */
 	for (setno = 0; setno < numGroupingSets; setno++)
 		ReScanExprContext(node->aggcontexts[setno]);
-	if (node->hashcontext)
-		ReScanExprContext(node->hashcontext);
+	for (setno = 0; setno < node->num_hashes; setno++)
+		ReScanExprContext(node->perhash[setno].hashcontext);
 
 	/*
 	 * We don't actually free any ExprContexts here (see comment in
@@ -4624,7 +4640,8 @@ ExecReScanAgg(AggState *node)
 		node->hash_spill_mode = false;
 		node->hash_ngroups_current = 0;
 
-		ReScanExprContext(node->hashcontext);
+		for (setno = 0; setno < node->num_hashes; setno++)
+			ReScanExprContext(node->perhash[setno].hashcontext);
 		/* Rebuild an empty hash table */
 		build_hash_tables(node);
 		node->table_filled = false;
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index c2b55728bf..72a9568bbe 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -307,6 +307,8 @@ typedef struct AggStatePerHashData
 	AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */
 	AttrNumber *hashGrpColIdxHash;	/* indices in hash table tuples */
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
+	MemoryContext	hash_metacxt;	/* memory for hash table itself */
+	ExprContext	*hashcontext;	/* context for hash table data */
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4c009b1a7c..3fc5989bf7 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2133,7 +2133,6 @@ typedef struct AggState
 	int			current_phase;	/* current phase number */
 	AggStatePerAgg peragg;		/* per-Aggref information */
 	AggStatePerTrans pertrans;	/* per-Trans state information */
-	ExprContext *hashcontext;	/* econtexts for long-lived data (hashtable) */
 	ExprContext **aggcontexts;	/* econtexts for long-lived data (per GS) */
 	ExprContext *tmpcontext;	/* econtext for input expressions */
 #define FIELDNO_AGGSTATE_CURAGGCONTEXT 14
@@ -2161,7 +2160,6 @@ typedef struct AggState
 	/* these fields are used in AGG_HASHED and AGG_MIXED modes: */
 	bool		table_filled;	/* hash table filled yet? */
 	int			num_hashes;
-	MemoryContext	hash_metacxt;	/* memory for hash table itself */
 	struct HashTapeInfo *hash_tapeinfo; /* metadata for spill tapes */
 	struct HashAggSpill *hash_spills; /* HashAggSpill for each grouping set,
 										 exists only during first pass */
-- 
2.17.0

v9-0002-explain-to-show-tuplehash-bucket-and-memory-stats.patchtext/x-diff; charset=us-asciiDownload
From 03c8334c4504afe60fcd2a7f1230aaa750900ec3 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Tue, 31 Dec 2019 18:49:41 -0600
Subject: [PATCH v9 2/8] explain to show tuplehash bucket and memory stats..

Note that hashed SubPlan and recursiveUnion aren't affected in explain output,
probably since hashtables aren't allocated at that point.

Discussion: https://www.postgresql.org/message-id/flat/20200103161925.GM12066@telsasoft.com
---
 src/backend/commands/explain.c            | 173 +++++++++++++++++-----
 src/backend/executor/execGrouping.c       |  33 +++++
 src/backend/executor/nodeAgg.c            |  17 +--
 src/backend/executor/nodeRecursiveunion.c |   3 +
 src/backend/executor/nodeSetOp.c          |   1 +
 src/backend/executor/nodeSubplan.c        |   3 +
 src/include/executor/executor.h           |   1 +
 src/include/nodes/execnodes.h             |  11 +-
 8 files changed, 194 insertions(+), 48 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 455f54ef83..ecc0469d35 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -18,6 +18,7 @@
 #include "commands/createas.h"
 #include "commands/defrem.h"
 #include "commands/prepare.h"
+#include "executor/nodeAgg.h"
 #include "executor/nodeHash.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
@@ -88,12 +89,14 @@ static void show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
 								   ExplainState *es);
 static void show_agg_keys(AggState *astate, List *ancestors,
 						  ExplainState *es);
-static void show_grouping_sets(PlanState *planstate, Agg *agg,
+static void show_grouping_sets(AggState *aggstate, Agg *agg,
 							   List *ancestors, ExplainState *es);
-static void show_grouping_set_keys(PlanState *planstate,
+static void show_grouping_set_info(AggState *aggstate,
 								   Agg *aggnode, Sort *sortnode,
 								   List *context, bool useprefix,
-								   List *ancestors, ExplainState *es);
+								   List *ancestors,
+								   HashTableInstrumentation *inst,
+								   ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -108,7 +111,8 @@ static void show_sort_info(SortState *sortstate, ExplainState *es);
 static void show_incremental_sort_info(IncrementalSortState *incrsortstate,
 									   ExplainState *es);
 static void show_hash_info(HashState *hashstate, ExplainState *es);
-static void show_hashagg_info(AggState *hashstate, ExplainState *es);
+static void show_tuplehash_info(HashTableInstrumentation *inst, AggState *as,
+		ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
@@ -1535,6 +1539,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 					appendStringInfo(es->str, " %s", setopcmd);
 				else
 					ExplainPropertyText("Command", setopcmd, es);
+				// show strategy in text mode ?
 			}
 			break;
 		default:
@@ -1928,11 +1933,24 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_Agg:
 			show_agg_keys(castNode(AggState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
-			show_hashagg_info((AggState *) planstate, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
 			break;
+		case T_SetOp:
+			{
+				SetOpState *sos = castNode(SetOpState, planstate);
+				if (sos->hashtable)
+					show_tuplehash_info(&sos->hashtable->instrument, NULL, es);
+			}
+			break;
+		case T_RecursiveUnion:
+			{
+				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
+				if (rus->hashtable)
+					show_tuplehash_info(&rus->hashtable->instrument, NULL, es);
+			}
+			break;
 		case T_Group:
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
@@ -2337,24 +2355,31 @@ show_agg_keys(AggState *astate, List *ancestors,
 		ancestors = lcons(plan, ancestors);
 
 		if (plan->groupingSets)
-			show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
+			show_grouping_sets(astate, plan, ancestors, es);
 		else
+		{
 			show_sort_group_keys(outerPlanState(astate), "Group Key",
 								 plan->numCols, 0, plan->grpColIdx,
 								 NULL, NULL, NULL,
 								 ancestors, es);
+			Assert(astate->num_hashes <= 1);
+			if (astate->num_hashes)
+				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, astate, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
 }
 
 static void
-show_grouping_sets(PlanState *planstate, Agg *agg,
+show_grouping_sets(AggState *aggstate, Agg *agg,
 				   List *ancestors, ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	List	   *context;
 	bool		useprefix;
 	ListCell   *lc;
+	int			setno = 0;
 
 	/* Set up deparsing context */
 	context = set_deparse_context_plan(es->deparse_cxt,
@@ -2364,27 +2389,41 @@ show_grouping_sets(PlanState *planstate, Agg *agg,
 
 	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
 
-	show_grouping_set_keys(planstate, agg, NULL,
-						   context, useprefix, ancestors, es);
+	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
+			aggstate->num_hashes ?
+			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			es);
 
 	foreach(lc, agg->chain)
 	{
 		Agg		   *aggnode = lfirst(lc);
 		Sort	   *sortnode = (Sort *) aggnode->plan.lefttree;
+		HashTableInstrumentation *inst = NULL;
 
-		show_grouping_set_keys(planstate, aggnode, sortnode,
-							   context, useprefix, ancestors, es);
+		if (aggnode->aggstrategy == AGG_HASHED ||
+				aggnode->aggstrategy == AGG_MIXED)
+		{
+			Assert(setno < aggstate->num_hashes);
+			inst = &aggstate->perhash[setno++].hashtable->instrument;
+		}
+
+		show_grouping_set_info(aggstate, aggnode, sortnode,
+							   context, useprefix, ancestors,
+							   inst, es);
 	}
 
 	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
 }
 
+/* Show keys and any hash instrumentation for a grouping set */
 static void
-show_grouping_set_keys(PlanState *planstate,
+show_grouping_set_info(AggState *aggstate,
 					   Agg *aggnode, Sort *sortnode,
 					   List *context, bool useprefix,
-					   List *ancestors, ExplainState *es)
+					   List *ancestors, HashTableInstrumentation *inst,
+					   ExplainState *es)
 {
+	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
 	char	   *exprstr;
 	ListCell   *lc;
@@ -2448,6 +2487,10 @@ show_grouping_set_keys(PlanState *planstate,
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
 
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, NULL, es);
+
 	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
 		es->indent--;
 
@@ -3059,37 +3102,78 @@ show_hash_info(HashState *hashstate, ExplainState *es)
 }
 
 /*
- * Show information on hash aggregate memory usage and batches.
+ * Show hash bucket stats and (optionally) memory.
  */
 static void
-show_hashagg_info(AggState *aggstate, ExplainState *es)
+show_tuplehash_info(HashTableInstrumentation *inst, AggState *aggstate, ExplainState *es)
 {
-	Agg		*agg	   = (Agg *)aggstate->ss.ps.plan;
-	int64	 memPeakKb = (aggstate->hash_mem_peak + 1023) / 1024;
-
-	Assert(IsA(aggstate, AggState));
-
-	if (agg->aggstrategy != AGG_HASHED &&
-		agg->aggstrategy != AGG_MIXED)
-		return;
+	int64	 spacePeakKb_tuples = (inst->space_peak_tuples + 1023) / 1024,
+		 spacePeakKb_hash = (inst->space_peak_hash + 1023) / 1024;
 
-	if (es->costs && aggstate->hash_planned_partitions > 0)
-	{
+	if (es->costs && aggstate!=NULL && aggstate->hash_planned_partitions > 0)
 		ExplainPropertyInteger("Planned Partitions", NULL,
 							   aggstate->hash_planned_partitions, es);
-	}
 
 	if (!es->analyze)
 		return;
 
-	/* EXPLAIN ANALYZE */
-	ExplainPropertyInteger("Peak Memory Usage", "kB", memPeakKb, es);
-	if (aggstate->hash_batches_used > 0)
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainPropertyInteger("Hash Buckets", NULL,
+							   inst->nbuckets, es);
+		ExplainPropertyInteger("Original Hash Buckets", NULL,
+							   inst->nbuckets_original, es);
+		ExplainPropertyInteger("Peak Memory Usage (hashtable)", "kB",
+							   spacePeakKb_hash, es);
+		ExplainPropertyInteger("Peak Memory Usage (tuples)", "kB",
+							   spacePeakKb_tuples, es);
+		if (aggstate != NULL)
+		{
+			Agg		*agg = (Agg *)aggstate->ss.ps.plan;
+			if (agg->aggstrategy == AGG_HASHED ||
+					agg->aggstrategy == AGG_MIXED)
+			{
+				ExplainPropertyInteger("Disk Usage", "kB",
+									   aggstate->hash_disk_used, es);
+				ExplainPropertyInteger("HashAgg Batches", NULL,
+									   aggstate->hash_batches_used, es);
+			}
+		}
+	}
+	else if (!inst->nbuckets)
+		; /* Do nothing */
+	else
 	{
-		ExplainPropertyInteger("Disk Usage", "kB",
-							   aggstate->hash_disk_used, es);
-		ExplainPropertyInteger("HashAgg Batches", NULL,
-							   aggstate->hash_batches_used, es);
+		if (inst->nbuckets_original != inst->nbuckets)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %lld (originally %lld)",
+						(long long)inst->nbuckets,
+						(long long)inst->nbuckets_original);
+		}
+		else
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str,
+						"Buckets: %lld",
+						(long long)inst->nbuckets);
+		}
+
+		appendStringInfoChar(es->str, '\n');
+		ExplainIndentText(es);
+		appendStringInfo(es->str,
+				"Peak Memory Usage: hashtable: %lldkB, tuples: %lldkB",
+				(long long)spacePeakKb_hash, (long long)spacePeakKb_tuples);
+		appendStringInfoChar(es->str, '\n');
+
+		if (aggstate != NULL && aggstate->hash_batches_used > 0)
+		{
+			ExplainPropertyInteger("Disk Usage", "kB",
+								   aggstate->hash_disk_used, es);
+			ExplainPropertyInteger("HashAgg Batches", NULL,
+								   aggstate->hash_batches_used, es);
+		}
 	}
 }
 
@@ -3798,6 +3882,29 @@ ExplainSubPlans(List *plans, List *ancestors,
 
 		ExplainNode(sps->planstate, ancestors,
 					relationship, sp->plan_name, es);
+		if (sps->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashtable->instrument, NULL, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (sps->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+			}
+			show_tuplehash_info(&sps->hashnulls->instrument, NULL, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 
 		ancestors = list_delete_first(ancestors);
 	}
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..10276d3f58 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,6 +188,7 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
+	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -203,6 +204,7 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
+	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -281,9 +283,40 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
+	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
+/* Update instrumentation stats */
+void
+UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
+{
+	hashtable->instrument.nbuckets = hashtable->hashtab->size;
+	if (initial)
+	{
+		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
+		// hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
+			// sizeof(TupleHashEntryData);
+		hashtable->instrument.space_peak_hash =
+			MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
+		hashtable->instrument.space_peak_tuples = 0;
+	}
+	else
+	{
+		/* hashtable->entrysize includes additionalsize */
+		size_t hash_size = MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
+		size_t tuple_size = MemoryContextMemAllocated(hashtable->tablecxt, true);
+
+		hashtable->instrument.space_peak_hash = Max(
+			hashtable->instrument.space_peak_hash,
+			hash_size);
+
+		hashtable->instrument.space_peak_tuples = Max(
+			hashtable->instrument.space_peak_tuples, tuple_size);
+				// hashtable->hashtab->members * hashtable->entrysize);
+	}
+}
+
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 48b0274b2e..2d6783843a 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1867,36 +1867,25 @@ hash_agg_enter_spill_mode(AggState *aggstate)
 static void
 hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 {
-	Size	meta_mem = 0;
 	Size	hash_mem = 0;
 	Size	buffer_mem;
-	Size	total_mem;
 
 	if (aggstate->aggstrategy != AGG_MIXED &&
 		aggstate->aggstrategy != AGG_HASHED)
 		return;
 
-
 	for (int i = 0; i < aggstate->num_hashes; ++i)
 	{
-		/* memory for the hash table itself */
-		meta_mem += MemoryContextMemAllocated(
-			aggstate->perhash[i].hash_metacxt, true);
-		/* memory for the group keys and transition states */
 		hash_mem += MemoryContextMemAllocated(
 			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
+		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
 	}
 
-	/* memory for read/write tape buffers, if spilled */
+	/* memory for read/write tape buffers, if spilled XXX */
 	buffer_mem = npartitions * HASHAGG_WRITE_BUFFER_SIZE;
 	if (from_tape)
 		buffer_mem += HASHAGG_READ_BUFFER_SIZE;
 
-	/* update peak mem */
-	total_mem = meta_mem + hash_mem + buffer_mem;
-	if (total_mem > aggstate->hash_mem_peak)
-		aggstate->hash_mem_peak = total_mem;
-
 	/* update disk usage */
 	if (aggstate->hash_tapeinfo != NULL)
 	{
@@ -3269,7 +3258,7 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
 	/*
 	 * Create expression contexts.  We need three or more, one for
 	 * per-input-tuple processing, one for per-output-tuple processing, one
-	 * for all the hashtables, and one for each grouping set.  The per-tuple
+	 * for each hashtable, and one for each grouping set.  The per-tuple
 	 * memory context of the per-grouping-set ExprContexts (aggcontexts)
 	 * replaces the standalone memory context formerly used to hold transition
 	 * values.  We cheat a little by using ExecAssignExprContext() to build
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 620414a1ed..93272c28b1 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -156,6 +156,9 @@ ExecRecursiveUnion(PlanState *pstate)
 		return slot;
 	}
 
+	if (node->hashtable)
+		UpdateTupleHashTableStats(node->hashtable, false);
+
 	return NULL;
 }
 
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index bfd148a41a..9c0e0ab96e 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -415,6 +415,7 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
+	UpdateTupleHashTableStats(setopstate->hashtable, false);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 298b7757f5..22c32612ba 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -621,6 +621,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
+	UpdateTupleHashTableStats(node->hashtable, false);
+	if (node->hashnulls)
+		UpdateTupleHashTableStats(node->hashnulls, false);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index c7deeac662..f71cc03ad5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,6 +150,7 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
+extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 3fc5989bf7..cdcd825c1e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -693,6 +693,14 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+typedef struct HashTableInstrumentation
+{
+	size_t	nbuckets;				/* number of buckets at end of execution */
+	size_t	nbuckets_original;		/* planned number of buckets */
+	size_t	space_peak_hash;		/* peak memory usage in bytes */
+	size_t	space_peak_tuples;		/* peak memory usage in bytes */
+} HashTableInstrumentation;
+
 typedef struct TupleHashTableData
 {
 	tuplehash_hash *hashtab;	/* underlying hash table */
@@ -711,6 +719,7 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
+	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -2173,9 +2182,9 @@ typedef struct AggState
 	int			hash_planned_partitions; /* number of partitions planned
 											for first pass */
 	double		hashentrysize;	/* estimate revised during execution */
-	Size		hash_mem_peak;	/* peak hash table memory usage */
 	uint64		hash_ngroups_current;	/* number of groups currently in
 										   memory in all hash tables */
+// Move these to instrumentation ?
 	uint64		hash_disk_used; /* kB of disk space used */
 	int			hash_batches_used;	/* batches used during entire execution */
 
-- 
2.17.0

v9-0003-refactor-show_grouping_set_keys.patchtext/x-diff; charset=us-asciiDownload
From 9268e602826890d701b8210e388555ef9a2b92b2 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 23 Feb 2020 23:13:07 -0600
Subject: [PATCH v9 3/8] refactor show_grouping_set_keys

---
 src/backend/commands/explain.c | 55 ++++++++++++++++++++--------------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ecc0469d35..0e749b6b5a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -97,6 +97,8 @@ static void show_grouping_set_info(AggState *aggstate,
 								   List *ancestors,
 								   HashTableInstrumentation *inst,
 								   ExplainState *es);
+static void show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List
+		*context, bool useprefix, ExplainState *es);
 static void show_group_keys(GroupState *gstate, List *ancestors,
 							ExplainState *es);
 static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
@@ -2422,6 +2424,37 @@ show_grouping_set_info(AggState *aggstate,
 					   List *context, bool useprefix,
 					   List *ancestors, HashTableInstrumentation *inst,
 					   ExplainState *es)
+{
+	PlanState	*planstate = outerPlanState(aggstate);
+
+	ExplainOpenGroup("Grouping Set", NULL, true, es);
+
+	if (sortnode)
+	{
+		show_sort_group_keys(planstate, "Sort Key",
+							 sortnode->numCols, 0, sortnode->sortColIdx,
+							 sortnode->sortOperators, sortnode->collations,
+							 sortnode->nullsFirst,
+							 ancestors, es);
+		if (es->format == EXPLAIN_FORMAT_TEXT)
+			es->indent++;
+	}
+
+	show_grouping_set_keys(aggstate, aggnode, context, useprefix, es);
+
+	if (aggnode->aggstrategy == AGG_HASHED ||
+			aggnode->aggstrategy == AGG_MIXED)
+		show_tuplehash_info(inst, NULL, es);
+
+	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
+		es->indent--;
+
+	ExplainCloseGroup("Grouping Set", NULL, true, es);
+}
+
+/* Show keys of a grouping set */
+static void
+show_grouping_set_keys(AggState *aggstate, Agg *aggnode, List *context, bool useprefix, ExplainState *es)
 {
 	PlanState	*planstate = outerPlanState(aggstate);
 	Plan	   *plan = planstate->plan;
@@ -2443,19 +2476,6 @@ show_grouping_set_info(AggState *aggstate,
 		keysetname = "Group Keys";
 	}
 
-	ExplainOpenGroup("Grouping Set", NULL, true, es);
-
-	if (sortnode)
-	{
-		show_sort_group_keys(planstate, "Sort Key",
-							 sortnode->numCols, 0, sortnode->sortColIdx,
-							 sortnode->sortOperators, sortnode->collations,
-							 sortnode->nullsFirst,
-							 ancestors, es);
-		if (es->format == EXPLAIN_FORMAT_TEXT)
-			es->indent++;
-	}
-
 	ExplainOpenGroup(keysetname, keysetname, false, es);
 
 	foreach(lc, gsets)
@@ -2486,15 +2506,6 @@ show_grouping_set_info(AggState *aggstate,
 	}
 
 	ExplainCloseGroup(keysetname, keysetname, false, es);
-
-	if (aggnode->aggstrategy == AGG_HASHED ||
-			aggnode->aggstrategy == AGG_MIXED)
-		show_tuplehash_info(inst, NULL, es);
-
-	if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
-		es->indent--;
-
-	ExplainCloseGroup("Grouping Set", NULL, true, es);
 }
 
 /*
-- 
2.17.0

v9-0004-Gross-hack-to-put-hash-stats-of-subplans-in-the-r.patchtext/x-diff; charset=us-asciiDownload
From 78d7c36ff6aec2715de5638835cfbde53c74cbb5 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 14:13:06 -0600
Subject: [PATCH v9 4/8] Gross hack to put hash stats of subplans in the
 right(?) place

---
 src/backend/commands/explain.c | 70 ++++++++++++++++++----------------
 1 file changed, 38 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 0e749b6b5a..faad6d676a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -66,7 +66,7 @@ static double elapsed_time(instr_time *starttime);
 static bool ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used);
 static void ExplainNode(PlanState *planstate, List *ancestors,
 						const char *relationship, const char *plan_name,
-						ExplainState *es);
+						SubPlanState *subplanstate, ExplainState *es);
 static void show_plan_tlist(PlanState *planstate, List *ancestors,
 							ExplainState *es);
 static void show_expression(Node *node, const char *qlabel,
@@ -764,7 +764,7 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ps = outerPlanState(ps);
 		es->hide_workers = true;
 	}
-	ExplainNode(ps, NIL, NULL, NULL, es);
+	ExplainNode(ps, NIL, NULL, NULL, NULL, es);
 
 	/*
 	 * If requested, include information about GUC parameters with values that
@@ -1126,7 +1126,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 static void
 ExplainNode(PlanState *planstate, List *ancestors,
 			const char *relationship, const char *plan_name,
-			ExplainState *es)
+			SubPlanState *subplanstate, ExplainState *es)
 {
 	Plan	   *plan = planstate->plan;
 	const char *pname;			/* node type name for text output */
@@ -1386,6 +1386,21 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			ExplainIndentText(es);
 			appendStringInfo(es->str, "%s\n", plan_name);
 			es->indent++;
+
+			Assert(subplanstate != NULL);
+			/* Show hash stats for hashed subplan */
+			if (subplanstate->hashtable)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+			}
+			if (subplanstate->hashnulls)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, "Null Hashtable: ");
+				show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+			}
 		}
 		if (es->indent)
 		{
@@ -1414,6 +1429,20 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (custom_name)
 			ExplainPropertyText("Custom Plan Provider", custom_name, es);
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+
+		if (subplanstate && subplanstate->hashtable)
+		{
+			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
+		}
+
+		if (subplanstate && subplanstate->hashnulls)
+		{
+			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
+			show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
+		}
 	}
 
 	switch (nodeTag(plan))
@@ -2097,12 +2126,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* lefttree */
 	if (outerPlanState(planstate))
 		ExplainNode(outerPlanState(planstate), ancestors,
-					"Outer", NULL, es);
+					"Outer", NULL, NULL, es);
 
 	/* righttree */
 	if (innerPlanState(planstate))
 		ExplainNode(innerPlanState(planstate), ancestors,
-					"Inner", NULL, es);
+					"Inner", NULL, NULL, es);
 
 	/* special child plans */
 	switch (nodeTag(plan))
@@ -2134,7 +2163,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			break;
 		case T_SubqueryScan:
 			ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
-						"Subquery", NULL, es);
+						"Subquery", NULL, NULL, es);
 			break;
 		case T_CustomScan:
 			ExplainCustomChildren((CustomScanState *) planstate,
@@ -3834,7 +3863,7 @@ ExplainMemberNodes(PlanState **planstates, int nplans,
 
 	for (j = 0; j < nplans; j++)
 		ExplainNode(planstates[j], ancestors,
-					"Member", NULL, es);
+					"Member", NULL, NULL, es);
 }
 
 /*
@@ -3892,30 +3921,7 @@ ExplainSubPlans(List *plans, List *ancestors,
 		ancestors = lcons(sp, ancestors);
 
 		ExplainNode(sps->planstate, ancestors,
-					relationship, sp->plan_name, es);
-		if (sps->hashtable)
-		{
-			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashtable->instrument, NULL, es);
-			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
-		}
-
-		if (sps->hashnulls)
-		{
-			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			if (es->format == EXPLAIN_FORMAT_TEXT)
-			{
-				ExplainIndentText(es);
-				appendStringInfoString(es->str, "Null Hashtable: ");
-			}
-			show_tuplehash_info(&sps->hashnulls->instrument, NULL, es);
-			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
-		}
+					relationship, sp->plan_name, sps, es);
 
 		ancestors = list_delete_first(ancestors);
 	}
@@ -3932,7 +3938,7 @@ ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
 	(list_length(css->custom_ps) != 1 ? "children" : "child");
 
 	foreach(cell, css->custom_ps)
-		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+		ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, NULL, es);
 }
 
 /*
-- 
2.17.0

v9-0005-implement-hash-stats-for-bitmapHeapScan.patchtext/x-diff; charset=us-asciiDownload
From 21bae7eb3d8621596e317d84455d64c78a514e72 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Wed, 12 Feb 2020 23:40:45 -0600
Subject: [PATCH v9 5/8] implement hash stats for bitmapHeapScan..

TIDBitmap is a private structure, so add an accessor function to return its
instrumentation, and duplicate instrumentation struct in BitmapHeapState.

The instrumentation itself could be implemented in simplehash.h.  But I think
the higher layer BitmapHeapScan would have to include an instrumentation struct
anyway, since explain.c cannot look into tbm->pagetable to get .instrument (and
the pagetable structure itself doesn't match tuplehash).

Also, if instrumentation were implemented in simplehash.h, I think every
insertion or deletion would need to check ->members and ->size (which isn't
necessary for Agg, but is necessary in the general case, and specifically for
tidbitmap, since it actually DELETEs hashtable entries).  Or else simplehash
would need a new function like UpdateTupleHashStats, which the higher level nodes
would need to call after filling the hashtable or before deleting tuples, which
seems to defeat the purpose of implementing stats at a lower layer.

Note, this doesn't affect any regression tests, since hashtable isn't allocated
during "explain".  Note that "explain analyze" would show memory stats, which
we'd have to filter.
---
 src/backend/commands/explain.c            |  5 ++--
 src/backend/executor/nodeBitmapHeapscan.c |  3 +++
 src/backend/nodes/tidbitmap.c             | 29 +++++++++++++++++++++++
 src/include/nodes/execnodes.h             |  1 +
 src/include/nodes/tidbitmap.h             |  4 ++++
 5 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index faad6d676a..579a14abf2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1780,8 +1780,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
 										   planstate, es);
-			if (es->analyze)
-				show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
+			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
 			break;
 		case T_SampleScan:
 			show_tablesample(((SampleScan *) plan)->tablesample,
@@ -3243,6 +3242,8 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 			appendStringInfoChar(es->str, '\n');
 		}
 	}
+
+	show_tuplehash_info(&planstate->instrument, es, NULL);
 }
 
 /*
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index 726d3a2d9a..1785c78091 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -182,6 +182,8 @@ BitmapHeapNext(BitmapHeapScanState *node)
 #endif							/* USE_PREFETCH */
 		}
 		node->initialized = true;
+		if (node->tbm)
+			node->instrument = *tbm_instrumentation(node->tbm);
 	}
 
 	for (;;)
@@ -741,6 +743,7 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
 	scanstate->shared_tbmiterator = NULL;
 	scanstate->shared_prefetch_iterator = NULL;
 	scanstate->pstate = NULL;
+	memset(&scanstate->instrument, 0, sizeof(scanstate->instrument));
 
 	/*
 	 * We can potentially skip fetching heap pages if we do not need any
diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index ad4e071ca3..ab81cce3b2 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -44,6 +44,7 @@
 #include "common/hashfn.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "nodes/execnodes.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 
@@ -166,6 +167,7 @@ struct TIDBitmap
 	dsa_pointer ptpages;		/* dsa_pointer to the page array */
 	dsa_pointer ptchunks;		/* dsa_pointer to the chunk array */
 	dsa_area   *dsa;			/* reference to per-query dsa area */
+	HashTableInstrumentation instrument;	/* Returned by accessor function */
 };
 
 /*
@@ -294,6 +296,7 @@ tbm_create_pagetable(TIDBitmap *tbm)
 	Assert(tbm->pagetable == NULL);
 
 	tbm->pagetable = pagetable_create(tbm->mcxt, 128, tbm);
+	tbm->instrument.nbuckets_original = tbm->pagetable->size;
 
 	/* If entry1 is valid, push it into the hashtable */
 	if (tbm->status == TBM_ONE_PAGE)
@@ -1147,6 +1150,32 @@ tbm_end_iterate(TBMIterator *iterator)
 	pfree(iterator);
 }
 
+/*
+ * tbm_instrumentation - update stored stats and return pointer to
+ * instrumentation structure
+ *
+ * This updates stats when called.
+ * Returned data is within the iterator's tbm, and destroyed with it.
+ */
+HashTableInstrumentation *
+tbm_instrumentation(TIDBitmap *tbm)
+{
+	if (tbm->pagetable)
+	{
+		tbm->instrument.nbuckets = tbm->pagetable->size;
+		tbm->instrument.space_peak_hash = sizeof(PagetableEntry) * tbm->pagetable->size;
+
+		/*
+		 * If there are lossy pages, then at one point, we filled maxentries;
+		 * otherwise, number of pages is "->members".
+		 */
+		tbm->instrument.space_peak_tuples = sizeof(BlockNumber) *
+			(tbm->nchunks>0 ? tbm->maxentries : tbm->pagetable->members);
+	}
+
+	return &tbm->instrument;
+}
+
 /*
  * tbm_end_shared_iterate - finish a shared iteration over a TIDBitmap
  *
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cdcd825c1e..19b657263b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1611,6 +1611,7 @@ typedef struct BitmapHeapScanState
 	TBMSharedIterator *shared_tbmiterator;
 	TBMSharedIterator *shared_prefetch_iterator;
 	ParallelBitmapHeapState *pstate;
+	HashTableInstrumentation instrument;
 } BitmapHeapScanState;
 
 /* ----------------
diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h
index d562fcae34..de0cdfb91f 100644
--- a/src/include/nodes/tidbitmap.h
+++ b/src/include/nodes/tidbitmap.h
@@ -26,6 +26,9 @@
 #include "utils/dsa.h"
 
 
+/* Forward decl */
+typedef struct HashTableInstrumentation HashTableInstrumentation;
+
 /*
  * Actual bitmap representation is private to tidbitmap.c.  Callers can
  * do IsA(x, TIDBitmap) on it, but nothing else.
@@ -71,5 +74,6 @@ extern void tbm_end_shared_iterate(TBMSharedIterator *iterator);
 extern TBMSharedIterator *tbm_attach_shared_iterate(dsa_area *dsa,
 													dsa_pointer dp);
 extern long tbm_calculate_entries(double maxbytes);
+extern HashTableInstrumentation *tbm_instrumentation(TIDBitmap *tbm);
 
 #endif							/* TIDBITMAP_H */
-- 
2.17.0

v9-0006-Refactor-for-consistency-symmetry.patchtext/x-diff; charset=us-asciiDownload
From b4f7147ac797197e0fd0137b07ace4fdb3d76860 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sun, 9 Feb 2020 15:08:14 -0600
Subject: [PATCH v9 6/8] Refactor for consistency/symmetry

This moves hash instrumentation out of execGrouping.c / TupleHashTable and into
higher level nodes, for consistency with bitmapHeapScan.

This might be unimportant and maybe clearer left in execGrouping.c.
---
 .../postgres_fdw/expected/postgres_fdw.out    | 56 +++++++++++++------
 src/backend/commands/explain.c                | 20 +++----
 src/backend/executor/execGrouping.c           | 33 -----------
 src/backend/executor/nodeAgg.c                |  7 ++-
 src/backend/executor/nodeRecursiveunion.c     |  4 +-
 src/backend/executor/nodeSetOp.c              |  6 +-
 src/backend/executor/nodeSubplan.c            | 12 +++-
 src/include/executor/executor.h               |  1 -
 src/include/executor/nodeAgg.h                |  1 +
 src/include/nodes/execnodes.h                 | 24 +++++++-
 10 files changed, 98 insertions(+), 66 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62c2697920..2ddae83178 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2086,9 +2086,11 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
          ->  HashAggregate
                Output: t1.c1, avg((t1.c1 + t2.c1))
                Group Key: t1.c1
+               Buckets: 256
                ->  HashAggregate
                      Output: t1.c1, t2.c1
                      Group Key: t1.c1, t2.c1
+                     Buckets: 4096
                      ->  Append
                            ->  Foreign Scan
                                  Output: t1.c1, t2.c1
@@ -2098,7 +2100,7 @@ SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                                  Output: t1_1.c1, t2_1.c1
                                  Relations: (public.ft1 t1_1) INNER JOIN (public.ft2 t2_1)
                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
-(20 rows)
+(22 rows)
 
 SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
  t1c1 |         avg          
@@ -2129,11 +2131,12 @@ SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
          ->  HashAggregate
                Output: t2.c1, t3.c1
                Group Key: t2.c1, t3.c1
+               Buckets: 2
                ->  Foreign Scan
                      Output: t2.c1, t3.c1
                      Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
-(13 rows)
+(14 rows)
 
 SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  C 1 
@@ -2610,10 +2613,11 @@ select c2 * (random() <= 1)::int as c2 from ft2 group by c2 * (random() <= 1)::i
    ->  HashAggregate
          Output: ((c2 * ((random() <= '1'::double precision))::integer))
          Group Key: (ft2.c2 * ((random() <= '1'::double precision))::integer)
+         Buckets: 2
          ->  Foreign Scan on public.ft2
                Output: (c2 * ((random() <= '1'::double precision))::integer)
                Remote SQL: SELECT c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 -- GROUP BY clause in various forms, cardinal, alias and constant expression
 explain (verbose, costs off)
@@ -2713,11 +2717,12 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
    ->  HashAggregate
          Output: sum(c1), c2
          Group Key: ft1.c2
+         Buckets: 16
          Filter: (avg((ft1.c1 * ((random() <= '1'::double precision))::integer)) > '100'::numeric)
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(10 rows)
+(11 rows)
 
 -- Remote aggregate in combination with a local Param (for the output
 -- of an initplan) can be trouble, per bug #15781
@@ -2963,10 +2968,11 @@ select sum(c1) filter (where (c1 / c1) * random() <= 1) from ft1 group by c2 ord
    ->  HashAggregate
          Output: sum(c1) FILTER (WHERE ((((c1 / c1))::double precision * random()) <= '1'::double precision)), c2
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c1, c2
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
-(9 rows)
+(10 rows)
 
 explain (verbose, costs off)
 select sum(c2) filter (where c2 in (select c2 from ft1 where c2 < 5)) from ft1;
@@ -3229,6 +3235,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
    ->  HashAggregate
          Output: count(*), x.b
          Group Key: x.b
+         Buckets: 16
          ->  Hash Join
                Output: x.b
                Inner Unique: true
@@ -3244,7 +3251,7 @@ select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x w
                                  Output: ft1_1.c2, (sum(ft1_1.c1))
                                  Relations: Aggregate on (public.ft1 ft1_1)
                                  Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1
-(21 rows)
+(22 rows)
 
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
  count |   b   
@@ -3449,11 +3456,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls la
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by rollup(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3474,11 +3482,12 @@ select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last
    ->  MixedAggregate
          Output: c2, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Group Key: ()
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(11 rows)
 
 select c2, sum(c1) from ft1 where c2 < 3 group by cube(c2) order by 1 nulls last;
  c2 |  sum   
@@ -3499,11 +3508,13 @@ select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) orde
    ->  HashAggregate
          Output: c2, c6, sum(c1)
          Hash Key: ft1.c2
+         Buckets: 16
          Hash Key: ft1.c6
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c6, c1
                Remote SQL: SELECT "C 1", c2, c6 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(10 rows)
+(12 rows)
 
 select c2, c6, sum(c1) from ft1 where c2 < 3 group by grouping sets(c2, c6) order by 1 nulls last, 2 nulls last;
  c2 | c6 |  sum  
@@ -3526,10 +3537,11 @@ select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nu
    ->  HashAggregate
          Output: c2, sum(c1), GROUPING(c2)
          Group Key: ft1.c2
+         Buckets: 16
          ->  Foreign Scan on public.ft1
                Output: c2, c1
                Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 < 3))
-(9 rows)
+(10 rows)
 
 select c2, sum(c1), grouping(c2) from ft1 where c2 < 3 group by c2 order by 1 nulls last;
  c2 |  sum  | grouping 
@@ -7147,13 +7159,14 @@ select * from bar where f1 in (select f1 from foo) for update;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for update;
  f1 | f2 
@@ -7185,13 +7198,14 @@ select * from bar where f1 in (select f1 from foo) for share;
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(23 rows)
+(24 rows)
 
 select * from bar where f1 in (select f1 from foo) for share;
  f1 | f2 
@@ -7222,6 +7236,7 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
@@ -7240,13 +7255,14 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                ->  HashAggregate
                      Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                      Group Key: foo.f1
+                     Buckets: 256
                      ->  Append
                            ->  Seq Scan on public.foo foo_1
                                  Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(39 rows)
+(41 rows)
 
 update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
 select tableoid::regclass, * from bar order by 1,2;
@@ -8751,12 +8767,13 @@ SELECT a, sum(b), min(b), count(*) FROM pagg_tab GROUP BY a HAVING avg(b) < 22 O
    Sort Key: pagg_tab.a
    ->  HashAggregate
          Group Key: pagg_tab.a
+         Buckets: 64
          Filter: (avg(pagg_tab.b) < '22'::numeric)
          ->  Append
                ->  Foreign Scan on fpagg_tab_p1 pagg_tab_1
                ->  Foreign Scan on fpagg_tab_p2 pagg_tab_2
                ->  Foreign Scan on fpagg_tab_p3 pagg_tab_3
-(9 rows)
+(10 rows)
 
 -- Plan with partitionwise aggregates is enabled
 SET enable_partitionwise_aggregate TO true;
@@ -8799,6 +8816,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1.a, count(((t1.*)::pagg_tab))
                Group Key: t1.a
+               Buckets: 16
                Filter: (avg(t1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p1 t1
                      Output: t1.a, t1.*, t1.b
@@ -8806,6 +8824,7 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_1.a, count(((t1_1.*)::pagg_tab))
                Group Key: t1_1.a
+               Buckets: 16
                Filter: (avg(t1_1.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p2 t1_1
                      Output: t1_1.a, t1_1.*, t1_1.b
@@ -8813,11 +8832,12 @@ SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
          ->  HashAggregate
                Output: t1_2.a, count(((t1_2.*)::pagg_tab))
                Group Key: t1_2.a
+               Buckets: 16
                Filter: (avg(t1_2.b) < '22'::numeric)
                ->  Foreign Scan on public.fpagg_tab_p3 t1_2
                      Output: t1_2.a, t1_2.*, t1_2.b
                      Remote SQL: SELECT a, b, c FROM public.pagg_tab_p3
-(25 rows)
+(28 rows)
 
 SELECT a, count(t1) FROM pagg_tab t1 GROUP BY a HAVING avg(b) < 22 ORDER BY 1;
  a  | count 
@@ -8839,18 +8859,22 @@ SELECT b, avg(a), max(a), count(*) FROM pagg_tab GROUP BY b HAVING sum(a) < 700
    Sort Key: pagg_tab.b
    ->  Finalize HashAggregate
          Group Key: pagg_tab.b
+         Buckets: 64
          Filter: (sum(pagg_tab.a) < 700)
          ->  Append
                ->  Partial HashAggregate
                      Group Key: pagg_tab.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p1 pagg_tab
                ->  Partial HashAggregate
                      Group Key: pagg_tab_1.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p2 pagg_tab_1
                ->  Partial HashAggregate
                      Group Key: pagg_tab_2.b
+                     Buckets: 64
                      ->  Foreign Scan on fpagg_tab_p3 pagg_tab_2
-(15 rows)
+(19 rows)
 
 -- ===================================================================
 -- access rights and superuser
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 579a14abf2..b80f7b3c16 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1393,13 +1393,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+				show_tuplehash_info(&subplanstate->instrument, NULL, es);
 			}
 			if (subplanstate->hashnulls)
 			{
 				ExplainIndentText(es);
 				appendStringInfoString(es->str, "Null Hashtable: ");
-				show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+				show_tuplehash_info(&subplanstate->instrument_nulls, NULL, es);
 			}
 		}
 		if (es->indent)
@@ -1433,14 +1433,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		if (subplanstate && subplanstate->hashtable)
 		{
 			ExplainOpenGroup("Hashtable", "Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashtable->instrument, NULL, es);
+			show_tuplehash_info(&subplanstate->instrument, NULL, es);
 			ExplainCloseGroup("Hashtable", "Hashtable", true, es);
 		}
 
 		if (subplanstate && subplanstate->hashnulls)
 		{
 			ExplainOpenGroup("Null Hashtable", "Null Hashtable", true, es);
-			show_tuplehash_info(&subplanstate->hashnulls->instrument, NULL, es);
+			show_tuplehash_info(&subplanstate->instrument_nulls, NULL, es);
 			ExplainCloseGroup("Null Hashtable", "Null Hashtable", true, es);
 		}
 	}
@@ -1971,14 +1971,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			{
 				SetOpState *sos = castNode(SetOpState, planstate);
 				if (sos->hashtable)
-					show_tuplehash_info(&sos->hashtable->instrument, NULL, es);
+					show_tuplehash_info(&sos->instrument, NULL, es);
 			}
 			break;
 		case T_RecursiveUnion:
 			{
 				RecursiveUnionState *rus = (RecursiveUnionState *)planstate;
 				if (rus->hashtable)
-					show_tuplehash_info(&rus->hashtable->instrument, NULL, es);
+					show_tuplehash_info(&rus->instrument, NULL, es);
 			}
 			break;
 		case T_Group:
@@ -2394,7 +2394,7 @@ show_agg_keys(AggState *astate, List *ancestors,
 								 ancestors, es);
 			Assert(astate->num_hashes <= 1);
 			if (astate->num_hashes)
-				show_tuplehash_info(&astate->perhash[0].hashtable->instrument, astate, es);
+				show_tuplehash_info(&astate->perhash[0].instrument, astate, es);
 		}
 
 		ancestors = list_delete_first(ancestors);
@@ -2421,7 +2421,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 
 	show_grouping_set_info(aggstate, agg, NULL, context, useprefix, ancestors,
 			aggstate->num_hashes ?
-			&aggstate->perhash[setno++].hashtable->instrument : NULL,
+			&aggstate->perhash[setno++].instrument : NULL,
 			es);
 
 	foreach(lc, agg->chain)
@@ -2434,7 +2434,7 @@ show_grouping_sets(AggState *aggstate, Agg *agg,
 				aggnode->aggstrategy == AGG_MIXED)
 		{
 			Assert(setno < aggstate->num_hashes);
-			inst = &aggstate->perhash[setno++].hashtable->instrument;
+			inst = &aggstate->perhash[setno++].instrument;
 		}
 
 		show_grouping_set_info(aggstate, aggnode, sortnode,
@@ -3243,7 +3243,7 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
 		}
 	}
 
-	show_tuplehash_info(&planstate->instrument, es, NULL);
+	show_tuplehash_info(&planstate->instrument, NULL, es);
 }
 
 /*
diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 10276d3f58..009d27b9a8 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -188,7 +188,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
 	hashtable->cur_eq_func = NULL;
-	memset(&hashtable->instrument, 0, sizeof(hashtable->instrument));
 
 	/*
 	 * If parallelism is in use, even if the master backend is performing the
@@ -204,7 +203,6 @@ BuildTupleHashTableExt(PlanState *parent,
 		hashtable->hash_iv = 0;
 
 	hashtable->hashtab = tuplehash_create(metacxt, nbuckets, hashtable);
-	UpdateTupleHashTableStats(hashtable, true);
 
 	/*
 	 * We copy the input tuple descriptor just for safety --- we assume all
@@ -283,40 +281,9 @@ BuildTupleHashTable(PlanState *parent,
 void
 ResetTupleHashTable(TupleHashTable hashtable)
 {
-	UpdateTupleHashTableStats(hashtable, false);
 	tuplehash_reset(hashtable->hashtab);
 }
 
-/* Update instrumentation stats */
-void
-UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial)
-{
-	hashtable->instrument.nbuckets = hashtable->hashtab->size;
-	if (initial)
-	{
-		hashtable->instrument.nbuckets_original = hashtable->hashtab->size;
-		// hashtable->instrument.space_peak_hash = hashtable->hashtab->size *
-			// sizeof(TupleHashEntryData);
-		hashtable->instrument.space_peak_hash =
-			MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
-		hashtable->instrument.space_peak_tuples = 0;
-	}
-	else
-	{
-		/* hashtable->entrysize includes additionalsize */
-		size_t hash_size = MemoryContextMemAllocated(hashtable->hashtab->ctx, true);
-		size_t tuple_size = MemoryContextMemAllocated(hashtable->tablecxt, true);
-
-		hashtable->instrument.space_peak_hash = Max(
-			hashtable->instrument.space_peak_hash,
-			hash_size);
-
-		hashtable->instrument.space_peak_tuples = Max(
-			hashtable->instrument.space_peak_tuples, tuple_size);
-				// hashtable->hashtab->members * hashtable->entrysize);
-	}
-}
-
 /*
  * Find or create a hashtable entry for the tuple group containing the
  * given tuple.  The tuple must be the same type as the hashtable entries.
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 2d6783843a..c6d03521e4 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1500,6 +1500,10 @@ build_hash_table(AggState *aggstate, int setno, long nbuckets)
 		hashcxt,
 		tmpcxt,
 		DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit));
+
+	InitTupleHashTableStats(perhash->instrument,
+			perhash->hashtable->hashtab,
+			hashcxt, additionalsize);
 }
 
 /*
@@ -1878,7 +1882,8 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
 	{
 		hash_mem += MemoryContextMemAllocated(
 			aggstate->perhash[i].hashcontext->ecxt_per_tuple_memory, true);
-		UpdateTupleHashTableStats(aggstate->perhash[i].hashtable, false);
+		UpdateTupleHashTableStats(aggstate->perhash[i].instrument,
+				aggstate->perhash[i].hashtable->hashtab);
 	}
 
 	/* memory for read/write tape buffers, if spilled XXX */
diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c
index 93272c28b1..5e70e008e5 100644
--- a/src/backend/executor/nodeRecursiveunion.c
+++ b/src/backend/executor/nodeRecursiveunion.c
@@ -50,6 +50,8 @@ build_hash_table(RecursiveUnionState *rustate)
 												rustate->tableContext,
 												rustate->tempContext,
 												false);
+
+	InitTupleHashTableStats(rustate->instrument, rustate->hashtable->hashtab, rustate->tableContext, 0);
 }
 
 
@@ -157,7 +159,7 @@ ExecRecursiveUnion(PlanState *pstate)
 	}
 
 	if (node->hashtable)
-		UpdateTupleHashTableStats(node->hashtable, false);
+		UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 
 	return NULL;
 }
diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c
index 9c0e0ab96e..5eca128183 100644
--- a/src/backend/executor/nodeSetOp.c
+++ b/src/backend/executor/nodeSetOp.c
@@ -139,6 +139,9 @@ build_hash_table(SetOpState *setopstate)
 												   setopstate->tableContext,
 												   econtext->ecxt_per_tuple_memory,
 												   false);
+
+	InitTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab, setopstate->tableContext, 0);
 }
 
 /*
@@ -415,7 +418,8 @@ setop_fill_hash_table(SetOpState *setopstate)
 
 	setopstate->table_filled = true;
 	/* Initialize to walk the hash table */
-	UpdateTupleHashTableStats(setopstate->hashtable, false);
+	UpdateTupleHashTableStats(setopstate->instrument,
+			setopstate->hashtable->hashtab);
 	ResetTupleHashIterator(setopstate->hashtable, &setopstate->hashiter);
 }
 
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 22c32612ba..0de6be40e4 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -505,6 +505,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	if (node->hashtable)
 		ResetTupleHashTable(node->hashtable);
 	else
+	{
 		node->hashtable = BuildTupleHashTableExt(node->parent,
 												 node->descRight,
 												 ncols,
@@ -518,6 +519,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 												 node->hashtablecxt,
 												 node->hashtempcxt,
 												 false);
+		InitTupleHashTableStats(node->instrument, node->hashtable->hashtab,
+				node->hashtablecxt, 0);
+	}
 
 	if (!subplan->unknownEqFalse)
 	{
@@ -533,6 +537,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 		if (node->hashnulls)
 			ResetTupleHashTable(node->hashnulls);
 		else
+		{
 			node->hashnulls = BuildTupleHashTableExt(node->parent,
 													 node->descRight,
 													 ncols,
@@ -546,6 +551,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 													 node->hashtablecxt,
 													 node->hashtempcxt,
 													 false);
+			InitTupleHashTableStats(node->instrument_nulls,
+					node->hashnulls->hashtab, node->hashtablecxt, 0);
+		}
 	}
 	else
 		node->hashnulls = NULL;
@@ -621,9 +629,9 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext)
 	ExecClearTuple(node->projRight->pi_state.resultslot);
 
 	MemoryContextSwitchTo(oldcontext);
-	UpdateTupleHashTableStats(node->hashtable, false);
+	UpdateTupleHashTableStats(node->instrument, node->hashtable->hashtab);
 	if (node->hashnulls)
-		UpdateTupleHashTableStats(node->hashnulls, false);
+		UpdateTupleHashTableStats(node->instrument_nulls, node->hashnulls->hashtab);
 }
 
 /*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index f71cc03ad5..c7deeac662 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -150,7 +150,6 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable,
 										 ExprState *eqcomp,
 										 FmgrInfo *hashfunctions);
 extern void ResetTupleHashTable(TupleHashTable hashtable);
-extern void UpdateTupleHashTableStats(TupleHashTable hashtable, bool initial);
 
 /*
  * prototypes from functions in execJunk.c
diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h
index 72a9568bbe..c49f068f5c 100644
--- a/src/include/executor/nodeAgg.h
+++ b/src/include/executor/nodeAgg.h
@@ -309,6 +309,7 @@ typedef struct AggStatePerHashData
 	Agg		   *aggnode;		/* original Agg node, for numGroups etc. */
 	MemoryContext	hash_metacxt;	/* memory for hash table itself */
 	ExprContext	*hashcontext;	/* context for hash table data */
+	HashTableInstrumentation    instrument;
 }			AggStatePerHashData;
 
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 19b657263b..10239aea4f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -693,12 +693,31 @@ typedef struct TupleHashEntryData
 #define SH_DECLARE
 #include "lib/simplehash.h"
 
+#define InitTupleHashTableStats(instr, htable, tupctx, addsize) \
+	do{\
+	instr.entrysize = sizeof(MinimalTuple) + addsize; \
+	instr.tuplectx = tupctx; \
+	instr.nbuckets = htable->size; \
+	instr.nbuckets_original = htable->size; \
+	instr.space_peak_hash = MemoryContextMemAllocated(htable->ctx, false); \
+	instr.space_peak_tuples = 0; \
+	}while(0)
+
+#define UpdateTupleHashTableStats(instr, htable) \
+	do{\
+	instr.nbuckets = htable->size; \
+	instr.space_peak_hash = Max(instr.space_peak_hash, MemoryContextMemAllocated(htable->ctx, false)); \
+	instr.space_peak_tuples = Max(instr.space_peak_tuples, MemoryContextMemAllocated(instr.tuplectx, false)); \
+	}while(0)
+
 typedef struct HashTableInstrumentation
 {
+	size_t	entrysize;				/* Includes additionalsize */
 	size_t	nbuckets;				/* number of buckets at end of execution */
 	size_t	nbuckets_original;		/* planned number of buckets */
 	size_t	space_peak_hash;		/* peak memory usage in bytes */
 	size_t	space_peak_tuples;		/* peak memory usage in bytes */
+	MemoryContext	tuplectx;		/* Context where tuples are stored */
 } HashTableInstrumentation;
 
 typedef struct TupleHashTableData
@@ -719,7 +738,6 @@ typedef struct TupleHashTableData
 	ExprState  *cur_eq_func;	/* comparator for input vs. table */
 	uint32		hash_iv;		/* hash-function IV */
 	ExprContext *exprcontext;	/* expression context */
-	HashTableInstrumentation instrument;
 }			TupleHashTableData;
 
 typedef tuplehash_iterator TupleHashIterator;
@@ -885,6 +903,8 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+	HashTableInstrumentation instrument;
+	HashTableInstrumentation instrument_nulls; /* instrumentation for nulls hashtable */
 } SubPlanState;
 
 /* ----------------
@@ -1293,6 +1313,7 @@ typedef struct RecursiveUnionState
 	MemoryContext tempContext;	/* short-term context for comparisons */
 	TupleHashTable hashtable;	/* hash table for tuples already seen */
 	MemoryContext tableContext; /* memory context containing hash table */
+	HashTableInstrumentation instrument;
 } RecursiveUnionState;
 
 /* ----------------
@@ -2424,6 +2445,7 @@ typedef struct SetOpState
 	MemoryContext tableContext; /* memory context containing hash table */
 	bool		table_filled;	/* hash table filled yet? */
 	TupleHashIterator hashiter; /* for iterating through hash table */
+	HashTableInstrumentation instrument;
 } SetOpState;
 
 /* ----------------
-- 
2.17.0

v9-0007-TupleHashTable.entrysize-was-unused-except-for-in.patchtext/x-diff; charset=us-asciiDownload
From 09c7237b7689d0eae5e2da0d5c46795e64d9bc87 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 17:19:21 -0600
Subject: [PATCH v9 7/8] TupleHashTable.entrysize was unused except for
 instrumentation..

---
 src/backend/executor/execGrouping.c | 1 -
 src/include/nodes/execnodes.h       | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c
index 009d27b9a8..98fd4bf8bd 100644
--- a/src/backend/executor/execGrouping.c
+++ b/src/backend/executor/execGrouping.c
@@ -183,7 +183,6 @@ BuildTupleHashTableExt(PlanState *parent,
 	hashtable->tab_collations = collations;
 	hashtable->tablecxt = tablecxt;
 	hashtable->tempcxt = tempcxt;
-	hashtable->entrysize = entrysize;
 	hashtable->tableslot = NULL;	/* will be made on first lookup */
 	hashtable->inputslot = NULL;
 	hashtable->in_hash_funcs = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 10239aea4f..bca77d52b6 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -730,7 +730,6 @@ typedef struct TupleHashTableData
 	Oid		   *tab_collations; /* collations for hash and comparison */
 	MemoryContext tablecxt;		/* memory context containing table */
 	MemoryContext tempcxt;		/* context for function evaluations */
-	Size		entrysize;		/* actual size to make each hash entry */
 	TupleTableSlot *tableslot;	/* slot for referencing table entries */
 	/* The following fields are set transiently for each table search: */
 	TupleTableSlot *inputslot;	/* current input tuple's slot */
-- 
2.17.0

v9-0008-Update-comment-obsolete-since-69c3936a.patchtext/x-diff; charset=us-asciiDownload
From baca179593ff661c933764a81076e129e1e03804 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Sat, 15 Feb 2020 15:53:34 -0600
Subject: [PATCH v9 8/8] Update comment obsolete since 69c3936a

---
 src/backend/executor/nodeAgg.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index c6d03521e4..5cdd92acaf 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -2058,8 +2058,7 @@ lookup_hash_entry(AggState *aggstate, uint32 hash, bool *in_hash_table)
 }
 
 /*
- * Look up hash entries for the current tuple in all hashed grouping sets,
- * returning an array of pergroup pointers suitable for advance_aggregates.
+ * Look up hash entries for the current tuple in all hashed grouping sets.
  *
  * Be aware that lookup_hash_entry can reset the tmpcontext.
  *
-- 
2.17.0

#22Jeff Davis
pgsql@j-davis.com
In reply to: Justin Pryzby (#21)
Re: explain HashAggregate to report bucket and memory stats

On Wed, 2020-04-08 at 16:00 -0500, Justin Pryzby wrote:

90% of the initial goal of this patch was handled by instrumentation
added by
"hash spill to disk" (1f39bce02), but this *also* adds:

- separate accounting for tuples vs hashtable;
- number of hash buckets;
- handles other agg nodes, and bitmap scan;

Should I continue pursuing this patch?
Does it still serve any significant purpose?

Those things would be useful for me trying to tune the performance and
cost model. I think we need to put some of these things under "VERBOSE"
or maybe invent a new explain option to provide this level of detail,
though.

Regards,
Jeff Davis

#23Daniel Gustafsson
daniel@yesql.se
In reply to: Jeff Davis (#22)
Re: explain HashAggregate to report bucket and memory stats

On 9 Apr 2020, at 00:24, Jeff Davis <pgsql@j-davis.com> wrote:

On Wed, 2020-04-08 at 16:00 -0500, Justin Pryzby wrote:

90% of the initial goal of this patch was handled by instrumentation
added by
"hash spill to disk" (1f39bce02), but this *also* adds:

- separate accounting for tuples vs hashtable;
- number of hash buckets;
- handles other agg nodes, and bitmap scan;

Should I continue pursuing this patch?
Does it still serve any significant purpose?

Those things would be useful for me trying to tune the performance and
cost model. I think we need to put some of these things under "VERBOSE"
or maybe invent a new explain option to provide this level of detail,
though.

This thread has stalled and the patch has been Waiting on Author since March,
and skimming the thread there seems to be questions raised over the value
proposition. Is there progress happening behind the scenes or should we close
this entry for now, to re-open in case there is renewed activity/interest?

cheers ./daniel

#24Daniel Gustafsson
daniel@yesql.se
In reply to: Daniel Gustafsson (#23)
Re: explain HashAggregate to report bucket and memory stats

On 12 Jul 2020, at 21:52, Daniel Gustafsson <daniel@yesql.se> wrote:

This thread has stalled and the patch has been Waiting on Author since March,
and skimming the thread there seems to be questions raised over the value
proposition. Is there progress happening behind the scenes or should we close
this entry for now, to re-open in case there is renewed activity/interest?

With not too many days of the commitfest left, I'm closing this in 2020-07.
Please feel free to add a new entry if there is renewed interest in this patch.

cheers ./daniel