From 257882eb9d6ad6d2c12d0c5be19d972a7f4f6618 Mon Sep 17 00:00:00 2001 From: Matthias van de Meent Date: Mon, 26 Feb 2024 20:17:40 +0100 Subject: [PATCH v9 1/2] Explain: Add SERIALIZE option This option integrates with both MEMORY and 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 , heavily modified by Matthias van de Meent --- src/backend/commands/explain.c | 511 +++++++++++++++++++++++++- src/bin/psql/tab-complete.c | 4 +- src/include/commands/explain.h | 9 + src/test/regress/expected/explain.out | 57 ++- src/test/regress/sql/explain.sql | 28 +- 5 files changed, 602 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index a9d5056af4..9b1f4b6ba1 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,15 @@ 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 */ + MemoryContextCounters memory; /* memory context counters */ + MemoryContextCounters emptyMemory; /* memory context counters */ + ExplainSerializeFormat format; /* serialization format */ +} ExplSerInstrumentation; /* OR-able flags for ExplainXMLTag() */ #define X_OPENING 0 @@ -59,6 +70,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); @@ -155,7 +168,8 @@ static void ExplainJSONLineEnding(ExplainState *es); static void ExplainYAMLLineStarting(ExplainState *es); static void escape_yaml(StringInfo buf, const char *str); - +static DestReceiver *CreateExplainSerializeDestReceiver(ExplainState *es); +static ExplSerInstrumentation GetSerializationMetrics(DestReceiver *dest); /* * ExplainQuery - @@ -193,6 +207,34 @@ 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) + 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; @@ -247,6 +289,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, @@ -577,6 +625,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); @@ -605,11 +654,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; @@ -648,6 +701,13 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, /* run cleanup too */ ExecutorFinish(queryDesc); + /* grab the metrics before we destroy the DestReceiver */ + if (es->serialize) + 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); } @@ -729,6 +789,10 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, ExplainPropertyFloat("Execution Time", "ms", 1000.0 * totaltime, 3, es); + /* print the info about serialization of data */ + if (es->summary && es->analyze && es->serialize != EXPLAIN_SERIALIZE_NONE) + ExplainPrintSerialize(es, &serializeMetrics); + ExplainCloseGroup("Query", NULL, true, es); } @@ -5193,3 +5257,444 @@ 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; + +typedef struct SerializeDestReceiver +{ + /* receiver for the tuples, that just serializes */ + DestReceiver destRecevier; + MemoryContext memoryContext; + ExplainState *es; /* this EXPLAIN-statement's ExplainState */ + int8 format; /* text of binary, like pq wire protocol */ + TupleDesc attrinfo; + int nattrs; + StringInfoData buf; /* serialization buffer to hold temporary data */ + ExplSerInstrumentation metrics; /* metrics */ + SerializeAttrInfo *infos; /* Cached info about each attr */ +} 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->infos) + pfree(receiver->infos); + receiver->infos = NULL; + + receiver->attrinfo = typeinfo; + receiver->nattrs = nattrs; + if (nattrs <= 0) + return; + + receiver->infos = (SerializeAttrInfo *) + palloc0(nattrs * sizeof(SerializeAttrInfo)); + + for (int i = 0; i < nattrs; i++) + { + SerializeAttrInfo *info = &receiver->infos[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; + + tupdesc = slot->tts_tupleDescriptor; + + /* only measure time if requested */ + if (receiver->es->timing) + INSTR_TIME_SET_CURRENT(start); + + /* Cache attribute infos and function oid if outdated */ + if (receiver->attrinfo != 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->infos + i; + Datum attr = slot->tts_values[i]; + + if (slot->tts_isnull[i]) + { + pq_sendint32(buf, -1); + continue; + } + + /* + * Here we catch undefined bytes in datums that are returned to the + * client without hitting disk; see comments at the related check in + * PageAddItem(). This test is most useful for uncompressed, + * non-external datums, but we're quite likely to see such here when + * testing new C functions. + */ + if (thisState->typisvarlena) + VALGRIND_CHECK_MEM_IS_DEFINED(DatumGetPointer(attr), + VARSIZE_ANY(attr)); + + 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); + } + + /* + * 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; + + /* + * Now that we're done processing we profile memory usage, if that was + * requested by the user. + */ + if (receiver->es->memory) + { + MemoryContextCounters counters; + MemoryContextMemConsumed(receiver->memoryContext, &counters); + + /* + * Note: Although the freespace counter can (and likely does!) + * underflow, that won't be an issue for the printed results: this + * will only add used memory if more total space was allocated for + * the context due to excessive allocations, but will always increase + * the difference between the total totalspace and freespace by the + * amount of bytes allocated each iteration by underflowing the + * freespace counter. As memory used = totalspace - freespace, a + * negative value for freespace also adds to the used counter, even + * if it may be meaningless (and even nonsense!) on its own. + * + * However, it was decided to do it this way, to not overwhelm the + * user with stats of at least 8kiB of Memory Allocated per output + * tuple when that memory was actually retained in the Memory Context. + */ + receiver->metrics.memory.totalspace += + counters.totalspace - receiver->metrics.emptyMemory.totalspace; + receiver->metrics.memory.freespace += + counters.freespace - receiver->metrics.emptyMemory.freespace; + receiver->metrics.memory.freechunks += + counters.freechunks - receiver->metrics.emptyMemory.freechunks; + receiver->metrics.memory.nblocks += + counters.nblocks - receiver->metrics.emptyMemory.nblocks; + } + + /* cleanup and reset */ + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(receiver->memoryContext); + + return true; +} + +static void +serializeAnalyzeStartup(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SerializeDestReceiver *receiver = (SerializeDestReceiver*) self; + /* 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); + + /* + * We ensure our memory accounting is accurate by subtracting the memory + * usage of the empty memory context from measurements, so that we don't + * count these allocations every time we receive a tuple: we don't + * re-allocate the memory context every iteration; we only reset it. + */ + if (receiver->es->memory) + { + MemoryContextMemConsumed(receiver->memoryContext, + &receiver->metrics.emptyMemory); + /* + * ... But do count this memory context's empty allocations once at + * the start, so that using a memory context with lower base overhead + * shows up in these metrics. + */ + receiver->metrics.memory = receiver->metrics.emptyMemory; + } + + /* + * Note that we don't actually serialize the RowDescriptor message here. + * It is assumed that this has negligible overhead in the grand scheme of + * things; but if so desired it can be updated without much issue. + */ + + /* account for the attribute headers for send bytes */ + receiver->metrics.bytesSent += 3; /* protocol message type and attribute-count */ + for (int i = 0; i < typeinfo->natts; ++i) + { + Form_pg_attribute att = TupleDescAttr(typeinfo, i); + char *name = NameStr(att->attname); + Size namelen = strlen(name); + + /* convert from server encoding to client encoding if needed */ + char *converted = pg_server_to_client(name, (int) namelen); + + if (converted != name) + { + namelen = strlen(converted); + pfree(converted); /* don't leak it */ + } + + /* see printtup.h why we add 18 bytes here. These are the infos + * needed for each attribute plus the attribute's name */ + receiver->metrics.bytesSent += (int64) namelen + 1 + 18; + } +} + +/* + * serializeAnalyzeShutdown - shut down the serializeAnalyze receiver + */ +static void +serializeAnalyzeShutdown(DestReceiver *self) +{ + SerializeDestReceiver *receiver = (SerializeDestReceiver*) self; + + if (receiver->infos) + pfree(receiver->infos); + receiver->infos = 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. */ +static DestReceiver * +CreateExplainSerializeDestReceiver(ExplainState *es) +{ + SerializeDestReceiver *self; + + self = (SerializeDestReceiver*) palloc0(sizeof(SerializeDestReceiver)); + + self->destRecevier.receiveSlot = serializeAnalyzeReceive; + self->destRecevier.rStartup = serializeAnalyzeStartup; + self->destRecevier.rShutdown = serializeAnalyzeShutdown; + self->destRecevier.rDestroy = serializeAnalyzeDestroy; + self->destRecevier.mydest = DestNone; + + switch (es->serialize) + { + case EXPLAIN_SERIALIZE_NONE: + Assert(false); + elog(ERROR, "Invalid explain serialization format code %d", es->serialize); + break; + case EXPLAIN_SERIALIZE_TEXT: + self->format = 0; /* wire protocol format text */ + break; + case EXPLAIN_SERIALIZE_BINARY: + self->format = 1; /* wire protocol format binary */ + break; + } + + /* store the ExplainState, for easier access to various fields */ + self->es = es; + + self->metrics.format = es->serialize; + + return (DestReceiver *) self; +} + +static ExplSerInstrumentation +GetSerializationMetrics(DestReceiver *dest) +{ + return ((SerializeDestReceiver*) dest)->metrics; +} + +/* Print data for the SERIALIZE option */ +static void +ExplainPrintSerialize(ExplainState *es, ExplSerInstrumentation *instr) +{ + char *format; + if (instr->format == EXPLAIN_SERIALIZE_TEXT) + format = "text"; + else + { + /* We shouldn't get called for EXPLAIN_SERIALIZE_NONE */ + Assert(instr->format == EXPLAIN_SERIALIZE_BINARY); + format = "binary"; + } + + ExplainOpenGroup("Serialization", "Serialization", true, es); + + if (es->format == EXPLAIN_FORMAT_TEXT) + { + ExplainIndentText(es); + appendStringInfoString(es->str, "Serialization:"); + appendStringInfoChar(es->str, '\n'); + es->indent++; + ExplainIndentText(es); + + /* timing is optional */ + if (es->timing) + appendStringInfo(es->str, "Serialize: time=%.3f ms produced=%lld bytes format=%s", + 1000.0 * INSTR_TIME_GET_DOUBLE(instr->timeSpent), + (long long) instr->bytesSent, + format); + else + appendStringInfo(es->str, "Serialize: produced=%lld bytes format=%s", + (long long) instr->bytesSent, + format); + + appendStringInfoChar(es->str, '\n'); + + /* output memory stats, if applicable */ + if (es->memory) + show_memory_counters(es, &instr->memory); + es->indent--; + } + else + { + if (es->timing) + { + ExplainPropertyFloat("Time", "ms", + 1000.0 * INSTR_TIME_GET_DOUBLE(instr->timeSpent), + 3, es); + } + + ExplainPropertyUInteger("Produced", "bytes", + instr->bytesSent, es); + ExplainPropertyText("Format", format, es); + + if (es->memory) + show_memory_counters(es, &instr->memory); + } + + ExplainCloseGroup("Serialization", "Serialization", true, es); +} diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 73133ce735..822d65b71e 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -3849,9 +3849,11 @@ psql_completion(const char *text, int start, int end) */ if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) COMPLETE_WITH("ANALYZE", "VERBOSE", "COSTS", "SETTINGS", "GENERIC_PLAN", - "BUFFERS", "WAL", "TIMING", "SUMMARY", "FORMAT"); + "BUFFERS", "SERIALIZE", "WAL", "TIMING", "SUMMARY", "FORMAT"); else if (TailMatches("ANALYZE|VERBOSE|COSTS|SETTINGS|GENERIC_PLAN|BUFFERS|WAL|TIMING|SUMMARY")) COMPLETE_WITH("ON", "OFF"); + else if (TailMatches("SERIALIZE")) + COMPLETE_WITH("NONE", "TEXT", "BINARY"); else if (TailMatches("FORMAT")) COMPLETE_WITH("TEXT", "XML", "JSON", "YAML"); } diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index cf195f1359..b4bd6a2fcf 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 */ diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out index 1299ee79ad..ab39c75606 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 (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,7 +175,11 @@ select explain_filter('explain (analyze, buffers, format yaml) select * from int Temp Written Blocks: N + Planning Time: N.N + Triggers: + - Execution Time: N.N + Execution Time: N.N + + Serialization: + + Time: N.N + + Produced: N + + Format: "text" (1 row) select explain_filter('explain (buffers, format text) select * from int8_tbl i8'); @@ -639,3 +643,52 @@ 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 + Execution Time: N.N ms + Serialization: + Serialize: time=N.N ms produced=N bytes format=text +(5 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, memory) 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) + Memory: used=N bytes allocated=N bytes + Planning Time: N.N ms + Execution Time: N.N ms + Serialization: + Serialize: time=N.N ms produced=N bytes format=binary + Memory: used=N bytes allocated=N bytes +(7 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..02604b0b14 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, memory) 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; -- 2.40.1