Invent SERIALIZE option for EXPLAIN.
authorTom Lane <[email protected]>
Wed, 3 Apr 2024 21:41:54 +0000 (17:41 -0400)
committerTom Lane <[email protected]>
Wed, 3 Apr 2024 21:41:57 +0000 (17:41 -0400)
EXPLAIN (ANALYZE, SERIALIZE) allows collection of statistics about
the volume of data emitted by a query, as well as the time taken
to convert the data to the on-the-wire format.  Previously there
was no way to investigate this without actually sending the data
to the client, in which case network transmission costs might
swamp what you wanted to see.  In particular this feature allows
investigating the costs of de-TOASTing compressed or out-of-line
data during formatting.

Stepan Rutz and Matthias van de Meent,
reviewed by Tomas Vondra and myself

Discussion: https://postgr.es/m/ca0adb0e-fa4e-c37e-1cd7-91170b18cae1@gmx.de

doc/src/sgml/perform.sgml
doc/src/sgml/ref/explain.sgml
src/backend/access/common/printtup.c
src/backend/commands/explain.c
src/backend/tcop/dest.c
src/include/commands/explain.h
src/include/tcop/dest.h
src/test/regress/expected/explain.out
src/test/regress/sql/explain.sql
src/tools/pgindent/typedefs.list

index 4b831a62066e603eb4c7453974382b8372c8ffc3..2d0097f121a47a9f7b9b08b3fc33fe45c1d0007f 100644 (file)
@@ -144,11 +144,11 @@ EXPLAIN SELECT * FROM tenk1;
     It's important to understand that the cost of an upper-level node includes
     the cost of all its child nodes.  It's also important to realize that
     the cost only reflects things that the planner cares about.
-    In particular, the cost does not consider the time spent transmitting
-    result rows to the client, which could be an important
-    factor in the real elapsed time; but the planner ignores it because
-    it cannot change it by altering the plan.  (Every correct plan will
-    output the same row set, we trust.)
+    In particular, the cost does not consider the time spent to convert
+    output values to text form or to transmit them to the client, which
+    could be important factors in the real elapsed time; but the planner
+    ignores those costs because it cannot change them by altering the
+    plan.  (Every correct plan will output the same row set, we trust.)
    </para>
 
    <para>
@@ -956,6 +956,17 @@ EXPLAIN UPDATE parent SET f2 = f2 + 1 WHERE f1 = 101;
     <command>EXPLAIN ANALYZE</command>.
    </para>
 
+   <para>
+    The time shown for the top-level node does not include any time needed
+    to convert the query's output data into displayable form or to send it
+    to the client.  While <command>EXPLAIN ANALYZE</command> will never
+    send the data to the client, it can be told to convert the query's
+    output data to displayable form and measure the time needed for that,
+    by specifying the <literal>SERIALIZE</literal> option.  That time will
+    be shown separately, and it's also included in the
+    total <literal>Execution time</literal>.
+   </para>
+
   </sect2>
 
   <sect2 id="using-explain-caveats">
@@ -965,7 +976,8 @@ EXPLAIN UPDATE parent SET f2 = f2 + 1 WHERE f1 = 101;
     There are two significant ways in which run times measured by
     <command>EXPLAIN ANALYZE</command> can deviate from normal execution of
     the same query.  First, since no output rows are delivered to the client,
-    network transmission costs and I/O conversion costs are not included.
+    network transmission costs are not included.  I/O conversion costs are
+    not included either unless <literal>SERIALIZE</literal> is specified.
     Second, the measurement overhead added by <command>EXPLAIN
     ANALYZE</command> can be significant, especially on machines with slow
     <function>gettimeofday()</function> operating-system calls. You can use the
index a4b6564bdb3adda79246525a6a1826208a431a61..db9d3a8549add498428cd602e4a4652e15fedc03 100644 (file)
@@ -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,34 @@ ROLLBACK;
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>SERIALIZE</literal></term>
+    <listitem>
+     <para>
+      Include information on the cost
+      of <firstterm>serializing</firstterm> the query's output data, that
+      is converting it to text or binary format to send to the client.
+      This can be a significant part of the time required for regular
+      execution of the query, if the datatype output functions are
+      expensive or if <acronym>TOAST</acronym>ed values must be fetched
+      from out-of-line storage.  <command>EXPLAIN</command>'s default
+      behavior, <literal>SERIALIZE NONE</literal>, does not perform these
+      conversions.  If <literal>SERIALIZE TEXT</literal>
+      or <literal>SERIALIZE BINARY</literal> is specified, the appropriate
+      conversions are performed, and the time spent doing so is measured
+      (unless <literal>TIMING OFF</literal> is specified).  If
+      the <literal>BUFFERS</literal> option is also specified, then any
+      buffer accesses involved in the conversions are counted too.
+      In no case, however, will <command>EXPLAIN</command> actually send
+      the resulting data to the client; hence network transmission costs
+      cannot be investigated this way.
+      Serialization may only be enabled when <literal>ANALYZE</literal> is
+      also enabled.  If <literal>SERIALIZE</literal> is written without an
+      argument, <literal>TEXT</literal> is assumed.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>WAL</literal></term>
     <listitem>
index 37c5aa2b9554682afa5d42bae557f61cccdf1aca..f2d5ca14feea522b611f217374d846c5488e8e0b 100644 (file)
@@ -294,6 +294,9 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 
 /* ----------------
  *     printtup --- send a tuple to the client
+ *
+ * Note: if you change this function, see also serializeAnalyzeReceive
+ * in explain.c, which is meant to replicate the computations done here.
  * ----------------
  */
 static bool
@@ -317,7 +320,7 @@ printtup(TupleTableSlot *slot, DestReceiver *self)
    oldcontext = MemoryContextSwitchTo(myState->tmpcontext);
 
    /*
-    * Prepare a DataRow message (note buffer is in per-row context)
+    * Prepare a DataRow message (note buffer is in per-query context)
     */
    pq_beginmessage_reuse(buf, 'D');
 
index 926d70afaf8ec807b0c3bf193a3b9c3f175a1ce0..895d18ebd599b27f2346e42cd84c23ef2630c388 100644 (file)
@@ -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"
@@ -47,6 +48,14 @@ ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;
 explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
 
 
+/* Instrumentation data for SERIALIZE option */
+typedef struct SerializeMetrics
+{
+   uint64      bytesSent;      /* # of bytes serialized */
+   instr_time  timeSpent;      /* time spent serializing */
+   BufferUsage bufferUsage;    /* buffers accessed during serialization */
+} SerializeMetrics;
+
 /* OR-able flags for ExplainXMLTag() */
 #define X_OPENING 0
 #define X_CLOSING 1
@@ -59,6 +68,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,
+                                 SerializeMetrics *metrics);
 static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
                            ExplainState *es);
 static double elapsed_time(instr_time *starttime);
@@ -153,6 +164,7 @@ static void ExplainIndentText(ExplainState *es);
 static void ExplainJSONLineEnding(ExplainState *es);
 static void ExplainYAMLLineStarting(ExplainState *es);
 static void escape_yaml(StringInfo buf, const char *str);
+static SerializeMetrics GetSerializationMetrics(DestReceiver *dest);
 
 
 
@@ -204,6 +216,31 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
        }
        else if (strcmp(opt->defname, "memory") == 0)
            es->memory = defGetBoolean(opt);
+       else if (strcmp(opt->defname, "serialize") == 0)
+       {
+           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
+           {
+               /* SERIALIZE without an argument is taken as 'text' */
+               es->serialize = EXPLAIN_SERIALIZE_TEXT;
+           }
+       }
        else if (strcmp(opt->defname, "format") == 0)
        {
            char       *p = defGetString(opt);
@@ -246,6 +283,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 +619,7 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
    double      totaltime = 0;
    int         eflags;
    int         instrument_option = 0;
+   SerializeMetrics serializeMetrics = {0};
 
    Assert(plannedstmt->commandType != CMD_UTILITY);
 
@@ -604,11 +648,17 @@ 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, while
+    * the SERIALIZE option requires its own tuple receiver.  (If you specify
+    * SERIALIZE while explaining CREATE TABLE AS, you'll see zeroes for the
+    * results, which is appropriate since no data would have gone to the
+    * client.)
     */
    if (into)
        dest = CreateIntoRelDestReceiver(into);
+   else if (es->serialize != EXPLAIN_SERIALIZE_NONE)
+       dest = CreateExplainSerializeDestReceiver(es);
    else
        dest = None_Receiver;
 
@@ -651,6 +701,13 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
        totaltime += elapsed_time(&starttime);
    }
 
+   /* grab serialization 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);
+
    ExplainOpenGroup("Query", NULL, true, es);
 
    /* Create textual dump of plan tree */
@@ -700,6 +757,10 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
    if (es->costs)
        ExplainPrintJITSummary(es, queryDesc);
 
+   /* Print info about serialization of output */
+   if (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).
@@ -1033,6 +1094,62 @@ ExplainPrintJIT(ExplainState *es, int jit_flags, JitInstrumentation *ji)
    ExplainCloseGroup("JIT", "JIT", true, es);
 }
 
+/*
+ * ExplainPrintSerialize -
+ *   Append information about query output volume to es->str.
+ */
+static void
+ExplainPrintSerialize(ExplainState *es, SerializeMetrics *metrics)
+{
+   const char *format;
+
+   /* We shouldn't get called for EXPLAIN_SERIALIZE_NONE */
+   if (es->serialize == EXPLAIN_SERIALIZE_TEXT)
+       format = "text";
+   else
+   {
+       Assert(es->serialize == EXPLAIN_SERIALIZE_BINARY);
+       format = "binary";
+   }
+
+   ExplainOpenGroup("Serialization", "Serialization", true, es);
+
+   if (es->format == EXPLAIN_FORMAT_TEXT)
+   {
+       ExplainIndentText(es);
+       if (es->timing)
+           appendStringInfo(es->str, "Serialization: time=%.3f ms  output=" INT64_FORMAT "kB  format=%s\n",
+                            1000.0 * INSTR_TIME_GET_DOUBLE(metrics->timeSpent),
+                            (metrics->bytesSent + 512) / 1024,
+                            format);
+       else
+           appendStringInfo(es->str, "Serialization: output=" INT64_FORMAT "kB  format=%s\n",
+                            (metrics->bytesSent + 512) / 1024,
+                            format);
+
+       if (es->buffers && peek_buffer_usage(es, &metrics->bufferUsage))
+       {
+           es->indent++;
+           show_buffer_usage(es, &metrics->bufferUsage);
+           es->indent--;
+       }
+   }
+   else
+   {
+       if (es->timing)
+           ExplainPropertyFloat("Time", "ms",
+                                1000.0 * INSTR_TIME_GET_DOUBLE(metrics->timeSpent),
+                                3, es);
+       ExplainPropertyUInteger("Output Volume", "kB",
+                               (metrics->bytesSent + 512) / 1024, es);
+       ExplainPropertyText("Format", format, es);
+       if (es->buffers)
+           show_buffer_usage(es, &metrics->bufferUsage);
+   }
+
+   ExplainCloseGroup("Serialization", "Serialization", true, es);
+}
+
 /*
  * ExplainQueryText -
  *   add a "Query Text" node that contains the actual text of the query
@@ -5161,3 +5278,293 @@ escape_yaml(StringInfo buf, const char *str)
 {
    escape_json(buf, str);
 }
+
+
+/*
+ * DestReceiver functions for SERIALIZE option
+ *
+ * A DestReceiver for query tuples, that serializes passed rows into RowData
+ * messages while measuring the resources expended and total serialized size,
+ * while never sending the data to the client.  This allows measuring the
+ * overhead of deTOASTing and datatype out/sendfuncs, which are not otherwise
+ * exercisable without actually hitting the network.
+ */
+typedef struct SerializeDestReceiver
+{
+   DestReceiver pub;
+   ExplainState *es;           /* this EXPLAIN statement's ExplainState */
+   int8        format;         /* text or binary, like pq wire protocol */
+   TupleDesc   attrinfo;       /* the output tuple desc */
+   int         nattrs;         /* current number of columns */
+   FmgrInfo   *finfos;         /* precomputed call info for output fns */
+   MemoryContext tmpcontext;   /* per-row temporary memory context */
+   StringInfoData buf;         /* buffer to hold the constructed message */
+   SerializeMetrics metrics;   /* collected metrics */
+} SerializeDestReceiver;
+
+/*
+ * Get the function lookup info that we'll need for output.
+ *
+ * This is a subset of what printtup_prepare_info() does.  We don't need to
+ * cope with format choices varying across columns, so it's slightly simpler.
+ */
+static void
+serialize_prepare_info(SerializeDestReceiver *receiver,
+                      TupleDesc typeinfo, int nattrs)
+{
+   /* get rid of any old data */
+   if (receiver->finfos)
+       pfree(receiver->finfos);
+   receiver->finfos = NULL;
+
+   receiver->attrinfo = typeinfo;
+   receiver->nattrs = nattrs;
+   if (nattrs <= 0)
+       return;
+
+   receiver->finfos = (FmgrInfo *) palloc0(nattrs * sizeof(FmgrInfo));
+
+   for (int i = 0; i < nattrs; i++)
+   {
+       FmgrInfo   *finfo = receiver->finfos + i;
+       Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+       Oid         typoutput;
+       Oid         typsend;
+       bool        typisvarlena;
+
+       if (receiver->format == 0)
+       {
+           /* wire protocol format text */
+           getTypeOutputInfo(attr->atttypid,
+                             &typoutput,
+                             &typisvarlena);
+           fmgr_info(typoutput, finfo);
+       }
+       else if (receiver->format == 1)
+       {
+           /* wire protocol format binary */
+           getTypeBinaryOutputInfo(attr->atttypid,
+                                   &typsend,
+                                   &typisvarlena);
+           fmgr_info(typsend, finfo);
+       }
+       else
+           ereport(ERROR,
+                   (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                    errmsg("unsupported format code: %d", receiver->format)));
+   }
+}
+
+/*
+ * serializeAnalyzeReceive - collect tuples for EXPLAIN (SERIALIZE)
+ *
+ * This should match printtup() in printtup.c as closely as possible,
+ * except for the addition of measurement code.
+ */
+static bool
+serializeAnalyzeReceive(TupleTableSlot *slot, DestReceiver *self)
+{
+   TupleDesc   typeinfo = slot->tts_tupleDescriptor;
+   SerializeDestReceiver *myState = (SerializeDestReceiver *) self;
+   MemoryContext oldcontext;
+   StringInfo  buf = &myState->buf;
+   int         natts = typeinfo->natts;
+   instr_time  start,
+               end;
+   BufferUsage instr_start;
+
+   /* only measure time, buffers if requested */
+   if (myState->es->timing)
+       INSTR_TIME_SET_CURRENT(start);
+   if (myState->es->buffers)
+       instr_start = pgBufferUsage;
+
+   /* Set or update my derived attribute info, if needed */
+   if (myState->attrinfo != typeinfo || myState->nattrs != natts)
+       serialize_prepare_info(myState, typeinfo, natts);
+
+   /* Make sure the tuple is fully deconstructed */
+   slot_getallattrs(slot);
+
+   /* Switch into per-row context so we can recover memory below */
+   oldcontext = MemoryContextSwitchTo(myState->tmpcontext);
+
+   /*
+    * Prepare a DataRow message (note buffer is in per-query context)
+    *
+    * Note that we fill a StringInfo buffer the same as printtup() does, so
+    * as to capture the costs of manipulating the strings accurately.
+    */
+   pq_beginmessage_reuse(buf, 'D');
+
+   pq_sendint16(buf, natts);
+
+   /*
+    * send the attributes of this tuple
+    */
+   for (int i = 0; i < natts; i++)
+   {
+       FmgrInfo   *finfo = myState->finfos + i;
+       Datum       attr = slot->tts_values[i];
+
+       if (slot->tts_isnull[i])
+       {
+           pq_sendint32(buf, -1);
+           continue;
+       }
+
+       if (myState->format == 0)
+       {
+           /* Text output */
+           char       *outputstr;
+
+           outputstr = OutputFunctionCall(finfo, attr);
+           pq_sendcountedtext(buf, outputstr, strlen(outputstr));
+       }
+       else
+       {
+           /* Binary output */
+           bytea      *outputbytes;
+
+           outputbytes = SendFunctionCall(finfo, attr);
+           pq_sendint32(buf, VARSIZE(outputbytes) - VARHDRSZ);
+           pq_sendbytes(buf, VARDATA(outputbytes),
+                        VARSIZE(outputbytes) - VARHDRSZ);
+       }
+   }
+
+   /*
+    * We mustn't call pq_endmessage_reuse(), since that would actually send
+    * the data to the client.  Just count the data, instead.  We can leave
+    * the buffer alone; it'll be reset on the next iteration (as would also
+    * happen in printtup()).
+    */
+   myState->metrics.bytesSent += buf->len;
+
+   /* Return to caller's context, and flush row's temporary memory */
+   MemoryContextSwitchTo(oldcontext);
+   MemoryContextReset(myState->tmpcontext);
+
+   /* Update timing data */
+   if (myState->es->timing)
+   {
+       INSTR_TIME_SET_CURRENT(end);
+       INSTR_TIME_ACCUM_DIFF(myState->metrics.timeSpent, end, start);
+   }
+
+   /* Update buffer metrics */
+   if (myState->es->buffers)
+       BufferUsageAccumDiff(&myState->metrics.bufferUsage,
+                            &pgBufferUsage,
+                            &instr_start);
+
+   return true;
+}
+
+/*
+ * serializeAnalyzeStartup - start up the serializeAnalyze receiver
+ */
+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);
+           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;
+   }
+
+   /* Create per-row temporary memory context */
+   receiver->tmpcontext = AllocSetContextCreate(CurrentMemoryContext,
+                                                "SerializeTupleReceive",
+                                                ALLOCSET_DEFAULT_SIZES);
+
+   /* The output buffer is re-used across rows, as in printtup.c */
+   initStringInfo(&receiver->buf);
+
+   /* Initialize results counters */
+   memset(&receiver->metrics, 0, sizeof(SerializeMetrics));
+   INSTR_TIME_SET_ZERO(receiver->metrics.timeSpent);
+}
+
+/*
+ * serializeAnalyzeShutdown - shut down the serializeAnalyze receiver
+ */
+static void
+serializeAnalyzeShutdown(DestReceiver *self)
+{
+   SerializeDestReceiver *receiver = (SerializeDestReceiver *) self;
+
+   if (receiver->finfos)
+       pfree(receiver->finfos);
+   receiver->finfos = NULL;
+
+   if (receiver->buf.data)
+       pfree(receiver->buf.data);
+   receiver->buf.data = NULL;
+
+   if (receiver->tmpcontext)
+       MemoryContextDelete(receiver->tmpcontext);
+   receiver->tmpcontext = NULL;
+}
+
+/*
+ * serializeAnalyzeDestroy - destroy the serializeAnalyze receiver
+ */
+static void
+serializeAnalyzeDestroy(DestReceiver *self)
+{
+   pfree(self);
+}
+
+/*
+ * Build a DestReceiver for 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;
+}
+
+/*
+ * GetSerializationMetrics - collect metrics
+ *
+ * We have to be careful here since the receiver could be an IntoRel
+ * receiver if the subject statement is CREATE TABLE AS.  In that
+ * case, return all-zeroes stats.
+ */
+static SerializeMetrics
+GetSerializationMetrics(DestReceiver *dest)
+{
+   SerializeMetrics empty;
+
+   if (dest->mydest == DestExplainSerialize)
+       return ((SerializeDestReceiver *) dest)->metrics;
+
+   memset(&empty, 0, sizeof(SerializeMetrics));
+   INSTR_TIME_SET_ZERO(empty.timeSpent);
+
+   return empty;
+}
index 6d727ae24fcd82eb084c557d9493620b8a8c0561..96f80b30463460e0765476444b924ccf3412ae88 100644 (file)
@@ -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;
    }
 }
index cf195f1359761795bac733f5793e230582ad82b8..9b8b351d9a218afb1792a13c61848f54fafd1ae5 100644 (file)
 #include "lib/stringinfo.h"
 #include "parser/parse_node.h"
 
+typedef enum ExplainSerializeOption
+{
+   EXPLAIN_SERIALIZE_NONE,
+   EXPLAIN_SERIALIZE_TEXT,
+   EXPLAIN_SERIALIZE_BINARY,
+} ExplainSerializeOption;
+
 typedef enum ExplainFormat
 {
    EXPLAIN_FORMAT_TEXT,
@@ -48,6 +55,7 @@ typedef struct ExplainState
    bool        memory;         /* print planner's memory usage information */
    bool        settings;       /* print modified settings */
    bool        generic;        /* generate a generic plan */
+   ExplainSerializeOption serialize;   /* serialize the query's output? */
    ExplainFormat format;       /* output format */
    /* state for output formatting --- not reset for each new plan tree */
    int         indent;         /* current indentation level */
@@ -132,4 +140,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 */
index 7e613bd7fcc6306169fac9b1bdedd03b6efd34c0..a3d521b6f97f8ede4fd569a31342857dacf9a55a 100644 (file)
@@ -96,6 +96,7 @@ 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 serialized and discarded */
 } CommandDest;
 
 /* ----------------
index 1299ee79ad58c50358ee25c1eb3daba64e7b4e2a..703800d856dc72358995ed67bbe0dc26e0453088 100644 (file)
@@ -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                +
+     Output Volume: 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,41 @@ select explain_filter('explain (verbose) select * from int8_tbl i8');
  Query Identifier: N
 (3 rows)
 
+-- Test SERIALIZE option
+select explain_filter('explain (analyze,serialize) select * from int8_tbl i8');
+                                        explain_filter                                         
+-----------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8  (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 ms  output=NkB  format=text
+ Execution Time: N.N ms
+(4 rows)
+
+select explain_filter('explain (analyze,serialize text,buffers,timing off) select * from int8_tbl i8');
+                                 explain_filter                                  
+---------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8  (cost=N.N..N.N rows=N width=N) (actual rows=N loops=N)
+ Planning Time: N.N ms
+ Serialization: output=NkB  format=text
+ Execution Time: N.N ms
+(4 rows)
+
+select explain_filter('explain (analyze,serialize binary,buffers,timing) select * from int8_tbl i8');
+                                        explain_filter                                         
+-----------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8  (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 ms  output=NkB  format=binary
+ Execution Time: N.N ms
+(4 rows)
+
+-- this tests an edge case where we have no data to return
+select explain_filter('explain (analyze,serialize) create temp table explain_temp as select * from int8_tbl i8');
+                                        explain_filter                                         
+-----------------------------------------------------------------------------------------------
+ Seq Scan on int8_tbl i8  (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 ms  output=NkB  format=text
+ Execution Time: N.N ms
+(4 rows)
+
index 2274dc1b5a662d91b6788e873a441037a5d71453..c7055f850c557b9d264e3b4e788845b24fa882d0 100644 (file)
@@ -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,10 @@ 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 SERIALIZE option
+select explain_filter('explain (analyze,serialize) select * from int8_tbl i8');
+select explain_filter('explain (analyze,serialize text,buffers,timing off) select * from int8_tbl i8');
+select explain_filter('explain (analyze,serialize binary,buffers,timing) select * from int8_tbl i8');
+-- this tests an edge case where we have no data to return
+select explain_filter('explain (analyze,serialize) create temp table explain_temp as select * from int8_tbl i8');
index fa1ede5fe7a7ac73c1bf0c457a2eac84d0817386..8d08386d65c3ec446148f7ccfc61f779524b8e78 100644 (file)
@@ -713,6 +713,7 @@ ExplainForeignModify_function
 ExplainForeignScan_function
 ExplainFormat
 ExplainOneQuery_hook_type
+ExplainSerializeOption
 ExplainState
 ExplainStmt
 ExplainWorkersState
@@ -2536,6 +2537,8 @@ SerCommitSeqNo
 SerialControl
 SerialIOData
 SerializableXactHandle
+SerializeDestReceiver
+SerializeMetrics
 SerializedActiveRelMaps
 SerializedClientConnectionInfo
 SerializedRanges