From 90447a90fca3c5a32c16509a296a8a9fefaaf5f8 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Mon, 26 Feb 2024 20:17:40 +0100
Subject: [PATCH v10] Explain: Add SERIALIZE option

This option integrates with TIMING, and is gated behind ANALYZE.

EXPLAIN (SERIALIZE) allows analysis of the cost of actually serializing
the resultset, which usually can't be tested without actually consuming
the resultset on the client. As sending a resultset of gigabytes across
e.g. a VPN connection can be slow and expensive, this option increases
coverage of EXPLAIN and allows for further diagnostics in case of e.g.
attributes that are slow to deTOAST.

Future iterations may want to further instrument the deTOAST and ANALYZE
infrastructure to measure counts of deTOAST operations, but that is not
part of this patch.

Original patch by Stepan Rutz <stepan.rutz@gmx.de>, heavily modified by
myself (Matthias van de Meent <boekewurm+postgres@gmail.com>)

Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
---
 doc/src/sgml/ref/explain.sgml         |  27 ++
 src/backend/commands/explain.c        | 444 +++++++++++++++++++++++++-
 src/backend/tcop/dest.c               |   7 +
 src/include/commands/explain.h        |  11 +
 src/include/tcop/dest.h               |   2 +
 src/test/regress/expected/explain.out |  61 +++-
 src/test/regress/sql/explain.sql      |  28 +-
 src/tools/pgindent/typedefs.list      |   1 +
 8 files changed, 576 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index a4b6564bdb..237fbe00d1 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -41,6 +41,7 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep
     SETTINGS [ <replaceable class="parameter">boolean</replaceable> ]
     GENERIC_PLAN [ <replaceable class="parameter">boolean</replaceable> ]
     BUFFERS [ <replaceable class="parameter">boolean</replaceable> ]
+    SERIALIZE [ { NONE | TEXT | BINARY } ]
     WAL [ <replaceable class="parameter">boolean</replaceable> ]
     TIMING [ <replaceable class="parameter">boolean</replaceable> ]
     SUMMARY [ <replaceable class="parameter">boolean</replaceable> ]
@@ -206,6 +207,32 @@ ROLLBACK;
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>SERIALIZE</literal></term>
+    <listitem>
+     <para>
+      Specifies whether the query's results should be serialized as if the
+      data was sent to the client using textual or binary representations.
+      If the value value is <literal>NONE</literal> we don't process output
+      tuples, so data from <acronym>TOAST</acronym>ed values is not accessed
+      and EXPLAIN timings may be unexpected.  If the value is
+      <literal>TEXT</literal> or <literal>BINARY</literal>,
+      <productname>PostgreSQL</productname> will measure the size of the
+      would-be transmitted data after serializing the rows to
+      <literal>DataRow</literal> packets, using each column type's
+      <literal>output</literal> (for <literal>TEXT</literal>) or
+      <literal>send</literal> (for <literal>BINARY</literal>) functions for
+      serializing the column's values.  When the <literal>TIMING</literal>
+      option is enabled, the output also includes how much time was spent to
+      serialize the data.
+      This parameter may only be used when <literal>ANALYZE</literal> is also
+      enabled.  The default value for this parameter is <literal>NONE</literal>,
+      but when <literal>SERIALIZE</literal> is provided without parameters,
+      <literal>TEXT</literal> is used instead.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WAL</literal></term>
     <listitem>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 926d70afaf..19e9805245 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -20,6 +20,7 @@
 #include "commands/prepare.h"
 #include "foreign/fdwapi.h"
 #include "jit/jit.h"
+#include "libpq/pqformat.h"
 #include "nodes/extensible.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -32,6 +33,7 @@
 #include "utils/guc_tables.h"
 #include "utils/json.h"
 #include "utils/lsyscache.h"
+#include "utils/memdebug.h"
 #include "utils/rel.h"
 #include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
@@ -46,6 +48,14 @@ ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;
 /* Hook for plugins to get control in explain_get_index_name() */
 explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
 
+/* Instrumentation structures for EXPLAIN's SERIALIZE option */
+typedef struct ExplSerInstrumentation
+{
+	uint64		bytesSent;		/* # of bytes serialized */
+	instr_time	timeSpent;		/* time spent serializing */
+	BufferUsage bufferUsage;	/* buffers accessed for serialization */
+	ExplainSerializeFormat serialize;	/* serialization format */
+}			ExplSerInstrumentation;
 
 /* OR-able flags for ExplainXMLTag() */
 #define X_OPENING 0
@@ -59,6 +69,8 @@ static void ExplainOneQuery(Query *query, int cursorOptions,
 							QueryEnvironment *queryEnv);
 static void ExplainPrintJIT(ExplainState *es, int jit_flags,
 							JitInstrumentation *ji);
+static void ExplainPrintSerialize(ExplainState *es,
+								  ExplSerInstrumentation * instr);
 static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
 							ExplainState *es);
 static double elapsed_time(instr_time *starttime);
@@ -154,7 +166,7 @@ static void ExplainJSONLineEnding(ExplainState *es);
 static void ExplainYAMLLineStarting(ExplainState *es);
 static void escape_yaml(StringInfo buf, const char *str);
 
-
+static ExplSerInstrumentation GetSerializationMetrics(DestReceiver *dest);
 
 /*
  * ExplainQuery -
@@ -192,6 +204,35 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 			es->settings = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "generic_plan") == 0)
 			es->generic = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "serialize") == 0)
+		{
+			/* check the optional argument, if defined */
+			if (opt->arg)
+			{
+				char	   *p = defGetString(opt);
+
+				if (strcmp(p, "off") == 0 || strcmp(p, "none") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_NONE;
+				else if (strcmp(p, "text") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_TEXT;
+				else if (strcmp(p, "binary") == 0)
+					es->serialize = EXPLAIN_SERIALIZE_BINARY;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
+									opt->defname, p),
+							 parser_errposition(pstate, opt->location)));
+			}
+			else
+			{
+				/*
+				 * The default serialization mode when the option is specified
+				 * is 'text'.
+				 */
+				es->serialize = EXPLAIN_SERIALIZE_TEXT;
+			}
+		}
 		else if (strcmp(opt->defname, "timing") == 0)
 		{
 			timing_set = true;
@@ -246,6 +287,12 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("EXPLAIN option TIMING requires ANALYZE")));
 
+	/* check that serialize is used with EXPLAIN ANALYZE */
+	if (es->serialize != EXPLAIN_SERIALIZE_NONE && !es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN option SERIALIZE requires ANALYZE")));
+
 	/* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
 	if (es->generic && es->analyze)
 		ereport(ERROR,
@@ -576,6 +623,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	double		totaltime = 0;
 	int			eflags;
 	int			instrument_option = 0;
+	ExplSerInstrumentation serializeMetrics = {0};
 
 	Assert(plannedstmt->commandType != CMD_UTILITY);
 
@@ -604,11 +652,15 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	UpdateActiveSnapshotCommandId();
 
 	/*
-	 * Normally we discard the query's output, but if explaining CREATE TABLE
-	 * AS, we'd better use the appropriate tuple receiver.
+	 * We discard the output if we have no use for it. If we're explaining
+	 * CREATE TABLE AS, we'd better use the appropriate tuple receiver, and
+	 * when we EXPLAIN (ANALYZE, SERIALIZE) we better set up a serializing
+	 * (but discarding) DestReceiver.
 	 */
 	if (into)
 		dest = CreateIntoRelDestReceiver(into);
+	else if (es->analyze && es->serialize != EXPLAIN_SERIALIZE_NONE)
+		dest = CreateExplainSerializeDestReceiver(es);
 	else
 		dest = None_Receiver;
 
@@ -647,6 +699,13 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 		/* run cleanup too */
 		ExecutorFinish(queryDesc);
 
+		/* grab the metrics before we destroy the DestReceiver */
+		if (es->serialize != EXPLAIN_SERIALIZE_NONE)
+			serializeMetrics = GetSerializationMetrics(dest);
+
+		/* call the DestReceiver's destroy method even during explain */
+		dest->rDestroy(dest);
+
 		/* We can't run ExecutorEnd 'till we're done printing the stats... */
 		totaltime += elapsed_time(&starttime);
 	}
@@ -700,6 +759,10 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
 	if (es->costs)
 		ExplainPrintJITSummary(es, queryDesc);
 
+	/* print the info about serialization of data */
+	if (es->analyze && es->serialize != EXPLAIN_SERIALIZE_NONE)
+		ExplainPrintSerialize(es, &serializeMetrics);
+
 	/*
 	 * Close down the query and free resources.  Include time for this in the
 	 * total execution time (although it should be pretty minimal).
@@ -5161,3 +5224,378 @@ escape_yaml(StringInfo buf, const char *str)
 {
 	escape_json(buf, str);
 }
+
+
+/*
+ * Serializing DestReceiver functions
+ *
+ * EXPLAIN (ANALYZE) can fail to provide accurate results for some queries,
+ * which can usually be attributed to a lack of deTOASTing when the resultset
+ * isn't fully serialized, or other features usually only accessed in the
+ * DestReceiver functions. To measure the overhead of transferring the
+ * resulting dataset of a query, the SERIALIZE option is added, which can show
+ * and measure the relevant metrics available to a PostgreSQL server. This
+ * allows the measuring of server time spent on deTOASTing, serialization and
+ * copying of data.
+ *
+ * However, this critically does not measure the network performance: All
+ * measured timings are about processes inside the database.
+ */
+
+/* an attribute info cached for each column */
+typedef struct SerializeAttrInfo
+{								/* Per-attribute information */
+	Oid			typoutput;		/* Oid for the type's text output fn */
+	Oid			typsend;		/* Oid for the type's binary output fn */
+	bool		typisvarlena;	/* is it varlena (ie possibly toastable)? */
+	int8		format;			/* text of binary, like pq wire protocol */
+	FmgrInfo	finfo;			/* Precomputed call info for output fn */
+}			SerializeAttrInfo;
+
+/*
+ * A DestReceiver for query tuples, that serializes passed rows to RowData
+ * messages while measuring time taken in serialization and total serialized
+ * size, while never sending the data to the client.
+ */
+typedef struct SerializeDestReceiver
+{
+	DestReceiver pub;
+	MemoryContext memoryContext;
+	ExplainState *es;			/* this EXPLAIN-statement's ExplainState */
+	int8		format;			/* text or binary, like pq wire protocol */
+	TupleDesc	tupdesc;		/* the output tuple desc */
+	SerializeAttrInfo *attrInfo;	/* Cached info about each attr */
+	int			nattrs;
+	StringInfoData buf;			/* serialization buffer to hold the output
+								 * data */
+	ExplSerInstrumentation metrics; /* metrics */
+} SerializeDestReceiver;
+
+/*
+ * Get the lookup info that the row-callback of the receiver needs. this code
+ * is similar to the code from printup.c except that it doesn't do any actual
+ * output.
+ */
+static void
+serialize_prepare_info(SerializeDestReceiver *receiver, TupleDesc typeinfo,
+					   int nattrs)
+{
+	/* get rid of any old data */
+	if (receiver->attrInfo)
+		pfree(receiver->attrInfo);
+	receiver->attrInfo = NULL;
+
+	receiver->tupdesc = typeinfo;
+	receiver->nattrs = nattrs;
+	if (nattrs <= 0)
+		return;
+
+	receiver->attrInfo = (SerializeAttrInfo *)
+		palloc0(nattrs * sizeof(SerializeAttrInfo));
+
+	for (int i = 0; i < nattrs; i++)
+	{
+		SerializeAttrInfo *info = &receiver->attrInfo[i];
+		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+		info->format = receiver->format;
+
+		if (info->format == 0)
+		{
+			/* wire protocol format text */
+			getTypeOutputInfo(attr->atttypid,
+							  &info->typoutput,
+							  &info->typisvarlena);
+			fmgr_info(info->typoutput, &info->finfo);
+		}
+		else if (info->format == 1)
+		{
+			/* wire protocol format binary */
+			getTypeBinaryOutputInfo(attr->atttypid,
+									&info->typsend,
+									&info->typisvarlena);
+			fmgr_info(info->typsend, &info->finfo);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unsupported format code: %d", info->format)));
+		}
+	}
+}
+
+
+
+/*
+ * serializeAnalyzeReceive - process tuples for EXPLAIN (SERIALIZE)
+ *
+ * This method receives the tuples/records during EXPLAIN (ANALYZE, SERIALIZE)
+ * and serializes them while measuring various things about that
+ * serialization, in a way that should be as close as possible to printtup.c
+ * without actually sending the data; thus capturing the overhead of
+ * deTOASTing and type's out/sendfuncs, which are not otherwise exercisable
+ * without actually hitting the network, thus increasing the number of paths
+ * you can exercise with EXPLAIN.
+ *
+ * See also: printtup() in printtup.c, the older twin of this code.
+ */
+static bool
+serializeAnalyzeReceive(TupleTableSlot *slot, DestReceiver *self)
+{
+	TupleDesc	tupdesc;
+	MemoryContext oldcontext;
+	SerializeDestReceiver *receiver = (SerializeDestReceiver *) self;
+	StringInfo	buf = &receiver->buf;
+	instr_time	start,
+				end;
+	BufferUsage instr_start;
+
+	tupdesc = slot->tts_tupleDescriptor;
+
+	/* only measure time, buffers if requested */
+	if (receiver->es->timing)
+		INSTR_TIME_SET_CURRENT(start);
+	if (receiver->es->buffers)
+		instr_start = pgBufferUsage;
+
+	/* Cache attribute infos and function oid if outdated */
+	if (receiver->tupdesc != tupdesc || receiver->nattrs != tupdesc->natts)
+		serialize_prepare_info(receiver, tupdesc, tupdesc->natts);
+
+	/*
+	 * Fill all the slot's attributes, we can now use slot->tts_values and its
+	 * tts_isnull array which should be long enough even if added a
+	 * null-column to the table
+	 */
+	slot_getallattrs(slot);
+
+	oldcontext = MemoryContextSwitchTo(receiver->memoryContext);
+
+	/*
+	 * Note that we us an actual StringInfo buffer. This is to include the
+	 * cost of memory accesses and copy operations, reducing the number of
+	 * operations unique to the true printtup path vs the EXPLAIN (SERIALIZE)
+	 * path.
+	 */
+	pq_beginmessage_reuse(buf, 'D');
+	pq_sendint16(buf, receiver->nattrs);
+
+	/*
+	 * Iterate over all attributes of the tuple and invoke the output func (or
+	 * send function in case of a binary format). We'll completely ignore the
+	 * result. The MemoryContext is reset at the end of this per-tuple
+	 * callback anyhow.
+	 */
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		SerializeAttrInfo *thisState = receiver->attrInfo + i;
+		Datum		attr = slot->tts_values[i];
+
+		if (slot->tts_isnull[i])
+		{
+			pq_sendint32(buf, -1);
+			continue;
+		}
+
+		if (thisState->format == 0)
+		{
+			/* Text output */
+			char	   *outputstr;
+
+			outputstr = OutputFunctionCall(&thisState->finfo, attr);
+			pq_sendcountedtext(buf, outputstr, strlen(outputstr));
+		}
+		else
+		{
+			/* Binary output */
+			bytea	   *outputbytes;
+
+			Assert(thisState->format == 1);
+
+			outputbytes = SendFunctionCall(&thisState->finfo, attr);
+			pq_sendint32(buf, VARSIZE(outputbytes) - VARHDRSZ);
+			pq_sendbytes(buf, VARDATA(outputbytes),
+						 VARSIZE(outputbytes) - VARHDRSZ);
+		}
+	}
+
+	/* finalize the timers */
+	if (receiver->es->timing)
+	{
+		INSTR_TIME_SET_CURRENT(end);
+		INSTR_TIME_ACCUM_DIFF(receiver->metrics.timeSpent, end, start);
+	}
+
+	/* finalize buffer metrics */
+	if (receiver->es->buffers)
+		BufferUsageAccumDiff(&receiver->metrics.bufferUsage,
+							 &pgBufferUsage,
+							 &instr_start);
+
+	/*
+	 * Register the size of the packet we would've sent to the client. The
+	 * buffer will be dropped on the next iteration.
+	 */
+	receiver->metrics.bytesSent += buf->len;
+
+	/* cleanup and reset */
+	MemoryContextSwitchTo(oldcontext);
+	MemoryContextReset(receiver->memoryContext);
+
+	return true;
+}
+
+static void
+serializeAnalyzeStartup(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+	SerializeDestReceiver *receiver = (SerializeDestReceiver *) self;
+
+	Assert(receiver->es != NULL);
+
+	switch (receiver->es->serialize)
+	{
+		case EXPLAIN_SERIALIZE_NONE:
+			Assert(false);
+			elog(ERROR, "Invalid explain serialization format code %d", receiver->es->serialize);
+			break;
+		case EXPLAIN_SERIALIZE_TEXT:
+			receiver->format = 0;	/* wire protocol format text */
+			break;
+		case EXPLAIN_SERIALIZE_BINARY:
+			receiver->format = 1;	/* wire protocol format binary */
+			break;
+	}
+
+	memset(&receiver->metrics, 0, sizeof(ExplSerInstrumentation));
+	receiver->metrics.serialize = receiver->es->serialize;
+
+	/* memory context for our work */
+	receiver->memoryContext = AllocSetContextCreate(CurrentMemoryContext,
+													"SerializeTupleReceive", ALLOCSET_DEFAULT_SIZES);
+
+	/* initialize various fields */
+	INSTR_TIME_SET_ZERO(receiver->metrics.timeSpent);
+	initStringInfo(&receiver->buf);
+}
+
+/*
+ * serializeAnalyzeShutdown - shut down the serializeAnalyze receiver
+ */
+static void
+serializeAnalyzeShutdown(DestReceiver *self)
+{
+	SerializeDestReceiver *receiver = (SerializeDestReceiver *) self;
+
+	if (receiver->attrInfo)
+		pfree(receiver->attrInfo);
+	receiver->attrInfo = NULL;
+
+	if (receiver->buf.data)
+		pfree(receiver->buf.data);
+	receiver->buf.data = NULL;
+
+	if (receiver->memoryContext)
+		MemoryContextDelete(receiver->memoryContext);
+	receiver->memoryContext = NULL;
+}
+
+/*
+ * serializeAnalyzeShutdown - shut down the serializeAnalyze receiver
+ */
+static void
+serializeAnalyzeDestroy(DestReceiver *self)
+{
+	pfree(self);
+}
+
+/* Build a DestReceiver with EXPLAIN (SERIALIZE) instrumentation. */
+DestReceiver *
+CreateExplainSerializeDestReceiver(ExplainState *es)
+{
+	SerializeDestReceiver *self;
+
+	self = (SerializeDestReceiver *) palloc0(sizeof(SerializeDestReceiver));
+
+	self->pub.receiveSlot = serializeAnalyzeReceive;
+	self->pub.rStartup = serializeAnalyzeStartup;
+	self->pub.rShutdown = serializeAnalyzeShutdown;
+	self->pub.rDestroy = serializeAnalyzeDestroy;
+	self->pub.mydest = DestExplainSerialize;
+
+	self->es = es;
+
+	return (DestReceiver *) self;
+}
+
+static ExplSerInstrumentation
+GetSerializationMetrics(DestReceiver *dest)
+{
+	ExplSerInstrumentation empty;
+
+	if (dest->mydest == DestExplainSerialize)
+		return ((SerializeDestReceiver *) dest)->metrics;
+
+	memset(&empty, 0, sizeof(ExplSerInstrumentation));
+	return empty;
+}
+
+/* Print data for the SERIALIZE option */
+static void
+ExplainPrintSerialize(ExplainState *es, ExplSerInstrumentation * instr)
+{
+	char	   *format;
+
+	Assert(es->serialize == instr->serialize);
+	/* We shouldn't get called for EXPLAIN_SERIALIZE_NONE */
+	if (instr->serialize == EXPLAIN_SERIALIZE_TEXT)
+		format = "text";
+	else
+	{
+		Assert(instr->serialize == EXPLAIN_SERIALIZE_BINARY);
+		format = "binary";
+	}
+
+	ExplainOpenGroup("Serialization", "Serialization", true, es);
+
+	if (es->format == EXPLAIN_FORMAT_TEXT)
+	{
+		ExplainIndentText(es);
+
+		/* timing is optional */
+		if (es->timing)
+			appendStringInfo(es->str, "Serialization: time=%.3f  produced=" INT64_FORMAT "kB  format=%s",
+							 1000.0 * INSTR_TIME_GET_DOUBLE(instr->timeSpent),
+							 instr->bytesSent / 1024,
+							 format);
+		else
+			appendStringInfo(es->str, "Serialization: produced=" INT64_FORMAT "kB  format=%s",
+							 instr->bytesSent / 1024,
+							 format);
+
+		appendStringInfoChar(es->str, '\n');
+
+		if (es->buffers && peek_buffer_usage(es, &instr->bufferUsage))
+		{
+			es->indent++;
+			show_buffer_usage(es, &instr->bufferUsage);
+			es->indent--;
+		}
+	}
+	else
+	{
+		if (es->timing)
+		{
+			ExplainPropertyFloat("Time", "ms",
+								 1000.0 * INSTR_TIME_GET_DOUBLE(instr->timeSpent),
+								 3, es);
+		}
+
+		ExplainPropertyUInteger("Written Bytes", "bytes",
+								instr->bytesSent, es);
+		ExplainPropertyText("Format", format, es);
+		show_buffer_usage(es, &instr->bufferUsage);
+	}
+
+	ExplainCloseGroup("Serialization", "Serialization", true, es);
+}
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 6d727ae24f..96f80b3046 100644
--- a/src/backend/tcop/dest.c
+++ b/src/backend/tcop/dest.c
@@ -33,6 +33,7 @@
 #include "access/xact.h"
 #include "commands/copy.h"
 #include "commands/createas.h"
+#include "commands/explain.h"
 #include "commands/matview.h"
 #include "executor/functions.h"
 #include "executor/tqueue.h"
@@ -151,6 +152,9 @@ CreateDestReceiver(CommandDest dest)
 
 		case DestTupleQueue:
 			return CreateTupleQueueDestReceiver(NULL);
+
+		case DestExplainSerialize:
+			return CreateExplainSerializeDestReceiver(NULL);
 	}
 
 	/* should never get here */
@@ -186,6 +190,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestExplainSerialize:
 			break;
 	}
 }
@@ -231,6 +236,7 @@ NullCommand(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestExplainSerialize:
 			break;
 	}
 }
@@ -274,6 +280,7 @@ ReadyForQuery(CommandDest dest)
 		case DestSQLFunction:
 		case DestTransientRel:
 		case DestTupleQueue:
+		case DestExplainSerialize:
 			break;
 	}
 }
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index cf195f1359..9f2da28f93 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -25,6 +25,13 @@ typedef enum ExplainFormat
 	EXPLAIN_FORMAT_YAML,
 } ExplainFormat;
 
+typedef enum ExplainSerializeFormat
+{
+	EXPLAIN_SERIALIZE_NONE,
+	EXPLAIN_SERIALIZE_TEXT,
+	EXPLAIN_SERIALIZE_BINARY,
+}			ExplainSerializeFormat;
+
 typedef struct ExplainWorkersState
 {
 	int			num_workers;	/* # of worker processes the plan used */
@@ -48,6 +55,8 @@ typedef struct ExplainState
 	bool		memory;			/* print planner's memory usage information */
 	bool		settings;		/* print modified settings */
 	bool		generic;		/* generate a generic plan */
+	ExplainSerializeFormat serialize;	/* do serialization (in ANALZYE) */
+
 	ExplainFormat format;		/* output format */
 	/* state for output formatting --- not reset for each new plan tree */
 	int			indent;			/* current indentation level */
@@ -132,4 +141,6 @@ extern void ExplainOpenGroup(const char *objtype, const char *labelname,
 extern void ExplainCloseGroup(const char *objtype, const char *labelname,
 							  bool labeled, ExplainState *es);
 
+extern DestReceiver *CreateExplainSerializeDestReceiver(ExplainState *es);
+
 #endif							/* EXPLAIN_H */
diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h
index 7e613bd7fc..851272a719 100644
--- a/src/include/tcop/dest.h
+++ b/src/include/tcop/dest.h
@@ -96,6 +96,8 @@ typedef enum
 	DestSQLFunction,			/* results sent to SQL-language func mgr */
 	DestTransientRel,			/* results sent to transient relation */
 	DestTupleQueue,				/* results sent to tuple queue */
+	DestExplainSerialize,		/* results are only serialized, not
+								 * transferred */
 } CommandDest;
 
 /* ----------------
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 1299ee79ad..bc121b0b52 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -135,7 +135,7 @@ select explain_filter('explain (analyze, buffers, format xml) select * from int8
  </explain>
 (1 row)
 
-select explain_filter('explain (analyze, buffers, format yaml) select * from int8_tbl i8');
+select explain_filter('explain (analyze, serialize, buffers, format yaml) select * from int8_tbl i8');
         explain_filter         
 -------------------------------
  - Plan:                      +
@@ -175,6 +175,20 @@ select explain_filter('explain (analyze, buffers, format yaml) select * from int
      Temp Written Blocks: N   +
    Planning Time: N.N         +
    Triggers:                  +
+   Serialization:             +
+     Time: N.N                +
+     Written Bytes: N         +
+     Format: "text"           +
+     Shared Hit Blocks: N     +
+     Shared Read Blocks: N    +
+     Shared Dirtied Blocks: N +
+     Shared Written Blocks: N +
+     Local Hit Blocks: N      +
+     Local Read Blocks: N     +
+     Local Dirtied Blocks: N  +
+     Local Written Blocks: N  +
+     Temp Read Blocks: N      +
+     Temp Written Blocks: N   +
    Execution Time: N.N
 (1 row)
 
@@ -639,3 +653,48 @@ select explain_filter('explain (verbose) select * from int8_tbl i8');
  Query Identifier: N
 (3 rows)
 
+-- Test that SERIALIZE is accepted as a parameter to explain
+-- timings are filtered out by explain_filter
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize) select * from test_serialize');
+                                          explain_filter                                          
+--------------------------------------------------------------------------------------------------
+ Seq Scan on test_serialize  (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ Planning Time: N.N ms
+ Serialization: time=N.N  produced=NkB  format=text
+ Execution Time: N.N ms
+(4 rows)
+
+drop table test_serialize;
+-- Test that SERIALIZE BINARY is accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize binary,buffers) select * from test_serialize');
+                                          explain_filter                                          
+--------------------------------------------------------------------------------------------------
+ Seq Scan on test_serialize  (cost=N.N..N.N rows=N width=N) (actual time=N.N..N.N rows=N loops=N)
+ Planning Time: N.N ms
+ Serialization: time=N.N  produced=NkB  format=binary
+ Execution Time: N.N ms
+(4 rows)
+
+drop table test_serialize;
+-- Test that _SERIALIZE invalidparameter_ is not accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize invalidparameter) select * from test_serialize');
+ERROR:  unrecognized value for EXPLAIN option "serialize": "invalidparameter"
+LINE 1: select explain_filter('explain (analyze,serialize invalidpar...
+                         ^
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
+drop table test_serialize;
+-- Test SERIALIZE is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize) select * from test_serialize');
+ERROR:  EXPLAIN option SERIALIZE requires ANALYZE
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
+drop table test_serialize;
+-- Test SERIALIZEBINARY is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize binary) select * from test_serialize');
+ERROR:  EXPLAIN option SERIALIZE requires ANALYZE
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
+drop table test_serialize;
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index 2274dc1b5a..76e441f258 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -66,7 +66,7 @@ select explain_filter('explain (analyze) select * from int8_tbl i8');
 select explain_filter('explain (analyze, verbose) select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers, format text) select * from int8_tbl i8');
 select explain_filter('explain (analyze, buffers, format xml) select * from int8_tbl i8');
-select explain_filter('explain (analyze, buffers, format yaml) select * from int8_tbl i8');
+select explain_filter('explain (analyze, serialize, buffers, format yaml) select * from int8_tbl i8');
 select explain_filter('explain (buffers, format text) select * from int8_tbl i8');
 select explain_filter('explain (buffers, format json) select * from int8_tbl i8');
 
@@ -162,3 +162,29 @@ select explain_filter('explain (verbose) select * from t1 where pg_temp.mysin(f1
 -- Test compute_query_id
 set compute_query_id = on;
 select explain_filter('explain (verbose) select * from int8_tbl i8');
+
+-- Test that SERIALIZE is accepted as a parameter to explain
+-- timings are filtered out by explain_filter
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize) select * from test_serialize');
+drop table test_serialize;
+
+-- Test that SERIALIZE BINARY is accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize binary,buffers) select * from test_serialize');
+drop table test_serialize;
+
+-- Test that _SERIALIZE invalidparameter_ is not accepted as a parameter to explain
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (analyze,serialize invalidparameter) select * from test_serialize');
+drop table test_serialize;
+
+-- Test SERIALIZE is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize) select * from test_serialize');
+drop table test_serialize;
+
+-- Test SERIALIZEBINARY is _not_ accepted as a parameter to explain unless ANALYZE is specified
+create table test_serialize(id bigserial, val text);
+select explain_filter('explain (serialize binary) select * from test_serialize');
+drop table test_serialize;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 79745ba913..873eaaa58a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2532,6 +2532,7 @@ SerCommitSeqNo
 SerialControl
 SerialIOData
 SerializableXactHandle
+SerializeDestReceiver
 SerializedActiveRelMaps
 SerializedClientConnectionInfo
 SerializedRanges
-- 
2.40.1

