summaryrefslogtreecommitdiffstats
path: root/src/backend/commands/explain.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 13:44:03 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 13:44:03 +0000
commit293913568e6a7a86fd1479e1cff8e2ecb58d6568 (patch)
treefc3b469a3ec5ab71b36ea97cc7aaddb838423a0c /src/backend/commands/explain.c
parentInitial commit. (diff)
downloadpostgresql-16-293913568e6a7a86fd1479e1cff8e2ecb58d6568.tar.xz
postgresql-16-293913568e6a7a86fd1479e1cff8e2ecb58d6568.zip
Adding upstream version 16.2.upstream/16.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/backend/commands/explain.c')
-rw-r--r--src/backend/commands/explain.c5054
1 files changed, 5054 insertions, 0 deletions
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
new file mode 100644
index 0000000..6c2e5c8
--- /dev/null
+++ b/src/backend/commands/explain.c
@@ -0,0 +1,5054 @@
+/*-------------------------------------------------------------------------
+ *
+ * explain.c
+ * Explain query execution plans
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/backend/commands/explain.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "catalog/pg_type.h"
+#include "commands/createas.h"
+#include "commands/defrem.h"
+#include "commands/prepare.h"
+#include "executor/nodeHash.h"
+#include "foreign/fdwapi.h"
+#include "jit/jit.h"
+#include "nodes/extensible.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/analyze.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "storage/bufmgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/guc_tables.h"
+#include "utils/json.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/ruleutils.h"
+#include "utils/snapmgr.h"
+#include "utils/tuplesort.h"
+#include "utils/typcache.h"
+#include "utils/xml.h"
+
+
+/* Hook for plugins to get control in ExplainOneQuery() */
+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;
+
+
+/* OR-able flags for ExplainXMLTag() */
+#define X_OPENING 0
+#define X_CLOSING 1
+#define X_CLOSE_IMMEDIATE 2
+#define X_NOWHITESPACE 4
+
+static void ExplainOneQuery(Query *query, int cursorOptions,
+ IntoClause *into, ExplainState *es,
+ const char *queryString, ParamListInfo params,
+ QueryEnvironment *queryEnv);
+static void ExplainPrintJIT(ExplainState *es, int jit_flags,
+ JitInstrumentation *ji);
+static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
+ ExplainState *es);
+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);
+static void show_plan_tlist(PlanState *planstate, List *ancestors,
+ ExplainState *es);
+static void show_expression(Node *node, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ bool useprefix, ExplainState *es);
+static void show_qual(List *qual, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ bool useprefix, ExplainState *es);
+static void show_scan_qual(List *qual, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ ExplainState *es);
+static void show_upper_qual(List *qual, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ ExplainState *es);
+static void show_sort_keys(SortState *sortstate, List *ancestors,
+ ExplainState *es);
+static void show_incremental_sort_keys(IncrementalSortState *incrsortstate,
+ List *ancestors, ExplainState *es);
+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,
+ List *ancestors, ExplainState *es);
+static void show_grouping_set_keys(PlanState *planstate,
+ Agg *aggnode, Sort *sortnode,
+ List *context, bool useprefix,
+ List *ancestors, ExplainState *es);
+static void show_group_keys(GroupState *gstate, List *ancestors,
+ ExplainState *es);
+static void show_sort_group_keys(PlanState *planstate, const char *qlabel,
+ int nkeys, int nPresortedKeys, AttrNumber *keycols,
+ Oid *sortOperators, Oid *collations, bool *nullsFirst,
+ List *ancestors, ExplainState *es);
+static void show_sortorder_options(StringInfo buf, Node *sortexpr,
+ Oid sortOperator, Oid collation, bool nullsFirst);
+static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
+ List *ancestors, ExplainState *es);
+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_memoize_info(MemoizeState *mstate, List *ancestors,
+ ExplainState *es);
+static void show_hashagg_info(AggState *aggstate, ExplainState *es);
+static void show_tidbitmap_info(BitmapHeapScanState *planstate,
+ ExplainState *es);
+static void show_instrumentation_count(const char *qlabel, int which,
+ PlanState *planstate, ExplainState *es);
+static void show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es);
+static void show_eval_params(Bitmapset *bms_params, ExplainState *es);
+static const char *explain_get_index_name(Oid indexId);
+static void show_buffer_usage(ExplainState *es, const BufferUsage *usage,
+ bool planning);
+static void show_wal_usage(ExplainState *es, const WalUsage *usage);
+static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
+ ExplainState *es);
+static void ExplainScanTarget(Scan *plan, ExplainState *es);
+static void ExplainModifyTarget(ModifyTable *plan, ExplainState *es);
+static void ExplainTargetRel(Plan *plan, Index rti, ExplainState *es);
+static void show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
+ ExplainState *es);
+static void ExplainMemberNodes(PlanState **planstates, int nplans,
+ List *ancestors, ExplainState *es);
+static void ExplainMissingMembers(int nplans, int nchildren, ExplainState *es);
+static void ExplainSubPlans(List *plans, List *ancestors,
+ const char *relationship, ExplainState *es);
+static void ExplainCustomChildren(CustomScanState *css,
+ List *ancestors, ExplainState *es);
+static ExplainWorkersState *ExplainCreateWorkersState(int num_workers);
+static void ExplainOpenWorker(int n, ExplainState *es);
+static void ExplainCloseWorker(int n, ExplainState *es);
+static void ExplainFlushWorkersState(ExplainState *es);
+static void ExplainProperty(const char *qlabel, const char *unit,
+ const char *value, bool numeric, ExplainState *es);
+static void ExplainOpenSetAsideGroup(const char *objtype, const char *labelname,
+ bool labeled, int depth, ExplainState *es);
+static void ExplainSaveGroup(ExplainState *es, int depth, int *state_save);
+static void ExplainRestoreGroup(ExplainState *es, int depth, int *state_save);
+static void ExplainDummyGroup(const char *objtype, const char *labelname,
+ ExplainState *es);
+static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es);
+static void ExplainIndentText(ExplainState *es);
+static void ExplainJSONLineEnding(ExplainState *es);
+static void ExplainYAMLLineStarting(ExplainState *es);
+static void escape_yaml(StringInfo buf, const char *str);
+
+
+
+/*
+ * ExplainQuery -
+ * execute an EXPLAIN command
+ */
+void
+ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
+ ParamListInfo params, DestReceiver *dest)
+{
+ ExplainState *es = NewExplainState();
+ TupOutputState *tstate;
+ JumbleState *jstate = NULL;
+ Query *query;
+ List *rewritten;
+ ListCell *lc;
+ bool timing_set = false;
+ bool summary_set = false;
+
+ /* Parse options list. */
+ foreach(lc, stmt->options)
+ {
+ DefElem *opt = (DefElem *) lfirst(lc);
+
+ if (strcmp(opt->defname, "analyze") == 0)
+ es->analyze = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "verbose") == 0)
+ es->verbose = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "costs") == 0)
+ es->costs = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "buffers") == 0)
+ es->buffers = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "wal") == 0)
+ es->wal = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "settings") == 0)
+ es->settings = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "generic_plan") == 0)
+ es->generic = defGetBoolean(opt);
+ else if (strcmp(opt->defname, "timing") == 0)
+ {
+ timing_set = true;
+ es->timing = defGetBoolean(opt);
+ }
+ else if (strcmp(opt->defname, "summary") == 0)
+ {
+ summary_set = true;
+ es->summary = defGetBoolean(opt);
+ }
+ else if (strcmp(opt->defname, "format") == 0)
+ {
+ char *p = defGetString(opt);
+
+ if (strcmp(p, "text") == 0)
+ es->format = EXPLAIN_FORMAT_TEXT;
+ else if (strcmp(p, "xml") == 0)
+ es->format = EXPLAIN_FORMAT_XML;
+ else if (strcmp(p, "json") == 0)
+ es->format = EXPLAIN_FORMAT_JSON;
+ else if (strcmp(p, "yaml") == 0)
+ es->format = EXPLAIN_FORMAT_YAML;
+ 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
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("unrecognized EXPLAIN option \"%s\"",
+ opt->defname),
+ parser_errposition(pstate, opt->location)));
+ }
+
+ /* check that WAL is used with EXPLAIN ANALYZE */
+ if (es->wal && !es->analyze)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("EXPLAIN option WAL requires ANALYZE")));
+
+ /* if the timing was not set explicitly, set default value */
+ es->timing = (timing_set) ? es->timing : es->analyze;
+
+ /* check that timing is used with EXPLAIN ANALYZE */
+ if (es->timing && !es->analyze)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("EXPLAIN option TIMING requires ANALYZE")));
+
+ /* check that GENERIC_PLAN is not used with EXPLAIN ANALYZE */
+ if (es->generic && es->analyze)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together")));
+
+ /* if the summary was not set explicitly, set default value */
+ es->summary = (summary_set) ? es->summary : es->analyze;
+
+ query = castNode(Query, stmt->query);
+ if (IsQueryIdEnabled())
+ jstate = JumbleQuery(query);
+
+ if (post_parse_analyze_hook)
+ (*post_parse_analyze_hook) (pstate, query, jstate);
+
+ /*
+ * Parse analysis was done already, but we still have to run the rule
+ * rewriter. We do not do AcquireRewriteLocks: we assume the query either
+ * came straight from the parser, or suitable locks were acquired by
+ * plancache.c.
+ */
+ rewritten = QueryRewrite(castNode(Query, stmt->query));
+
+ /* emit opening boilerplate */
+ ExplainBeginOutput(es);
+
+ if (rewritten == NIL)
+ {
+ /*
+ * In the case of an INSTEAD NOTHING, tell at least that. But in
+ * non-text format, the output is delimited, so this isn't necessary.
+ */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoString(es->str, "Query rewrites to nothing\n");
+ }
+ else
+ {
+ ListCell *l;
+
+ /* Explain every plan */
+ foreach(l, rewritten)
+ {
+ ExplainOneQuery(lfirst_node(Query, l),
+ CURSOR_OPT_PARALLEL_OK, NULL, es,
+ pstate->p_sourcetext, params, pstate->p_queryEnv);
+
+ /* Separate plans with an appropriate separator */
+ if (lnext(rewritten, l) != NULL)
+ ExplainSeparatePlans(es);
+ }
+ }
+
+ /* emit closing boilerplate */
+ ExplainEndOutput(es);
+ Assert(es->indent == 0);
+
+ /* output tuples */
+ tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt),
+ &TTSOpsVirtual);
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ do_text_output_multiline(tstate, es->str->data);
+ else
+ do_text_output_oneline(tstate, es->str->data);
+ end_tup_output(tstate);
+
+ pfree(es->str->data);
+}
+
+/*
+ * Create a new ExplainState struct initialized with default options.
+ */
+ExplainState *
+NewExplainState(void)
+{
+ ExplainState *es = (ExplainState *) palloc0(sizeof(ExplainState));
+
+ /* Set default options (most fields can be left as zeroes). */
+ es->costs = true;
+ /* Prepare output buffer. */
+ es->str = makeStringInfo();
+
+ return es;
+}
+
+/*
+ * ExplainResultDesc -
+ * construct the result tupledesc for an EXPLAIN
+ */
+TupleDesc
+ExplainResultDesc(ExplainStmt *stmt)
+{
+ TupleDesc tupdesc;
+ ListCell *lc;
+ Oid result_type = TEXTOID;
+
+ /* Check for XML format option */
+ foreach(lc, stmt->options)
+ {
+ DefElem *opt = (DefElem *) lfirst(lc);
+
+ if (strcmp(opt->defname, "format") == 0)
+ {
+ char *p = defGetString(opt);
+
+ if (strcmp(p, "xml") == 0)
+ result_type = XMLOID;
+ else if (strcmp(p, "json") == 0)
+ result_type = JSONOID;
+ else
+ result_type = TEXTOID;
+ /* don't "break", as ExplainQuery will use the last value */
+ }
+ }
+
+ /* Need a tuple descriptor representing a single TEXT or XML column */
+ tupdesc = CreateTemplateTupleDesc(1);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
+ result_type, -1, 0);
+ return tupdesc;
+}
+
+/*
+ * ExplainOneQuery -
+ * print out the execution plan for one Query
+ *
+ * "into" is NULL unless we are explaining the contents of a CreateTableAsStmt.
+ */
+static void
+ExplainOneQuery(Query *query, int cursorOptions,
+ IntoClause *into, ExplainState *es,
+ const char *queryString, ParamListInfo params,
+ QueryEnvironment *queryEnv)
+{
+ /* planner will not cope with utility statements */
+ if (query->commandType == CMD_UTILITY)
+ {
+ ExplainOneUtility(query->utilityStmt, into, es, queryString, params,
+ queryEnv);
+ return;
+ }
+
+ /* if an advisor plugin is present, let it manage things */
+ if (ExplainOneQuery_hook)
+ (*ExplainOneQuery_hook) (query, cursorOptions, into, es,
+ queryString, params, queryEnv);
+ else
+ {
+ PlannedStmt *plan;
+ instr_time planstart,
+ planduration;
+ BufferUsage bufusage_start,
+ bufusage;
+
+ if (es->buffers)
+ bufusage_start = pgBufferUsage;
+ INSTR_TIME_SET_CURRENT(planstart);
+
+ /* plan the query */
+ plan = pg_plan_query(query, queryString, cursorOptions, params);
+
+ INSTR_TIME_SET_CURRENT(planduration);
+ INSTR_TIME_SUBTRACT(planduration, planstart);
+
+ /* calc differences of buffer counters. */
+ if (es->buffers)
+ {
+ memset(&bufusage, 0, sizeof(BufferUsage));
+ BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &bufusage_start);
+ }
+
+ /* run it (if needed) and produce output */
+ ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+ &planduration, (es->buffers ? &bufusage : NULL));
+ }
+}
+
+/*
+ * ExplainOneUtility -
+ * print out the execution plan for one utility statement
+ * (In general, utility statements don't have plans, but there are some
+ * we treat as special cases)
+ *
+ * "into" is NULL unless we are explaining the contents of a CreateTableAsStmt.
+ *
+ * This is exported because it's called back from prepare.c in the
+ * EXPLAIN EXECUTE case. In that case, we'll be dealing with a statement
+ * that's in the plan cache, so we have to ensure we don't modify it.
+ */
+void
+ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es,
+ const char *queryString, ParamListInfo params,
+ QueryEnvironment *queryEnv)
+{
+ if (utilityStmt == NULL)
+ return;
+
+ if (IsA(utilityStmt, CreateTableAsStmt))
+ {
+ /*
+ * We have to rewrite the contained SELECT and then pass it back to
+ * ExplainOneQuery. Copy to be safe in the EXPLAIN EXECUTE case.
+ */
+ CreateTableAsStmt *ctas = (CreateTableAsStmt *) utilityStmt;
+ List *rewritten;
+
+ /*
+ * Check if the relation exists or not. This is done at this stage to
+ * avoid query planning or execution.
+ */
+ if (CreateTableAsRelExists(ctas))
+ {
+ if (ctas->objtype == OBJECT_TABLE)
+ ExplainDummyGroup("CREATE TABLE AS", NULL, es);
+ else if (ctas->objtype == OBJECT_MATVIEW)
+ ExplainDummyGroup("CREATE MATERIALIZED VIEW", NULL, es);
+ else
+ elog(ERROR, "unexpected object type: %d",
+ (int) ctas->objtype);
+ return;
+ }
+
+ rewritten = QueryRewrite(castNode(Query, copyObject(ctas->query)));
+ Assert(list_length(rewritten) == 1);
+ ExplainOneQuery(linitial_node(Query, rewritten),
+ CURSOR_OPT_PARALLEL_OK, ctas->into, es,
+ queryString, params, queryEnv);
+ }
+ else if (IsA(utilityStmt, DeclareCursorStmt))
+ {
+ /*
+ * Likewise for DECLARE CURSOR.
+ *
+ * Notice that if you say EXPLAIN ANALYZE DECLARE CURSOR then we'll
+ * actually run the query. This is different from pre-8.3 behavior
+ * but seems more useful than not running the query. No cursor will
+ * be created, however.
+ */
+ DeclareCursorStmt *dcs = (DeclareCursorStmt *) utilityStmt;
+ List *rewritten;
+
+ rewritten = QueryRewrite(castNode(Query, copyObject(dcs->query)));
+ Assert(list_length(rewritten) == 1);
+ ExplainOneQuery(linitial_node(Query, rewritten),
+ dcs->options, NULL, es,
+ queryString, params, queryEnv);
+ }
+ else if (IsA(utilityStmt, ExecuteStmt))
+ ExplainExecuteQuery((ExecuteStmt *) utilityStmt, into, es,
+ queryString, params, queryEnv);
+ else if (IsA(utilityStmt, NotifyStmt))
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoString(es->str, "NOTIFY\n");
+ else
+ ExplainDummyGroup("Notify", NULL, es);
+ }
+ else
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoString(es->str,
+ "Utility statements have no plan structure\n");
+ else
+ ExplainDummyGroup("Utility Statement", NULL, es);
+ }
+}
+
+/*
+ * ExplainOnePlan -
+ * given a planned query, execute it if needed, and then print
+ * EXPLAIN output
+ *
+ * "into" is NULL unless we are explaining the contents of a CreateTableAsStmt,
+ * in which case executing the query should result in creating that table.
+ *
+ * This is exported because it's called back from prepare.c in the
+ * EXPLAIN EXECUTE case, and because an index advisor plugin would need
+ * to call it.
+ */
+void
+ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es,
+ const char *queryString, ParamListInfo params,
+ QueryEnvironment *queryEnv, const instr_time *planduration,
+ const BufferUsage *bufusage)
+{
+ DestReceiver *dest;
+ QueryDesc *queryDesc;
+ instr_time starttime;
+ double totaltime = 0;
+ int eflags;
+ int instrument_option = 0;
+
+ Assert(plannedstmt->commandType != CMD_UTILITY);
+
+ if (es->analyze && es->timing)
+ instrument_option |= INSTRUMENT_TIMER;
+ else if (es->analyze)
+ instrument_option |= INSTRUMENT_ROWS;
+
+ if (es->buffers)
+ instrument_option |= INSTRUMENT_BUFFERS;
+ if (es->wal)
+ instrument_option |= INSTRUMENT_WAL;
+
+ /*
+ * We always collect timing for the entire statement, even when node-level
+ * timing is off, so we don't look at es->timing here. (We could skip
+ * this if !es->summary, but it's hardly worth the complication.)
+ */
+ INSTR_TIME_SET_CURRENT(starttime);
+
+ /*
+ * Use a snapshot with an updated command ID to ensure this query sees
+ * results of any previously executed queries.
+ */
+ PushCopiedSnapshot(GetActiveSnapshot());
+ UpdateActiveSnapshotCommandId();
+
+ /*
+ * Normally we discard the query's output, but if explaining CREATE TABLE
+ * AS, we'd better use the appropriate tuple receiver.
+ */
+ if (into)
+ dest = CreateIntoRelDestReceiver(into);
+ else
+ dest = None_Receiver;
+
+ /* Create a QueryDesc for the query */
+ queryDesc = CreateQueryDesc(plannedstmt, queryString,
+ GetActiveSnapshot(), InvalidSnapshot,
+ dest, params, queryEnv, instrument_option);
+
+ /* Select execution options */
+ if (es->analyze)
+ eflags = 0; /* default run-to-completion flags */
+ else
+ eflags = EXEC_FLAG_EXPLAIN_ONLY;
+ if (es->generic)
+ eflags |= EXEC_FLAG_EXPLAIN_GENERIC;
+ if (into)
+ eflags |= GetIntoRelEFlags(into);
+
+ /* call ExecutorStart to prepare the plan for execution */
+ ExecutorStart(queryDesc, eflags);
+
+ /* Execute the plan for statistics if asked for */
+ if (es->analyze)
+ {
+ ScanDirection dir;
+
+ /* EXPLAIN ANALYZE CREATE TABLE AS WITH NO DATA is weird */
+ if (into && into->skipData)
+ dir = NoMovementScanDirection;
+ else
+ dir = ForwardScanDirection;
+
+ /* run the plan */
+ ExecutorRun(queryDesc, dir, 0, true);
+
+ /* run cleanup too */
+ ExecutorFinish(queryDesc);
+
+ /* We can't run ExecutorEnd 'till we're done printing the stats... */
+ totaltime += elapsed_time(&starttime);
+ }
+
+ ExplainOpenGroup("Query", NULL, true, es);
+
+ /* Create textual dump of plan tree */
+ ExplainPrintPlan(es, queryDesc);
+
+ /* Show buffer usage in planning */
+ if (bufusage)
+ {
+ ExplainOpenGroup("Planning", "Planning", true, es);
+ show_buffer_usage(es, bufusage, true);
+ ExplainCloseGroup("Planning", "Planning", true, es);
+ }
+
+ if (es->summary && planduration)
+ {
+ double plantime = INSTR_TIME_GET_DOUBLE(*planduration);
+
+ ExplainPropertyFloat("Planning Time", "ms", 1000.0 * plantime, 3, es);
+ }
+
+ /* Print info about runtime of triggers */
+ if (es->analyze)
+ ExplainPrintTriggers(es, queryDesc);
+
+ /*
+ * Print info about JITing. Tied to es->costs because we don't want to
+ * display this in regression tests, as it'd cause output differences
+ * depending on build options. Might want to separate that out from COSTS
+ * at a later stage.
+ */
+ if (es->costs)
+ ExplainPrintJITSummary(es, queryDesc);
+
+ /*
+ * Close down the query and free resources. Include time for this in the
+ * total execution time (although it should be pretty minimal).
+ */
+ INSTR_TIME_SET_CURRENT(starttime);
+
+ ExecutorEnd(queryDesc);
+
+ FreeQueryDesc(queryDesc);
+
+ PopActiveSnapshot();
+
+ /* We need a CCI just in case query expanded to multiple plans */
+ if (es->analyze)
+ CommandCounterIncrement();
+
+ totaltime += elapsed_time(&starttime);
+
+ /*
+ * We only report execution time if we actually ran the query (that is,
+ * the user specified ANALYZE), and if summary reporting is enabled (the
+ * user can set SUMMARY OFF to not have the timing information included in
+ * the output). By default, ANALYZE sets SUMMARY to true.
+ */
+ if (es->summary && es->analyze)
+ ExplainPropertyFloat("Execution Time", "ms", 1000.0 * totaltime, 3,
+ es);
+
+ ExplainCloseGroup("Query", NULL, true, es);
+}
+
+/*
+ * ExplainPrintSettings -
+ * Print summary of modified settings affecting query planning.
+ */
+static void
+ExplainPrintSettings(ExplainState *es)
+{
+ int num;
+ struct config_generic **gucs;
+
+ /* bail out if information about settings not requested */
+ if (!es->settings)
+ return;
+
+ /* request an array of relevant settings */
+ gucs = get_explain_guc_options(&num);
+
+ if (es->format != EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainOpenGroup("Settings", "Settings", true, es);
+
+ for (int i = 0; i < num; i++)
+ {
+ char *setting;
+ struct config_generic *conf = gucs[i];
+
+ setting = GetConfigOptionByName(conf->name, NULL, true);
+
+ ExplainPropertyText(conf->name, setting, es);
+ }
+
+ ExplainCloseGroup("Settings", "Settings", true, es);
+ }
+ else
+ {
+ StringInfoData str;
+
+ /* In TEXT mode, print nothing if there are no options */
+ if (num <= 0)
+ return;
+
+ initStringInfo(&str);
+
+ for (int i = 0; i < num; i++)
+ {
+ char *setting;
+ struct config_generic *conf = gucs[i];
+
+ if (i > 0)
+ appendStringInfoString(&str, ", ");
+
+ setting = GetConfigOptionByName(conf->name, NULL, true);
+
+ if (setting)
+ appendStringInfo(&str, "%s = '%s'", conf->name, setting);
+ else
+ appendStringInfo(&str, "%s = NULL", conf->name);
+ }
+
+ ExplainPropertyText("Settings", str.data, es);
+ }
+}
+
+/*
+ * ExplainPrintPlan -
+ * convert a QueryDesc's plan tree to text and append it to es->str
+ *
+ * The caller should have set up the options fields of *es, as well as
+ * initializing the output buffer es->str. Also, output formatting state
+ * such as the indent level is assumed valid. Plan-tree-specific fields
+ * in *es are initialized here.
+ *
+ * NB: will not work on utility statements
+ */
+void
+ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
+{
+ Bitmapset *rels_used = NULL;
+ PlanState *ps;
+
+ /* Set up ExplainState fields associated with this plan tree */
+ Assert(queryDesc->plannedstmt != NULL);
+ es->pstmt = queryDesc->plannedstmt;
+ es->rtable = queryDesc->plannedstmt->rtable;
+ ExplainPreScanNode(queryDesc->planstate, &rels_used);
+ es->rtable_names = select_rtable_names_for_explain(es->rtable, rels_used);
+ es->deparse_cxt = deparse_context_for_plan_tree(queryDesc->plannedstmt,
+ es->rtable_names);
+ es->printed_subplans = NULL;
+
+ /*
+ * Sometimes we mark a Gather node as "invisible", which means that it's
+ * not to be displayed in EXPLAIN output. The purpose of this is to allow
+ * running regression tests with debug_parallel_query=regress to get the
+ * same results as running the same tests with debug_parallel_query=off.
+ * Such marking is currently only supported on a Gather at the top of the
+ * plan. We skip that node, and we must also hide per-worker detail data
+ * further down in the plan tree.
+ */
+ ps = queryDesc->planstate;
+ if (IsA(ps, GatherState) && ((Gather *) ps->plan)->invisible)
+ {
+ ps = outerPlanState(ps);
+ es->hide_workers = true;
+ }
+ ExplainNode(ps, NIL, NULL, NULL, es);
+
+ /*
+ * If requested, include information about GUC parameters with values that
+ * don't match the built-in defaults.
+ */
+ ExplainPrintSettings(es);
+
+ /*
+ * COMPUTE_QUERY_ID_REGRESS means COMPUTE_QUERY_ID_AUTO, but we don't show
+ * the queryid in any of the EXPLAIN plans to keep stable the results
+ * generated by regression test suites.
+ */
+ if (es->verbose && queryDesc->plannedstmt->queryId != UINT64CONST(0) &&
+ compute_query_id != COMPUTE_QUERY_ID_REGRESS)
+ {
+ /*
+ * Output the queryid as an int64 rather than a uint64 so we match
+ * what would be seen in the BIGINT pg_stat_statements.queryid column.
+ */
+ ExplainPropertyInteger("Query Identifier", NULL, (int64)
+ queryDesc->plannedstmt->queryId, es);
+ }
+}
+
+/*
+ * ExplainPrintTriggers -
+ * convert a QueryDesc's trigger statistics to text and append it to
+ * es->str
+ *
+ * The caller should have set up the options fields of *es, as well as
+ * initializing the output buffer es->str. Other fields in *es are
+ * initialized here.
+ */
+void
+ExplainPrintTriggers(ExplainState *es, QueryDesc *queryDesc)
+{
+ ResultRelInfo *rInfo;
+ bool show_relname;
+ List *resultrels;
+ List *routerels;
+ List *targrels;
+ ListCell *l;
+
+ resultrels = queryDesc->estate->es_opened_result_relations;
+ routerels = queryDesc->estate->es_tuple_routing_result_relations;
+ targrels = queryDesc->estate->es_trig_target_relations;
+
+ ExplainOpenGroup("Triggers", "Triggers", false, es);
+
+ show_relname = (list_length(resultrels) > 1 ||
+ routerels != NIL || targrels != NIL);
+ foreach(l, resultrels)
+ {
+ rInfo = (ResultRelInfo *) lfirst(l);
+ report_triggers(rInfo, show_relname, es);
+ }
+
+ foreach(l, routerels)
+ {
+ rInfo = (ResultRelInfo *) lfirst(l);
+ report_triggers(rInfo, show_relname, es);
+ }
+
+ foreach(l, targrels)
+ {
+ rInfo = (ResultRelInfo *) lfirst(l);
+ report_triggers(rInfo, show_relname, es);
+ }
+
+ ExplainCloseGroup("Triggers", "Triggers", false, es);
+}
+
+/*
+ * ExplainPrintJITSummary -
+ * Print summarized JIT instrumentation from leader and workers
+ */
+void
+ExplainPrintJITSummary(ExplainState *es, QueryDesc *queryDesc)
+{
+ JitInstrumentation ji = {0};
+
+ if (!(queryDesc->estate->es_jit_flags & PGJIT_PERFORM))
+ return;
+
+ /*
+ * Work with a copy instead of modifying the leader state, since this
+ * function may be called twice
+ */
+ if (queryDesc->estate->es_jit)
+ InstrJitAgg(&ji, &queryDesc->estate->es_jit->instr);
+
+ /* If this process has done JIT in parallel workers, merge stats */
+ if (queryDesc->estate->es_jit_worker_instr)
+ InstrJitAgg(&ji, queryDesc->estate->es_jit_worker_instr);
+
+ ExplainPrintJIT(es, queryDesc->estate->es_jit_flags, &ji);
+}
+
+/*
+ * ExplainPrintJIT -
+ * Append information about JITing to es->str.
+ */
+static void
+ExplainPrintJIT(ExplainState *es, int jit_flags, JitInstrumentation *ji)
+{
+ instr_time total_time;
+
+ /* don't print information if no JITing happened */
+ if (!ji || ji->created_functions == 0)
+ return;
+
+ /* calculate total time */
+ INSTR_TIME_SET_ZERO(total_time);
+ INSTR_TIME_ADD(total_time, ji->generation_counter);
+ INSTR_TIME_ADD(total_time, ji->inlining_counter);
+ INSTR_TIME_ADD(total_time, ji->optimization_counter);
+ INSTR_TIME_ADD(total_time, ji->emission_counter);
+
+ ExplainOpenGroup("JIT", "JIT", true, es);
+
+ /* for higher density, open code the text output format */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "JIT:\n");
+ es->indent++;
+
+ ExplainPropertyInteger("Functions", NULL, ji->created_functions, es);
+
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "Options: %s %s, %s %s, %s %s, %s %s\n",
+ "Inlining", jit_flags & PGJIT_INLINE ? "true" : "false",
+ "Optimization", jit_flags & PGJIT_OPT3 ? "true" : "false",
+ "Expressions", jit_flags & PGJIT_EXPR ? "true" : "false",
+ "Deforming", jit_flags & PGJIT_DEFORM ? "true" : "false");
+
+ if (es->analyze && es->timing)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str,
+ "Timing: %s %.3f ms, %s %.3f ms, %s %.3f ms, %s %.3f ms, %s %.3f ms\n",
+ "Generation", 1000.0 * INSTR_TIME_GET_DOUBLE(ji->generation_counter),
+ "Inlining", 1000.0 * INSTR_TIME_GET_DOUBLE(ji->inlining_counter),
+ "Optimization", 1000.0 * INSTR_TIME_GET_DOUBLE(ji->optimization_counter),
+ "Emission", 1000.0 * INSTR_TIME_GET_DOUBLE(ji->emission_counter),
+ "Total", 1000.0 * INSTR_TIME_GET_DOUBLE(total_time));
+ }
+
+ es->indent--;
+ }
+ else
+ {
+ ExplainPropertyInteger("Functions", NULL, ji->created_functions, es);
+
+ ExplainOpenGroup("Options", "Options", true, es);
+ ExplainPropertyBool("Inlining", jit_flags & PGJIT_INLINE, es);
+ ExplainPropertyBool("Optimization", jit_flags & PGJIT_OPT3, es);
+ ExplainPropertyBool("Expressions", jit_flags & PGJIT_EXPR, es);
+ ExplainPropertyBool("Deforming", jit_flags & PGJIT_DEFORM, es);
+ ExplainCloseGroup("Options", "Options", true, es);
+
+ if (es->analyze && es->timing)
+ {
+ ExplainOpenGroup("Timing", "Timing", true, es);
+
+ ExplainPropertyFloat("Generation", "ms",
+ 1000.0 * INSTR_TIME_GET_DOUBLE(ji->generation_counter),
+ 3, es);
+ ExplainPropertyFloat("Inlining", "ms",
+ 1000.0 * INSTR_TIME_GET_DOUBLE(ji->inlining_counter),
+ 3, es);
+ ExplainPropertyFloat("Optimization", "ms",
+ 1000.0 * INSTR_TIME_GET_DOUBLE(ji->optimization_counter),
+ 3, es);
+ ExplainPropertyFloat("Emission", "ms",
+ 1000.0 * INSTR_TIME_GET_DOUBLE(ji->emission_counter),
+ 3, es);
+ ExplainPropertyFloat("Total", "ms",
+ 1000.0 * INSTR_TIME_GET_DOUBLE(total_time),
+ 3, es);
+
+ ExplainCloseGroup("Timing", "Timing", true, es);
+ }
+ }
+
+ ExplainCloseGroup("JIT", "JIT", true, es);
+}
+
+/*
+ * ExplainQueryText -
+ * add a "Query Text" node that contains the actual text of the query
+ *
+ * The caller should have set up the options fields of *es, as well as
+ * initializing the output buffer es->str.
+ *
+ */
+void
+ExplainQueryText(ExplainState *es, QueryDesc *queryDesc)
+{
+ if (queryDesc->sourceText)
+ ExplainPropertyText("Query Text", queryDesc->sourceText, es);
+}
+
+/*
+ * ExplainQueryParameters -
+ * add a "Query Parameters" node that describes the parameters of the query
+ *
+ * The caller should have set up the options fields of *es, as well as
+ * initializing the output buffer es->str.
+ *
+ */
+void
+ExplainQueryParameters(ExplainState *es, ParamListInfo params, int maxlen)
+{
+ char *str;
+
+ /* This check is consistent with errdetail_params() */
+ if (params == NULL || params->numParams <= 0 || maxlen == 0)
+ return;
+
+ str = BuildParamLogString(params, NULL, maxlen);
+ if (str && str[0] != '\0')
+ ExplainPropertyText("Query Parameters", str, es);
+}
+
+/*
+ * report_triggers -
+ * report execution stats for a single relation's triggers
+ */
+static void
+report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es)
+{
+ int nt;
+
+ if (!rInfo->ri_TrigDesc || !rInfo->ri_TrigInstrument)
+ return;
+ for (nt = 0; nt < rInfo->ri_TrigDesc->numtriggers; nt++)
+ {
+ Trigger *trig = rInfo->ri_TrigDesc->triggers + nt;
+ Instrumentation *instr = rInfo->ri_TrigInstrument + nt;
+ char *relname;
+ char *conname = NULL;
+
+ /* Must clean up instrumentation state */
+ InstrEndLoop(instr);
+
+ /*
+ * We ignore triggers that were never invoked; they likely aren't
+ * relevant to the current query type.
+ */
+ if (instr->ntuples == 0)
+ continue;
+
+ ExplainOpenGroup("Trigger", NULL, true, es);
+
+ relname = RelationGetRelationName(rInfo->ri_RelationDesc);
+ if (OidIsValid(trig->tgconstraint))
+ conname = get_constraint_name(trig->tgconstraint);
+
+ /*
+ * In text format, we avoid printing both the trigger name and the
+ * constraint name unless VERBOSE is specified. In non-text formats
+ * we just print everything.
+ */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (es->verbose || conname == NULL)
+ appendStringInfo(es->str, "Trigger %s", trig->tgname);
+ else
+ appendStringInfoString(es->str, "Trigger");
+ if (conname)
+ appendStringInfo(es->str, " for constraint %s", conname);
+ if (show_relname)
+ appendStringInfo(es->str, " on %s", relname);
+ if (es->timing)
+ appendStringInfo(es->str, ": time=%.3f calls=%.0f\n",
+ 1000.0 * instr->total, instr->ntuples);
+ else
+ appendStringInfo(es->str, ": calls=%.0f\n", instr->ntuples);
+ }
+ else
+ {
+ ExplainPropertyText("Trigger Name", trig->tgname, es);
+ if (conname)
+ ExplainPropertyText("Constraint Name", conname, es);
+ ExplainPropertyText("Relation", relname, es);
+ if (es->timing)
+ ExplainPropertyFloat("Time", "ms", 1000.0 * instr->total, 3,
+ es);
+ ExplainPropertyFloat("Calls", NULL, instr->ntuples, 0, es);
+ }
+
+ if (conname)
+ pfree(conname);
+
+ ExplainCloseGroup("Trigger", NULL, true, es);
+ }
+}
+
+/* Compute elapsed time in seconds since given timestamp */
+static double
+elapsed_time(instr_time *starttime)
+{
+ instr_time endtime;
+
+ INSTR_TIME_SET_CURRENT(endtime);
+ INSTR_TIME_SUBTRACT(endtime, *starttime);
+ return INSTR_TIME_GET_DOUBLE(endtime);
+}
+
+/*
+ * ExplainPreScanNode -
+ * Prescan the planstate tree to identify which RTEs are referenced
+ *
+ * Adds the relid of each referenced RTE to *rels_used. The result controls
+ * which RTEs are assigned aliases by select_rtable_names_for_explain.
+ * This ensures that we don't confusingly assign un-suffixed aliases to RTEs
+ * that never appear in the EXPLAIN output (such as inheritance parents).
+ */
+static bool
+ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
+{
+ Plan *plan = planstate->plan;
+
+ switch (nodeTag(plan))
+ {
+ case T_SeqScan:
+ case T_SampleScan:
+ case T_IndexScan:
+ case T_IndexOnlyScan:
+ case T_BitmapHeapScan:
+ case T_TidScan:
+ case T_TidRangeScan:
+ case T_SubqueryScan:
+ case T_FunctionScan:
+ case T_TableFuncScan:
+ case T_ValuesScan:
+ case T_CteScan:
+ case T_NamedTuplestoreScan:
+ case T_WorkTableScan:
+ *rels_used = bms_add_member(*rels_used,
+ ((Scan *) plan)->scanrelid);
+ break;
+ case T_ForeignScan:
+ *rels_used = bms_add_members(*rels_used,
+ ((ForeignScan *) plan)->fs_base_relids);
+ break;
+ case T_CustomScan:
+ *rels_used = bms_add_members(*rels_used,
+ ((CustomScan *) plan)->custom_relids);
+ break;
+ case T_ModifyTable:
+ *rels_used = bms_add_member(*rels_used,
+ ((ModifyTable *) plan)->nominalRelation);
+ if (((ModifyTable *) plan)->exclRelRTI)
+ *rels_used = bms_add_member(*rels_used,
+ ((ModifyTable *) plan)->exclRelRTI);
+ break;
+ case T_Append:
+ *rels_used = bms_add_members(*rels_used,
+ ((Append *) plan)->apprelids);
+ break;
+ case T_MergeAppend:
+ *rels_used = bms_add_members(*rels_used,
+ ((MergeAppend *) plan)->apprelids);
+ break;
+ default:
+ break;
+ }
+
+ return planstate_tree_walker(planstate, ExplainPreScanNode, rels_used);
+}
+
+/*
+ * ExplainNode -
+ * Appends a description of a plan tree to es->str
+ *
+ * planstate points to the executor state node for the current plan node.
+ * We need to work from a PlanState node, not just a Plan node, in order to
+ * get at the instrumentation data (if any) as well as the list of subplans.
+ *
+ * ancestors is a list of parent Plan and SubPlan nodes, most-closely-nested
+ * first. These are needed in order to interpret PARAM_EXEC Params.
+ *
+ * relationship describes the relationship of this plan node to its parent
+ * (eg, "Outer", "Inner"); it can be null at top level. plan_name is an
+ * optional name to be attached to the node.
+ *
+ * In text format, es->indent is controlled in this function since we only
+ * want it to change at plan-node boundaries (but a few subroutines will
+ * transiently increment it). In non-text formats, es->indent corresponds
+ * to the nesting depth of logical output groups, and therefore is controlled
+ * by ExplainOpenGroup/ExplainCloseGroup.
+ */
+static void
+ExplainNode(PlanState *planstate, List *ancestors,
+ const char *relationship, const char *plan_name,
+ ExplainState *es)
+{
+ Plan *plan = planstate->plan;
+ const char *pname; /* node type name for text output */
+ const char *sname; /* node type name for non-text output */
+ const char *strategy = NULL;
+ const char *partialmode = NULL;
+ const char *operation = NULL;
+ const char *custom_name = NULL;
+ ExplainWorkersState *save_workers_state = es->workers_state;
+ int save_indent = es->indent;
+ bool haschildren;
+
+ /*
+ * Prepare per-worker output buffers, if needed. We'll append the data in
+ * these to the main output string further down.
+ */
+ if (planstate->worker_instrument && es->analyze && !es->hide_workers)
+ es->workers_state = ExplainCreateWorkersState(planstate->worker_instrument->num_workers);
+ else
+ es->workers_state = NULL;
+
+ /* Identify plan node type, and print generic details */
+ switch (nodeTag(plan))
+ {
+ case T_Result:
+ pname = sname = "Result";
+ break;
+ case T_ProjectSet:
+ pname = sname = "ProjectSet";
+ break;
+ case T_ModifyTable:
+ sname = "ModifyTable";
+ switch (((ModifyTable *) plan)->operation)
+ {
+ case CMD_INSERT:
+ pname = operation = "Insert";
+ break;
+ case CMD_UPDATE:
+ pname = operation = "Update";
+ break;
+ case CMD_DELETE:
+ pname = operation = "Delete";
+ break;
+ case CMD_MERGE:
+ pname = operation = "Merge";
+ break;
+ default:
+ pname = "???";
+ break;
+ }
+ break;
+ case T_Append:
+ pname = sname = "Append";
+ break;
+ case T_MergeAppend:
+ pname = sname = "Merge Append";
+ break;
+ case T_RecursiveUnion:
+ pname = sname = "Recursive Union";
+ break;
+ case T_BitmapAnd:
+ pname = sname = "BitmapAnd";
+ break;
+ case T_BitmapOr:
+ pname = sname = "BitmapOr";
+ break;
+ case T_NestLoop:
+ pname = sname = "Nested Loop";
+ break;
+ case T_MergeJoin:
+ pname = "Merge"; /* "Join" gets added by jointype switch */
+ sname = "Merge Join";
+ break;
+ case T_HashJoin:
+ pname = "Hash"; /* "Join" gets added by jointype switch */
+ sname = "Hash Join";
+ break;
+ case T_SeqScan:
+ pname = sname = "Seq Scan";
+ break;
+ case T_SampleScan:
+ pname = sname = "Sample Scan";
+ break;
+ case T_Gather:
+ pname = sname = "Gather";
+ break;
+ case T_GatherMerge:
+ pname = sname = "Gather Merge";
+ break;
+ case T_IndexScan:
+ pname = sname = "Index Scan";
+ break;
+ case T_IndexOnlyScan:
+ pname = sname = "Index Only Scan";
+ break;
+ case T_BitmapIndexScan:
+ pname = sname = "Bitmap Index Scan";
+ break;
+ case T_BitmapHeapScan:
+ pname = sname = "Bitmap Heap Scan";
+ break;
+ case T_TidScan:
+ pname = sname = "Tid Scan";
+ break;
+ case T_TidRangeScan:
+ pname = sname = "Tid Range Scan";
+ break;
+ case T_SubqueryScan:
+ pname = sname = "Subquery Scan";
+ break;
+ case T_FunctionScan:
+ pname = sname = "Function Scan";
+ break;
+ case T_TableFuncScan:
+ pname = sname = "Table Function Scan";
+ break;
+ case T_ValuesScan:
+ pname = sname = "Values Scan";
+ break;
+ case T_CteScan:
+ pname = sname = "CTE Scan";
+ break;
+ case T_NamedTuplestoreScan:
+ pname = sname = "Named Tuplestore Scan";
+ break;
+ case T_WorkTableScan:
+ pname = sname = "WorkTable Scan";
+ break;
+ case T_ForeignScan:
+ sname = "Foreign Scan";
+ switch (((ForeignScan *) plan)->operation)
+ {
+ case CMD_SELECT:
+ pname = "Foreign Scan";
+ operation = "Select";
+ break;
+ case CMD_INSERT:
+ pname = "Foreign Insert";
+ operation = "Insert";
+ break;
+ case CMD_UPDATE:
+ pname = "Foreign Update";
+ operation = "Update";
+ break;
+ case CMD_DELETE:
+ pname = "Foreign Delete";
+ operation = "Delete";
+ break;
+ default:
+ pname = "???";
+ break;
+ }
+ break;
+ case T_CustomScan:
+ sname = "Custom Scan";
+ custom_name = ((CustomScan *) plan)->methods->CustomName;
+ if (custom_name)
+ pname = psprintf("Custom Scan (%s)", custom_name);
+ else
+ pname = sname;
+ break;
+ case T_Material:
+ pname = sname = "Materialize";
+ break;
+ case T_Memoize:
+ pname = sname = "Memoize";
+ break;
+ case T_Sort:
+ pname = sname = "Sort";
+ break;
+ case T_IncrementalSort:
+ pname = sname = "Incremental Sort";
+ break;
+ case T_Group:
+ pname = sname = "Group";
+ break;
+ case T_Agg:
+ {
+ Agg *agg = (Agg *) plan;
+
+ sname = "Aggregate";
+ switch (agg->aggstrategy)
+ {
+ case AGG_PLAIN:
+ pname = "Aggregate";
+ strategy = "Plain";
+ break;
+ case AGG_SORTED:
+ pname = "GroupAggregate";
+ strategy = "Sorted";
+ break;
+ case AGG_HASHED:
+ pname = "HashAggregate";
+ strategy = "Hashed";
+ break;
+ case AGG_MIXED:
+ pname = "MixedAggregate";
+ strategy = "Mixed";
+ break;
+ default:
+ pname = "Aggregate ???";
+ strategy = "???";
+ break;
+ }
+
+ if (DO_AGGSPLIT_SKIPFINAL(agg->aggsplit))
+ {
+ partialmode = "Partial";
+ pname = psprintf("%s %s", partialmode, pname);
+ }
+ else if (DO_AGGSPLIT_COMBINE(agg->aggsplit))
+ {
+ partialmode = "Finalize";
+ pname = psprintf("%s %s", partialmode, pname);
+ }
+ else
+ partialmode = "Simple";
+ }
+ break;
+ case T_WindowAgg:
+ pname = sname = "WindowAgg";
+ break;
+ case T_Unique:
+ pname = sname = "Unique";
+ break;
+ case T_SetOp:
+ sname = "SetOp";
+ switch (((SetOp *) plan)->strategy)
+ {
+ case SETOP_SORTED:
+ pname = "SetOp";
+ strategy = "Sorted";
+ break;
+ case SETOP_HASHED:
+ pname = "HashSetOp";
+ strategy = "Hashed";
+ break;
+ default:
+ pname = "SetOp ???";
+ strategy = "???";
+ break;
+ }
+ break;
+ case T_LockRows:
+ pname = sname = "LockRows";
+ break;
+ case T_Limit:
+ pname = sname = "Limit";
+ break;
+ case T_Hash:
+ pname = sname = "Hash";
+ break;
+ default:
+ pname = sname = "???";
+ break;
+ }
+
+ ExplainOpenGroup("Plan",
+ relationship ? NULL : "Plan",
+ true, es);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (plan_name)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "%s\n", plan_name);
+ es->indent++;
+ }
+ if (es->indent)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "-> ");
+ es->indent += 2;
+ }
+ if (plan->parallel_aware)
+ appendStringInfoString(es->str, "Parallel ");
+ if (plan->async_capable)
+ appendStringInfoString(es->str, "Async ");
+ appendStringInfoString(es->str, pname);
+ es->indent++;
+ }
+ else
+ {
+ ExplainPropertyText("Node Type", sname, es);
+ if (strategy)
+ ExplainPropertyText("Strategy", strategy, es);
+ if (partialmode)
+ ExplainPropertyText("Partial Mode", partialmode, es);
+ if (operation)
+ ExplainPropertyText("Operation", operation, es);
+ if (relationship)
+ ExplainPropertyText("Parent Relationship", relationship, es);
+ if (plan_name)
+ ExplainPropertyText("Subplan Name", plan_name, es);
+ if (custom_name)
+ ExplainPropertyText("Custom Plan Provider", custom_name, es);
+ ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
+ ExplainPropertyBool("Async Capable", plan->async_capable, es);
+ }
+
+ switch (nodeTag(plan))
+ {
+ case T_SeqScan:
+ case T_SampleScan:
+ case T_BitmapHeapScan:
+ case T_TidScan:
+ case T_TidRangeScan:
+ case T_SubqueryScan:
+ case T_FunctionScan:
+ case T_TableFuncScan:
+ case T_ValuesScan:
+ case T_CteScan:
+ case T_WorkTableScan:
+ ExplainScanTarget((Scan *) plan, es);
+ break;
+ case T_ForeignScan:
+ case T_CustomScan:
+ if (((Scan *) plan)->scanrelid > 0)
+ ExplainScanTarget((Scan *) plan, es);
+ break;
+ case T_IndexScan:
+ {
+ IndexScan *indexscan = (IndexScan *) plan;
+
+ ExplainIndexScanDetails(indexscan->indexid,
+ indexscan->indexorderdir,
+ es);
+ ExplainScanTarget((Scan *) indexscan, es);
+ }
+ break;
+ case T_IndexOnlyScan:
+ {
+ IndexOnlyScan *indexonlyscan = (IndexOnlyScan *) plan;
+
+ ExplainIndexScanDetails(indexonlyscan->indexid,
+ indexonlyscan->indexorderdir,
+ es);
+ ExplainScanTarget((Scan *) indexonlyscan, es);
+ }
+ break;
+ case T_BitmapIndexScan:
+ {
+ BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan;
+ const char *indexname =
+ explain_get_index_name(bitmapindexscan->indexid);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfo(es->str, " on %s",
+ quote_identifier(indexname));
+ else
+ ExplainPropertyText("Index Name", indexname, es);
+ }
+ break;
+ case T_ModifyTable:
+ ExplainModifyTarget((ModifyTable *) plan, es);
+ break;
+ case T_NestLoop:
+ case T_MergeJoin:
+ case T_HashJoin:
+ {
+ const char *jointype;
+
+ switch (((Join *) plan)->jointype)
+ {
+ case JOIN_INNER:
+ jointype = "Inner";
+ break;
+ case JOIN_LEFT:
+ jointype = "Left";
+ break;
+ case JOIN_FULL:
+ jointype = "Full";
+ break;
+ case JOIN_RIGHT:
+ jointype = "Right";
+ break;
+ case JOIN_SEMI:
+ jointype = "Semi";
+ break;
+ case JOIN_ANTI:
+ jointype = "Anti";
+ break;
+ case JOIN_RIGHT_ANTI:
+ jointype = "Right Anti";
+ break;
+ default:
+ jointype = "???";
+ break;
+ }
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ /*
+ * For historical reasons, the join type is interpolated
+ * into the node type name...
+ */
+ if (((Join *) plan)->jointype != JOIN_INNER)
+ appendStringInfo(es->str, " %s Join", jointype);
+ else if (!IsA(plan, NestLoop))
+ appendStringInfoString(es->str, " Join");
+ }
+ else
+ ExplainPropertyText("Join Type", jointype, es);
+ }
+ break;
+ case T_SetOp:
+ {
+ const char *setopcmd;
+
+ switch (((SetOp *) plan)->cmd)
+ {
+ case SETOPCMD_INTERSECT:
+ setopcmd = "Intersect";
+ break;
+ case SETOPCMD_INTERSECT_ALL:
+ setopcmd = "Intersect All";
+ break;
+ case SETOPCMD_EXCEPT:
+ setopcmd = "Except";
+ break;
+ case SETOPCMD_EXCEPT_ALL:
+ setopcmd = "Except All";
+ break;
+ default:
+ setopcmd = "???";
+ break;
+ }
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfo(es->str, " %s", setopcmd);
+ else
+ ExplainPropertyText("Command", setopcmd, es);
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (es->costs)
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.0f width=%d)",
+ plan->startup_cost, plan->total_cost,
+ plan->plan_rows, plan->plan_width);
+ }
+ else
+ {
+ ExplainPropertyFloat("Startup Cost", NULL, plan->startup_cost,
+ 2, es);
+ ExplainPropertyFloat("Total Cost", NULL, plan->total_cost,
+ 2, es);
+ ExplainPropertyFloat("Plan Rows", NULL, plan->plan_rows,
+ 0, es);
+ ExplainPropertyInteger("Plan Width", NULL, plan->plan_width,
+ es);
+ }
+ }
+
+ /*
+ * We have to forcibly clean up the instrumentation state because we
+ * haven't done ExecutorEnd yet. This is pretty grotty ...
+ *
+ * Note: contrib/auto_explain could cause instrumentation to be set up
+ * even though we didn't ask for it here. Be careful not to print any
+ * instrumentation results the user didn't ask for. But we do the
+ * InstrEndLoop call anyway, if possible, to reduce the number of cases
+ * auto_explain has to contend with.
+ */
+ if (planstate->instrument)
+ InstrEndLoop(planstate->instrument);
+
+ if (es->analyze &&
+ planstate->instrument && planstate->instrument->nloops > 0)
+ {
+ double nloops = planstate->instrument->nloops;
+ double startup_ms = 1000.0 * planstate->instrument->startup / nloops;
+ double total_ms = 1000.0 * planstate->instrument->total / nloops;
+ double rows = planstate->instrument->ntuples / nloops;
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (es->timing)
+ appendStringInfo(es->str,
+ " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
+ startup_ms, total_ms, rows, nloops);
+ else
+ appendStringInfo(es->str,
+ " (actual rows=%.0f loops=%.0f)",
+ rows, nloops);
+ }
+ else
+ {
+ if (es->timing)
+ {
+ ExplainPropertyFloat("Actual Startup Time", "ms", startup_ms,
+ 3, es);
+ ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
+ 3, es);
+ }
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+ }
+ else if (es->analyze)
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoString(es->str, " (never executed)");
+ else
+ {
+ if (es->timing)
+ {
+ ExplainPropertyFloat("Actual Startup Time", "ms", 0.0, 3, es);
+ ExplainPropertyFloat("Actual Total Time", "ms", 0.0, 3, es);
+ }
+ ExplainPropertyFloat("Actual Rows", NULL, 0.0, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, 0.0, 0, es);
+ }
+ }
+
+ /* in text format, first line ends here */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoChar(es->str, '\n');
+
+ /* prepare per-worker general execution details */
+ if (es->workers_state && es->verbose)
+ {
+ WorkerInstrumentation *w = planstate->worker_instrument;
+
+ for (int n = 0; n < w->num_workers; n++)
+ {
+ Instrumentation *instrument = &w->instrument[n];
+ double nloops = instrument->nloops;
+ double startup_ms;
+ double total_ms;
+ double rows;
+
+ if (nloops <= 0)
+ continue;
+ startup_ms = 1000.0 * instrument->startup / nloops;
+ total_ms = 1000.0 * instrument->total / nloops;
+ rows = instrument->ntuples / nloops;
+
+ ExplainOpenWorker(n, es);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainIndentText(es);
+ if (es->timing)
+ appendStringInfo(es->str,
+ "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
+ startup_ms, total_ms, rows, nloops);
+ else
+ appendStringInfo(es->str,
+ "actual rows=%.0f loops=%.0f\n",
+ rows, nloops);
+ }
+ else
+ {
+ if (es->timing)
+ {
+ ExplainPropertyFloat("Actual Startup Time", "ms",
+ startup_ms, 3, es);
+ ExplainPropertyFloat("Actual Total Time", "ms",
+ total_ms, 3, es);
+ }
+ ExplainPropertyFloat("Actual Rows", NULL, rows, 0, es);
+ ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+ }
+
+ ExplainCloseWorker(n, es);
+ }
+ }
+
+ /* target list */
+ if (es->verbose)
+ show_plan_tlist(planstate, ancestors, es);
+
+ /* unique join */
+ switch (nodeTag(plan))
+ {
+ case T_NestLoop:
+ case T_MergeJoin:
+ case T_HashJoin:
+ /* try not to be too chatty about this in text mode */
+ if (es->format != EXPLAIN_FORMAT_TEXT ||
+ (es->verbose && ((Join *) plan)->inner_unique))
+ ExplainPropertyBool("Inner Unique",
+ ((Join *) plan)->inner_unique,
+ es);
+ break;
+ default:
+ break;
+ }
+
+ /* quals, sort keys, etc */
+ switch (nodeTag(plan))
+ {
+ case T_IndexScan:
+ show_scan_qual(((IndexScan *) plan)->indexqualorig,
+ "Index Cond", planstate, ancestors, es);
+ if (((IndexScan *) plan)->indexqualorig)
+ show_instrumentation_count("Rows Removed by Index Recheck", 2,
+ planstate, es);
+ show_scan_qual(((IndexScan *) plan)->indexorderbyorig,
+ "Order By", planstate, ancestors, es);
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ break;
+ case T_IndexOnlyScan:
+ show_scan_qual(((IndexOnlyScan *) plan)->indexqual,
+ "Index Cond", planstate, ancestors, es);
+ if (((IndexOnlyScan *) plan)->recheckqual)
+ show_instrumentation_count("Rows Removed by Index Recheck", 2,
+ planstate, es);
+ show_scan_qual(((IndexOnlyScan *) plan)->indexorderby,
+ "Order By", planstate, ancestors, es);
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ if (es->analyze)
+ ExplainPropertyFloat("Heap Fetches", NULL,
+ planstate->instrument->ntuples2, 0, es);
+ break;
+ case T_BitmapIndexScan:
+ show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
+ "Index Cond", planstate, ancestors, es);
+ break;
+ case T_BitmapHeapScan:
+ show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
+ "Recheck Cond", planstate, ancestors, es);
+ if (((BitmapHeapScan *) plan)->bitmapqualorig)
+ show_instrumentation_count("Rows Removed by Index Recheck", 2,
+ planstate, es);
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ if (es->analyze)
+ show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
+ break;
+ case T_SampleScan:
+ show_tablesample(((SampleScan *) plan)->tablesample,
+ planstate, ancestors, es);
+ /* fall through to print additional fields the same as SeqScan */
+ /* FALLTHROUGH */
+ case T_SeqScan:
+ case T_ValuesScan:
+ case T_CteScan:
+ case T_NamedTuplestoreScan:
+ case T_WorkTableScan:
+ case T_SubqueryScan:
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ break;
+ case T_Gather:
+ {
+ Gather *gather = (Gather *) plan;
+
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ ExplainPropertyInteger("Workers Planned", NULL,
+ gather->num_workers, es);
+
+ /* Show params evaluated at gather node */
+ if (gather->initParam)
+ show_eval_params(gather->initParam, es);
+
+ if (es->analyze)
+ {
+ int nworkers;
+
+ nworkers = ((GatherState *) planstate)->nworkers_launched;
+ ExplainPropertyInteger("Workers Launched", NULL,
+ nworkers, es);
+ }
+
+ if (gather->single_copy || es->format != EXPLAIN_FORMAT_TEXT)
+ ExplainPropertyBool("Single Copy", gather->single_copy, es);
+ }
+ break;
+ case T_GatherMerge:
+ {
+ GatherMerge *gm = (GatherMerge *) plan;
+
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ ExplainPropertyInteger("Workers Planned", NULL,
+ gm->num_workers, es);
+
+ /* Show params evaluated at gather-merge node */
+ if (gm->initParam)
+ show_eval_params(gm->initParam, es);
+
+ if (es->analyze)
+ {
+ int nworkers;
+
+ nworkers = ((GatherMergeState *) planstate)->nworkers_launched;
+ ExplainPropertyInteger("Workers Launched", NULL,
+ nworkers, es);
+ }
+ }
+ break;
+ case T_FunctionScan:
+ if (es->verbose)
+ {
+ List *fexprs = NIL;
+ ListCell *lc;
+
+ foreach(lc, ((FunctionScan *) plan)->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ fexprs = lappend(fexprs, rtfunc->funcexpr);
+ }
+ /* We rely on show_expression to insert commas as needed */
+ show_expression((Node *) fexprs,
+ "Function Call", planstate, ancestors,
+ es->verbose, es);
+ }
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ break;
+ case T_TableFuncScan:
+ if (es->verbose)
+ {
+ TableFunc *tablefunc = ((TableFuncScan *) plan)->tablefunc;
+
+ show_expression((Node *) tablefunc,
+ "Table Function Call", planstate, ancestors,
+ es->verbose, es);
+ }
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ break;
+ case T_TidScan:
+ {
+ /*
+ * The tidquals list has OR semantics, so be sure to show it
+ * as an OR condition.
+ */
+ List *tidquals = ((TidScan *) plan)->tidquals;
+
+ if (list_length(tidquals) > 1)
+ tidquals = list_make1(make_orclause(tidquals));
+ show_scan_qual(tidquals, "TID Cond", planstate, ancestors, es);
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ }
+ break;
+ case T_TidRangeScan:
+ {
+ /*
+ * The tidrangequals list has AND semantics, so be sure to
+ * show it as an AND condition.
+ */
+ List *tidquals = ((TidRangeScan *) plan)->tidrangequals;
+
+ if (list_length(tidquals) > 1)
+ tidquals = list_make1(make_andclause(tidquals));
+ show_scan_qual(tidquals, "TID Cond", planstate, ancestors, es);
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ }
+ break;
+ case T_ForeignScan:
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ show_foreignscan_info((ForeignScanState *) planstate, es);
+ break;
+ case T_CustomScan:
+ {
+ CustomScanState *css = (CustomScanState *) planstate;
+
+ show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ if (css->methods->ExplainCustomScan)
+ css->methods->ExplainCustomScan(css, ancestors, es);
+ }
+ break;
+ case T_NestLoop:
+ show_upper_qual(((NestLoop *) plan)->join.joinqual,
+ "Join Filter", planstate, ancestors, es);
+ if (((NestLoop *) plan)->join.joinqual)
+ show_instrumentation_count("Rows Removed by Join Filter", 1,
+ planstate, es);
+ show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 2,
+ planstate, es);
+ break;
+ case T_MergeJoin:
+ show_upper_qual(((MergeJoin *) plan)->mergeclauses,
+ "Merge Cond", planstate, ancestors, es);
+ show_upper_qual(((MergeJoin *) plan)->join.joinqual,
+ "Join Filter", planstate, ancestors, es);
+ if (((MergeJoin *) plan)->join.joinqual)
+ show_instrumentation_count("Rows Removed by Join Filter", 1,
+ planstate, es);
+ show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 2,
+ planstate, es);
+ break;
+ case T_HashJoin:
+ show_upper_qual(((HashJoin *) plan)->hashclauses,
+ "Hash Cond", planstate, ancestors, es);
+ show_upper_qual(((HashJoin *) plan)->join.joinqual,
+ "Join Filter", planstate, ancestors, es);
+ if (((HashJoin *) plan)->join.joinqual)
+ show_instrumentation_count("Rows Removed by Join Filter", 1,
+ planstate, es);
+ show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 2,
+ planstate, es);
+ break;
+ 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_WindowAgg:
+ show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ show_upper_qual(((WindowAgg *) plan)->runConditionOrig,
+ "Run Condition", planstate, ancestors, es);
+ break;
+ case T_Group:
+ show_group_keys(castNode(GroupState, planstate), ancestors, es);
+ show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ break;
+ case T_Sort:
+ show_sort_keys(castNode(SortState, planstate), ancestors, es);
+ show_sort_info(castNode(SortState, planstate), es);
+ break;
+ case T_IncrementalSort:
+ show_incremental_sort_keys(castNode(IncrementalSortState, planstate),
+ ancestors, es);
+ show_incremental_sort_info(castNode(IncrementalSortState, planstate),
+ es);
+ break;
+ case T_MergeAppend:
+ show_merge_append_keys(castNode(MergeAppendState, planstate),
+ ancestors, es);
+ break;
+ case T_Result:
+ show_upper_qual((List *) ((Result *) plan)->resconstantqual,
+ "One-Time Filter", planstate, ancestors, es);
+ show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
+ if (plan->qual)
+ show_instrumentation_count("Rows Removed by Filter", 1,
+ planstate, es);
+ break;
+ case T_ModifyTable:
+ show_modifytable_info(castNode(ModifyTableState, planstate), ancestors,
+ es);
+ break;
+ case T_Hash:
+ show_hash_info(castNode(HashState, planstate), es);
+ break;
+ case T_Memoize:
+ show_memoize_info(castNode(MemoizeState, planstate), ancestors,
+ es);
+ break;
+ default:
+ break;
+ }
+
+ /*
+ * Prepare per-worker JIT instrumentation. As with the overall JIT
+ * summary, this is printed only if printing costs is enabled.
+ */
+ if (es->workers_state && es->costs && es->verbose)
+ {
+ SharedJitInstrumentation *w = planstate->worker_jit_instrument;
+
+ if (w)
+ {
+ for (int n = 0; n < w->num_workers; n++)
+ {
+ ExplainOpenWorker(n, es);
+ ExplainPrintJIT(es, planstate->state->es_jit_flags,
+ &w->jit_instr[n]);
+ ExplainCloseWorker(n, es);
+ }
+ }
+ }
+
+ /* Show buffer/WAL usage */
+ if (es->buffers && planstate->instrument)
+ show_buffer_usage(es, &planstate->instrument->bufusage, false);
+ if (es->wal && planstate->instrument)
+ show_wal_usage(es, &planstate->instrument->walusage);
+
+ /* Prepare per-worker buffer/WAL usage */
+ if (es->workers_state && (es->buffers || es->wal) && es->verbose)
+ {
+ WorkerInstrumentation *w = planstate->worker_instrument;
+
+ for (int n = 0; n < w->num_workers; n++)
+ {
+ Instrumentation *instrument = &w->instrument[n];
+ double nloops = instrument->nloops;
+
+ if (nloops <= 0)
+ continue;
+
+ ExplainOpenWorker(n, es);
+ if (es->buffers)
+ show_buffer_usage(es, &instrument->bufusage, false);
+ if (es->wal)
+ show_wal_usage(es, &instrument->walusage);
+ ExplainCloseWorker(n, es);
+ }
+ }
+
+ /* Show per-worker details for this plan node, then pop that stack */
+ if (es->workers_state)
+ ExplainFlushWorkersState(es);
+ es->workers_state = save_workers_state;
+
+ /*
+ * If partition pruning was done during executor initialization, the
+ * number of child plans we'll display below will be less than the number
+ * of subplans that was specified in the plan. To make this a bit less
+ * mysterious, emit an indication that this happened. Note that this
+ * field is emitted now because we want it to be a property of the parent
+ * node; it *cannot* be emitted within the Plans sub-node we'll open next.
+ */
+ switch (nodeTag(plan))
+ {
+ case T_Append:
+ ExplainMissingMembers(((AppendState *) planstate)->as_nplans,
+ list_length(((Append *) plan)->appendplans),
+ es);
+ break;
+ case T_MergeAppend:
+ ExplainMissingMembers(((MergeAppendState *) planstate)->ms_nplans,
+ list_length(((MergeAppend *) plan)->mergeplans),
+ es);
+ break;
+ default:
+ break;
+ }
+
+ /* Get ready to display the child plans */
+ haschildren = planstate->initPlan ||
+ outerPlanState(planstate) ||
+ innerPlanState(planstate) ||
+ IsA(plan, Append) ||
+ IsA(plan, MergeAppend) ||
+ IsA(plan, BitmapAnd) ||
+ IsA(plan, BitmapOr) ||
+ IsA(plan, SubqueryScan) ||
+ (IsA(planstate, CustomScanState) &&
+ ((CustomScanState *) planstate)->custom_ps != NIL) ||
+ planstate->subPlan;
+ if (haschildren)
+ {
+ ExplainOpenGroup("Plans", "Plans", false, es);
+ /* Pass current Plan as head of ancestors list for children */
+ ancestors = lcons(plan, ancestors);
+ }
+
+ /* initPlan-s */
+ if (planstate->initPlan)
+ ExplainSubPlans(planstate->initPlan, ancestors, "InitPlan", es);
+
+ /* lefttree */
+ if (outerPlanState(planstate))
+ ExplainNode(outerPlanState(planstate), ancestors,
+ "Outer", NULL, es);
+
+ /* righttree */
+ if (innerPlanState(planstate))
+ ExplainNode(innerPlanState(planstate), ancestors,
+ "Inner", NULL, es);
+
+ /* special child plans */
+ switch (nodeTag(plan))
+ {
+ case T_Append:
+ ExplainMemberNodes(((AppendState *) planstate)->appendplans,
+ ((AppendState *) planstate)->as_nplans,
+ ancestors, es);
+ break;
+ case T_MergeAppend:
+ ExplainMemberNodes(((MergeAppendState *) planstate)->mergeplans,
+ ((MergeAppendState *) planstate)->ms_nplans,
+ ancestors, es);
+ break;
+ case T_BitmapAnd:
+ ExplainMemberNodes(((BitmapAndState *) planstate)->bitmapplans,
+ ((BitmapAndState *) planstate)->nplans,
+ ancestors, es);
+ break;
+ case T_BitmapOr:
+ ExplainMemberNodes(((BitmapOrState *) planstate)->bitmapplans,
+ ((BitmapOrState *) planstate)->nplans,
+ ancestors, es);
+ break;
+ case T_SubqueryScan:
+ ExplainNode(((SubqueryScanState *) planstate)->subplan, ancestors,
+ "Subquery", NULL, es);
+ break;
+ case T_CustomScan:
+ ExplainCustomChildren((CustomScanState *) planstate,
+ ancestors, es);
+ break;
+ default:
+ break;
+ }
+
+ /* subPlan-s */
+ if (planstate->subPlan)
+ ExplainSubPlans(planstate->subPlan, ancestors, "SubPlan", es);
+
+ /* end of child plans */
+ if (haschildren)
+ {
+ ancestors = list_delete_first(ancestors);
+ ExplainCloseGroup("Plans", "Plans", false, es);
+ }
+
+ /* in text format, undo whatever indentation we added */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ es->indent = save_indent;
+
+ ExplainCloseGroup("Plan",
+ relationship ? NULL : "Plan",
+ true, es);
+}
+
+/*
+ * Show the targetlist of a plan node
+ */
+static void
+show_plan_tlist(PlanState *planstate, List *ancestors, ExplainState *es)
+{
+ Plan *plan = planstate->plan;
+ List *context;
+ List *result = NIL;
+ bool useprefix;
+ ListCell *lc;
+
+ /* No work if empty tlist (this occurs eg in bitmap indexscans) */
+ if (plan->targetlist == NIL)
+ return;
+ /* The tlist of an Append isn't real helpful, so suppress it */
+ if (IsA(plan, Append))
+ return;
+ /* Likewise for MergeAppend and RecursiveUnion */
+ if (IsA(plan, MergeAppend))
+ return;
+ if (IsA(plan, RecursiveUnion))
+ return;
+
+ /*
+ * Likewise for ForeignScan that executes a direct INSERT/UPDATE/DELETE
+ *
+ * Note: the tlist for a ForeignScan that executes a direct INSERT/UPDATE
+ * might contain subplan output expressions that are confusing in this
+ * context. The tlist for a ForeignScan that executes a direct UPDATE/
+ * DELETE always contains "junk" target columns to identify the exact row
+ * to update or delete, which would be confusing in this context. So, we
+ * suppress it in all the cases.
+ */
+ if (IsA(plan, ForeignScan) &&
+ ((ForeignScan *) plan)->operation != CMD_SELECT)
+ return;
+
+ /* Set up deparsing context */
+ context = set_deparse_context_plan(es->deparse_cxt,
+ plan,
+ ancestors);
+ useprefix = list_length(es->rtable) > 1;
+
+ /* Deparse each result column (we now include resjunk ones) */
+ foreach(lc, plan->targetlist)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+ result = lappend(result,
+ deparse_expression((Node *) tle->expr, context,
+ useprefix, false));
+ }
+
+ /* Print results */
+ ExplainPropertyList("Output", result, es);
+}
+
+/*
+ * Show a generic expression
+ */
+static void
+show_expression(Node *node, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ bool useprefix, ExplainState *es)
+{
+ List *context;
+ char *exprstr;
+
+ /* Set up deparsing context */
+ context = set_deparse_context_plan(es->deparse_cxt,
+ planstate->plan,
+ ancestors);
+
+ /* Deparse the expression */
+ exprstr = deparse_expression(node, context, useprefix, false);
+
+ /* And add to es->str */
+ ExplainPropertyText(qlabel, exprstr, es);
+}
+
+/*
+ * Show a qualifier expression (which is a List with implicit AND semantics)
+ */
+static void
+show_qual(List *qual, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ bool useprefix, ExplainState *es)
+{
+ Node *node;
+
+ /* No work if empty qual */
+ if (qual == NIL)
+ return;
+
+ /* Convert AND list to explicit AND */
+ node = (Node *) make_ands_explicit(qual);
+
+ /* And show it */
+ show_expression(node, qlabel, planstate, ancestors, useprefix, es);
+}
+
+/*
+ * Show a qualifier expression for a scan plan node
+ */
+static void
+show_scan_qual(List *qual, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ ExplainState *es)
+{
+ bool useprefix;
+
+ useprefix = (IsA(planstate->plan, SubqueryScan) || es->verbose);
+ show_qual(qual, qlabel, planstate, ancestors, useprefix, es);
+}
+
+/*
+ * Show a qualifier expression for an upper-level plan node
+ */
+static void
+show_upper_qual(List *qual, const char *qlabel,
+ PlanState *planstate, List *ancestors,
+ ExplainState *es)
+{
+ bool useprefix;
+
+ useprefix = (list_length(es->rtable) > 1 || es->verbose);
+ show_qual(qual, qlabel, planstate, ancestors, useprefix, es);
+}
+
+/*
+ * Show the sort keys for a Sort node.
+ */
+static void
+show_sort_keys(SortState *sortstate, List *ancestors, ExplainState *es)
+{
+ Sort *plan = (Sort *) sortstate->ss.ps.plan;
+
+ show_sort_group_keys((PlanState *) sortstate, "Sort Key",
+ plan->numCols, 0, plan->sortColIdx,
+ plan->sortOperators, plan->collations,
+ plan->nullsFirst,
+ ancestors, es);
+}
+
+/*
+ * Show the sort keys for a IncrementalSort node.
+ */
+static void
+show_incremental_sort_keys(IncrementalSortState *incrsortstate,
+ List *ancestors, ExplainState *es)
+{
+ IncrementalSort *plan = (IncrementalSort *) incrsortstate->ss.ps.plan;
+
+ show_sort_group_keys((PlanState *) incrsortstate, "Sort Key",
+ plan->sort.numCols, plan->nPresortedCols,
+ plan->sort.sortColIdx,
+ plan->sort.sortOperators, plan->sort.collations,
+ plan->sort.nullsFirst,
+ ancestors, es);
+}
+
+/*
+ * Likewise, for a MergeAppend node.
+ */
+static void
+show_merge_append_keys(MergeAppendState *mstate, List *ancestors,
+ ExplainState *es)
+{
+ MergeAppend *plan = (MergeAppend *) mstate->ps.plan;
+
+ show_sort_group_keys((PlanState *) mstate, "Sort Key",
+ plan->numCols, 0, plan->sortColIdx,
+ plan->sortOperators, plan->collations,
+ plan->nullsFirst,
+ ancestors, es);
+}
+
+/*
+ * Show the grouping keys for an Agg node.
+ */
+static void
+show_agg_keys(AggState *astate, List *ancestors,
+ ExplainState *es)
+{
+ Agg *plan = (Agg *) astate->ss.ps.plan;
+
+ if (plan->numCols > 0 || plan->groupingSets)
+ {
+ /* The key columns refer to the tlist of the child plan */
+ ancestors = lcons(plan, ancestors);
+
+ if (plan->groupingSets)
+ show_grouping_sets(outerPlanState(astate), plan, ancestors, es);
+ else
+ show_sort_group_keys(outerPlanState(astate), "Group Key",
+ plan->numCols, 0, plan->grpColIdx,
+ NULL, NULL, NULL,
+ ancestors, es);
+
+ ancestors = list_delete_first(ancestors);
+ }
+}
+
+static void
+show_grouping_sets(PlanState *planstate, Agg *agg,
+ List *ancestors, ExplainState *es)
+{
+ List *context;
+ bool useprefix;
+ ListCell *lc;
+
+ /* Set up deparsing context */
+ context = set_deparse_context_plan(es->deparse_cxt,
+ planstate->plan,
+ ancestors);
+ useprefix = (list_length(es->rtable) > 1 || es->verbose);
+
+ ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, es);
+
+ show_grouping_set_keys(planstate, agg, NULL,
+ context, useprefix, ancestors, es);
+
+ foreach(lc, agg->chain)
+ {
+ Agg *aggnode = lfirst(lc);
+ Sort *sortnode = (Sort *) aggnode->plan.lefttree;
+
+ show_grouping_set_keys(planstate, aggnode, sortnode,
+ context, useprefix, ancestors, es);
+ }
+
+ ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, es);
+}
+
+static void
+show_grouping_set_keys(PlanState *planstate,
+ Agg *aggnode, Sort *sortnode,
+ List *context, bool useprefix,
+ List *ancestors, ExplainState *es)
+{
+ Plan *plan = planstate->plan;
+ char *exprstr;
+ ListCell *lc;
+ List *gsets = aggnode->groupingSets;
+ AttrNumber *keycols = aggnode->grpColIdx;
+ const char *keyname;
+ const char *keysetname;
+
+ if (aggnode->aggstrategy == AGG_HASHED || aggnode->aggstrategy == AGG_MIXED)
+ {
+ keyname = "Hash Key";
+ keysetname = "Hash Keys";
+ }
+ else
+ {
+ keyname = "Group Key";
+ 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)
+ {
+ List *result = NIL;
+ ListCell *lc2;
+
+ foreach(lc2, (List *) lfirst(lc))
+ {
+ Index i = lfirst_int(lc2);
+ AttrNumber keyresno = keycols[i];
+ TargetEntry *target = get_tle_by_resno(plan->targetlist,
+ keyresno);
+
+ if (!target)
+ elog(ERROR, "no tlist entry for key %d", keyresno);
+ /* Deparse the expression, showing any top-level cast */
+ exprstr = deparse_expression((Node *) target->expr, context,
+ useprefix, true);
+
+ result = lappend(result, exprstr);
+ }
+
+ if (!result && es->format == EXPLAIN_FORMAT_TEXT)
+ ExplainPropertyText(keyname, "()", es);
+ else
+ ExplainPropertyListNested(keyname, result, es);
+ }
+
+ ExplainCloseGroup(keysetname, keysetname, false, es);
+
+ if (sortnode && es->format == EXPLAIN_FORMAT_TEXT)
+ es->indent--;
+
+ ExplainCloseGroup("Grouping Set", NULL, true, es);
+}
+
+/*
+ * Show the grouping keys for a Group node.
+ */
+static void
+show_group_keys(GroupState *gstate, List *ancestors,
+ ExplainState *es)
+{
+ Group *plan = (Group *) gstate->ss.ps.plan;
+
+ /* The key columns refer to the tlist of the child plan */
+ ancestors = lcons(plan, ancestors);
+ show_sort_group_keys(outerPlanState(gstate), "Group Key",
+ plan->numCols, 0, plan->grpColIdx,
+ NULL, NULL, NULL,
+ ancestors, es);
+ ancestors = list_delete_first(ancestors);
+}
+
+/*
+ * Common code to show sort/group keys, which are represented in plan nodes
+ * as arrays of targetlist indexes. If it's a sort key rather than a group
+ * key, also pass sort operators/collations/nullsFirst arrays.
+ */
+static void
+show_sort_group_keys(PlanState *planstate, const char *qlabel,
+ int nkeys, int nPresortedKeys, AttrNumber *keycols,
+ Oid *sortOperators, Oid *collations, bool *nullsFirst,
+ List *ancestors, ExplainState *es)
+{
+ Plan *plan = planstate->plan;
+ List *context;
+ List *result = NIL;
+ List *resultPresorted = NIL;
+ StringInfoData sortkeybuf;
+ bool useprefix;
+ int keyno;
+
+ if (nkeys <= 0)
+ return;
+
+ initStringInfo(&sortkeybuf);
+
+ /* Set up deparsing context */
+ context = set_deparse_context_plan(es->deparse_cxt,
+ plan,
+ ancestors);
+ useprefix = (list_length(es->rtable) > 1 || es->verbose);
+
+ for (keyno = 0; keyno < nkeys; keyno++)
+ {
+ /* find key expression in tlist */
+ AttrNumber keyresno = keycols[keyno];
+ TargetEntry *target = get_tle_by_resno(plan->targetlist,
+ keyresno);
+ char *exprstr;
+
+ if (!target)
+ elog(ERROR, "no tlist entry for key %d", keyresno);
+ /* Deparse the expression, showing any top-level cast */
+ exprstr = deparse_expression((Node *) target->expr, context,
+ useprefix, true);
+ resetStringInfo(&sortkeybuf);
+ appendStringInfoString(&sortkeybuf, exprstr);
+ /* Append sort order information, if relevant */
+ if (sortOperators != NULL)
+ show_sortorder_options(&sortkeybuf,
+ (Node *) target->expr,
+ sortOperators[keyno],
+ collations[keyno],
+ nullsFirst[keyno]);
+ /* Emit one property-list item per sort key */
+ result = lappend(result, pstrdup(sortkeybuf.data));
+ if (keyno < nPresortedKeys)
+ resultPresorted = lappend(resultPresorted, exprstr);
+ }
+
+ ExplainPropertyList(qlabel, result, es);
+ if (nPresortedKeys > 0)
+ ExplainPropertyList("Presorted Key", resultPresorted, es);
+}
+
+/*
+ * Append nondefault characteristics of the sort ordering of a column to buf
+ * (collation, direction, NULLS FIRST/LAST)
+ */
+static void
+show_sortorder_options(StringInfo buf, Node *sortexpr,
+ Oid sortOperator, Oid collation, bool nullsFirst)
+{
+ Oid sortcoltype = exprType(sortexpr);
+ bool reverse = false;
+ TypeCacheEntry *typentry;
+
+ typentry = lookup_type_cache(sortcoltype,
+ TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);
+
+ /*
+ * Print COLLATE if it's not default for the column's type. There are
+ * some cases where this is redundant, eg if expression is a column whose
+ * declared collation is that collation, but it's hard to distinguish that
+ * here (and arguably, printing COLLATE explicitly is a good idea anyway
+ * in such cases).
+ */
+ if (OidIsValid(collation) && collation != get_typcollation(sortcoltype))
+ {
+ char *collname = get_collation_name(collation);
+
+ if (collname == NULL)
+ elog(ERROR, "cache lookup failed for collation %u", collation);
+ appendStringInfo(buf, " COLLATE %s", quote_identifier(collname));
+ }
+
+ /* Print direction if not ASC, or USING if non-default sort operator */
+ if (sortOperator == typentry->gt_opr)
+ {
+ appendStringInfoString(buf, " DESC");
+ reverse = true;
+ }
+ else if (sortOperator != typentry->lt_opr)
+ {
+ char *opname = get_opname(sortOperator);
+
+ if (opname == NULL)
+ elog(ERROR, "cache lookup failed for operator %u", sortOperator);
+ appendStringInfo(buf, " USING %s", opname);
+ /* Determine whether operator would be considered ASC or DESC */
+ (void) get_equality_op_for_ordering_op(sortOperator, &reverse);
+ }
+
+ /* Add NULLS FIRST/LAST only if it wouldn't be default */
+ if (nullsFirst && !reverse)
+ {
+ appendStringInfoString(buf, " NULLS FIRST");
+ }
+ else if (!nullsFirst && reverse)
+ {
+ appendStringInfoString(buf, " NULLS LAST");
+ }
+}
+
+/*
+ * Show TABLESAMPLE properties
+ */
+static void
+show_tablesample(TableSampleClause *tsc, PlanState *planstate,
+ List *ancestors, ExplainState *es)
+{
+ List *context;
+ bool useprefix;
+ char *method_name;
+ List *params = NIL;
+ char *repeatable;
+ ListCell *lc;
+
+ /* Set up deparsing context */
+ context = set_deparse_context_plan(es->deparse_cxt,
+ planstate->plan,
+ ancestors);
+ useprefix = list_length(es->rtable) > 1;
+
+ /* Get the tablesample method name */
+ method_name = get_func_name(tsc->tsmhandler);
+
+ /* Deparse parameter expressions */
+ foreach(lc, tsc->args)
+ {
+ Node *arg = (Node *) lfirst(lc);
+
+ params = lappend(params,
+ deparse_expression(arg, context,
+ useprefix, false));
+ }
+ if (tsc->repeatable)
+ repeatable = deparse_expression((Node *) tsc->repeatable, context,
+ useprefix, false);
+ else
+ repeatable = NULL;
+
+ /* Print results */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ bool first = true;
+
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "Sampling: %s (", method_name);
+ foreach(lc, params)
+ {
+ if (!first)
+ appendStringInfoString(es->str, ", ");
+ appendStringInfoString(es->str, (const char *) lfirst(lc));
+ first = false;
+ }
+ appendStringInfoChar(es->str, ')');
+ if (repeatable)
+ appendStringInfo(es->str, " REPEATABLE (%s)", repeatable);
+ appendStringInfoChar(es->str, '\n');
+ }
+ else
+ {
+ ExplainPropertyText("Sampling Method", method_name, es);
+ ExplainPropertyList("Sampling Parameters", params, es);
+ if (repeatable)
+ ExplainPropertyText("Repeatable Seed", repeatable, es);
+ }
+}
+
+/*
+ * If it's EXPLAIN ANALYZE, show tuplesort stats for a sort node
+ */
+static void
+show_sort_info(SortState *sortstate, ExplainState *es)
+{
+ if (!es->analyze)
+ return;
+
+ if (sortstate->sort_Done && sortstate->tuplesortstate != NULL)
+ {
+ Tuplesortstate *state = (Tuplesortstate *) sortstate->tuplesortstate;
+ TuplesortInstrumentation stats;
+ const char *sortMethod;
+ const char *spaceType;
+ int64 spaceUsed;
+
+ tuplesort_get_stats(state, &stats);
+ sortMethod = tuplesort_method_name(stats.sortMethod);
+ spaceType = tuplesort_space_type_name(stats.spaceType);
+ spaceUsed = stats.spaceUsed;
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "Sort Method: %s %s: " INT64_FORMAT "kB\n",
+ sortMethod, spaceType, spaceUsed);
+ }
+ else
+ {
+ ExplainPropertyText("Sort Method", sortMethod, es);
+ ExplainPropertyInteger("Sort Space Used", "kB", spaceUsed, es);
+ ExplainPropertyText("Sort Space Type", spaceType, es);
+ }
+ }
+
+ /*
+ * You might think we should just skip this stanza entirely when
+ * es->hide_workers is true, but then we'd get no sort-method output at
+ * all. We have to make it look like worker 0's data is top-level data.
+ * This is easily done by just skipping the OpenWorker/CloseWorker calls.
+ * Currently, we don't worry about the possibility that there are multiple
+ * workers in such a case; if there are, duplicate output fields will be
+ * emitted.
+ */
+ if (sortstate->shared_info != NULL)
+ {
+ int n;
+
+ for (n = 0; n < sortstate->shared_info->num_workers; n++)
+ {
+ TuplesortInstrumentation *sinstrument;
+ const char *sortMethod;
+ const char *spaceType;
+ int64 spaceUsed;
+
+ sinstrument = &sortstate->shared_info->sinstrument[n];
+ if (sinstrument->sortMethod == SORT_TYPE_STILL_IN_PROGRESS)
+ continue; /* ignore any unfilled slots */
+ sortMethod = tuplesort_method_name(sinstrument->sortMethod);
+ spaceType = tuplesort_space_type_name(sinstrument->spaceType);
+ spaceUsed = sinstrument->spaceUsed;
+
+ if (es->workers_state)
+ ExplainOpenWorker(n, es);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str,
+ "Sort Method: %s %s: " INT64_FORMAT "kB\n",
+ sortMethod, spaceType, spaceUsed);
+ }
+ else
+ {
+ ExplainPropertyText("Sort Method", sortMethod, es);
+ ExplainPropertyInteger("Sort Space Used", "kB", spaceUsed, es);
+ ExplainPropertyText("Sort Space Type", spaceType, es);
+ }
+
+ if (es->workers_state)
+ ExplainCloseWorker(n, es);
+ }
+ }
+}
+
+/*
+ * Incremental sort nodes sort in (a potentially very large number of) batches,
+ * so EXPLAIN ANALYZE needs to roll up the tuplesort stats from each batch into
+ * an intelligible summary.
+ *
+ * This function is used for both a non-parallel node and each worker in a
+ * parallel incremental sort node.
+ */
+static void
+show_incremental_sort_group_info(IncrementalSortGroupInfo *groupInfo,
+ const char *groupLabel, bool indent, ExplainState *es)
+{
+ ListCell *methodCell;
+ List *methodNames = NIL;
+
+ /* Generate a list of sort methods used across all groups. */
+ for (int bit = 0; bit < NUM_TUPLESORTMETHODS; bit++)
+ {
+ TuplesortMethod sortMethod = (1 << bit);
+
+ if (groupInfo->sortMethods & sortMethod)
+ {
+ const char *methodName = tuplesort_method_name(sortMethod);
+
+ methodNames = lappend(methodNames, unconstify(char *, methodName));
+ }
+ }
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (indent)
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfo(es->str, "%s Groups: " INT64_FORMAT " Sort Method", groupLabel,
+ groupInfo->groupCount);
+ /* plural/singular based on methodNames size */
+ if (list_length(methodNames) > 1)
+ appendStringInfoString(es->str, "s: ");
+ else
+ appendStringInfoString(es->str, ": ");
+ foreach(methodCell, methodNames)
+ {
+ appendStringInfoString(es->str, (char *) methodCell->ptr_value);
+ if (foreach_current_index(methodCell) < list_length(methodNames) - 1)
+ appendStringInfoString(es->str, ", ");
+ }
+
+ if (groupInfo->maxMemorySpaceUsed > 0)
+ {
+ int64 avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;
+ const char *spaceTypeName;
+
+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);
+ appendStringInfo(es->str, " Average %s: " INT64_FORMAT "kB Peak %s: " INT64_FORMAT "kB",
+ spaceTypeName, avgSpace,
+ spaceTypeName, groupInfo->maxMemorySpaceUsed);
+ }
+
+ if (groupInfo->maxDiskSpaceUsed > 0)
+ {
+ int64 avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;
+
+ const char *spaceTypeName;
+
+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_DISK);
+ appendStringInfo(es->str, " Average %s: " INT64_FORMAT "kB Peak %s: " INT64_FORMAT "kB",
+ spaceTypeName, avgSpace,
+ spaceTypeName, groupInfo->maxDiskSpaceUsed);
+ }
+ }
+ else
+ {
+ StringInfoData groupName;
+
+ initStringInfo(&groupName);
+ appendStringInfo(&groupName, "%s Groups", groupLabel);
+ ExplainOpenGroup("Incremental Sort Groups", groupName.data, true, es);
+ ExplainPropertyInteger("Group Count", NULL, groupInfo->groupCount, es);
+
+ ExplainPropertyList("Sort Methods Used", methodNames, es);
+
+ if (groupInfo->maxMemorySpaceUsed > 0)
+ {
+ int64 avgSpace = groupInfo->totalMemorySpaceUsed / groupInfo->groupCount;
+ const char *spaceTypeName;
+ StringInfoData memoryName;
+
+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);
+ initStringInfo(&memoryName);
+ appendStringInfo(&memoryName, "Sort Space %s", spaceTypeName);
+ ExplainOpenGroup("Sort Space", memoryName.data, true, es);
+
+ ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);
+ ExplainPropertyInteger("Peak Sort Space Used", "kB",
+ groupInfo->maxMemorySpaceUsed, es);
+
+ ExplainCloseGroup("Sort Space", memoryName.data, true, es);
+ }
+ if (groupInfo->maxDiskSpaceUsed > 0)
+ {
+ int64 avgSpace = groupInfo->totalDiskSpaceUsed / groupInfo->groupCount;
+ const char *spaceTypeName;
+ StringInfoData diskName;
+
+ spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_DISK);
+ initStringInfo(&diskName);
+ appendStringInfo(&diskName, "Sort Space %s", spaceTypeName);
+ ExplainOpenGroup("Sort Space", diskName.data, true, es);
+
+ ExplainPropertyInteger("Average Sort Space Used", "kB", avgSpace, es);
+ ExplainPropertyInteger("Peak Sort Space Used", "kB",
+ groupInfo->maxDiskSpaceUsed, es);
+
+ ExplainCloseGroup("Sort Space", diskName.data, true, es);
+ }
+
+ ExplainCloseGroup("Incremental Sort Groups", groupName.data, true, es);
+ }
+}
+
+/*
+ * If it's EXPLAIN ANALYZE, show tuplesort stats for an incremental sort node
+ */
+static void
+show_incremental_sort_info(IncrementalSortState *incrsortstate,
+ ExplainState *es)
+{
+ IncrementalSortGroupInfo *fullsortGroupInfo;
+ IncrementalSortGroupInfo *prefixsortGroupInfo;
+
+ fullsortGroupInfo = &incrsortstate->incsort_info.fullsortGroupInfo;
+
+ if (!es->analyze)
+ return;
+
+ /*
+ * Since we never have any prefix groups unless we've first sorted a full
+ * groups and transitioned modes (copying the tuples into a prefix group),
+ * we don't need to do anything if there were 0 full groups.
+ *
+ * We still have to continue after this block if there are no full groups,
+ * though, since it's possible that we have workers that did real work
+ * even if the leader didn't participate.
+ */
+ if (fullsortGroupInfo->groupCount > 0)
+ {
+ show_incremental_sort_group_info(fullsortGroupInfo, "Full-sort", true, es);
+ prefixsortGroupInfo = &incrsortstate->incsort_info.prefixsortGroupInfo;
+ if (prefixsortGroupInfo->groupCount > 0)
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoChar(es->str, '\n');
+ show_incremental_sort_group_info(prefixsortGroupInfo, "Pre-sorted", true, es);
+ }
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoChar(es->str, '\n');
+ }
+
+ if (incrsortstate->shared_info != NULL)
+ {
+ int n;
+ bool indent_first_line;
+
+ for (n = 0; n < incrsortstate->shared_info->num_workers; n++)
+ {
+ IncrementalSortInfo *incsort_info =
+ &incrsortstate->shared_info->sinfo[n];
+
+ /*
+ * If a worker hasn't processed any sort groups at all, then
+ * exclude it from output since it either didn't launch or didn't
+ * contribute anything meaningful.
+ */
+ fullsortGroupInfo = &incsort_info->fullsortGroupInfo;
+
+ /*
+ * Since we never have any prefix groups unless we've first sorted
+ * a full groups and transitioned modes (copying the tuples into a
+ * prefix group), we don't need to do anything if there were 0
+ * full groups.
+ */
+ if (fullsortGroupInfo->groupCount == 0)
+ continue;
+
+ if (es->workers_state)
+ ExplainOpenWorker(n, es);
+
+ indent_first_line = es->workers_state == NULL || es->verbose;
+ show_incremental_sort_group_info(fullsortGroupInfo, "Full-sort",
+ indent_first_line, es);
+ prefixsortGroupInfo = &incsort_info->prefixsortGroupInfo;
+ if (prefixsortGroupInfo->groupCount > 0)
+ {
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoChar(es->str, '\n');
+ show_incremental_sort_group_info(prefixsortGroupInfo, "Pre-sorted", true, es);
+ }
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ appendStringInfoChar(es->str, '\n');
+
+ if (es->workers_state)
+ ExplainCloseWorker(n, es);
+ }
+ }
+}
+
+/*
+ * Show information on hash buckets/batches.
+ */
+static void
+show_hash_info(HashState *hashstate, ExplainState *es)
+{
+ HashInstrumentation hinstrument = {0};
+
+ /*
+ * Collect stats from the local process, even when it's a parallel query.
+ * In a parallel query, the leader process may or may not have run the
+ * hash join, and even if it did it may not have built a hash table due to
+ * timing (if it started late it might have seen no tuples in the outer
+ * relation and skipped building the hash table). Therefore we have to be
+ * prepared to get instrumentation data from all participants.
+ */
+ if (hashstate->hinstrument)
+ memcpy(&hinstrument, hashstate->hinstrument,
+ sizeof(HashInstrumentation));
+
+ /*
+ * Merge results from workers. In the parallel-oblivious case, the
+ * results from all participants should be identical, except where
+ * participants didn't run the join at all so have no data. In the
+ * parallel-aware case, we need to consider all the results. Each worker
+ * may have seen a different subset of batches and we want to report the
+ * highest memory usage across all batches. We take the maxima of other
+ * values too, for the same reasons as in ExecHashAccumInstrumentation.
+ */
+ if (hashstate->shared_info)
+ {
+ SharedHashInfo *shared_info = hashstate->shared_info;
+ int i;
+
+ for (i = 0; i < shared_info->num_workers; ++i)
+ {
+ HashInstrumentation *worker_hi = &shared_info->hinstrument[i];
+
+ hinstrument.nbuckets = Max(hinstrument.nbuckets,
+ worker_hi->nbuckets);
+ hinstrument.nbuckets_original = Max(hinstrument.nbuckets_original,
+ worker_hi->nbuckets_original);
+ hinstrument.nbatch = Max(hinstrument.nbatch,
+ worker_hi->nbatch);
+ hinstrument.nbatch_original = Max(hinstrument.nbatch_original,
+ worker_hi->nbatch_original);
+ hinstrument.space_peak = Max(hinstrument.space_peak,
+ worker_hi->space_peak);
+ }
+ }
+
+ if (hinstrument.nbatch > 0)
+ {
+ 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)
+ {
+ 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);
+ }
+ else
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str,
+ "Buckets: %d Batches: %d Memory Usage: %ldkB\n",
+ hinstrument.nbuckets, hinstrument.nbatch,
+ spacePeakKb);
+ }
+ }
+}
+
+/*
+ * Show information on memoize hits/misses/evictions and memory usage.
+ */
+static void
+show_memoize_info(MemoizeState *mstate, List *ancestors, ExplainState *es)
+{
+ Plan *plan = ((PlanState *) mstate)->plan;
+ ListCell *lc;
+ List *context;
+ StringInfoData keystr;
+ char *separator = "";
+ bool useprefix;
+ int64 memPeakKb;
+
+ initStringInfo(&keystr);
+
+ /*
+ * It's hard to imagine having a memoize node with fewer than 2 RTEs, but
+ * let's just keep the same useprefix logic as elsewhere in this file.
+ */
+ useprefix = list_length(es->rtable) > 1 || es->verbose;
+
+ /* Set up deparsing context */
+ context = set_deparse_context_plan(es->deparse_cxt,
+ plan,
+ ancestors);
+
+ foreach(lc, ((Memoize *) plan)->param_exprs)
+ {
+ Node *expr = (Node *) lfirst(lc);
+
+ appendStringInfoString(&keystr, separator);
+
+ appendStringInfoString(&keystr, deparse_expression(expr, context,
+ useprefix, false));
+ separator = ", ";
+ }
+
+ if (es->format != EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainPropertyText("Cache Key", keystr.data, es);
+ ExplainPropertyText("Cache Mode", mstate->binary_mode ? "binary" : "logical", es);
+ }
+ else
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "Cache Key: %s\n", keystr.data);
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "Cache Mode: %s\n", mstate->binary_mode ? "binary" : "logical");
+ }
+
+ pfree(keystr.data);
+
+ if (!es->analyze)
+ return;
+
+ if (mstate->stats.cache_misses > 0)
+ {
+ /*
+ * mem_peak is only set when we freed memory, so we must use mem_used
+ * when mem_peak is 0.
+ */
+ if (mstate->stats.mem_peak > 0)
+ memPeakKb = (mstate->stats.mem_peak + 1023) / 1024;
+ else
+ memPeakKb = (mstate->mem_used + 1023) / 1024;
+
+ if (es->format != EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainPropertyInteger("Cache Hits", NULL, mstate->stats.cache_hits, es);
+ ExplainPropertyInteger("Cache Misses", NULL, mstate->stats.cache_misses, es);
+ ExplainPropertyInteger("Cache Evictions", NULL, mstate->stats.cache_evictions, es);
+ ExplainPropertyInteger("Cache Overflows", NULL, mstate->stats.cache_overflows, es);
+ ExplainPropertyInteger("Peak Memory Usage", "kB", memPeakKb, es);
+ }
+ else
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str,
+ "Hits: " UINT64_FORMAT " Misses: " UINT64_FORMAT " Evictions: " UINT64_FORMAT " Overflows: " UINT64_FORMAT " Memory Usage: " INT64_FORMAT "kB\n",
+ mstate->stats.cache_hits,
+ mstate->stats.cache_misses,
+ mstate->stats.cache_evictions,
+ mstate->stats.cache_overflows,
+ memPeakKb);
+ }
+ }
+
+ if (mstate->shared_info == NULL)
+ return;
+
+ /* Show details from parallel workers */
+ for (int n = 0; n < mstate->shared_info->num_workers; n++)
+ {
+ MemoizeInstrumentation *si;
+
+ si = &mstate->shared_info->sinstrument[n];
+
+ /*
+ * Skip workers that didn't do any work. We needn't bother checking
+ * for cache hits as a miss will always occur before a cache hit.
+ */
+ if (si->cache_misses == 0)
+ continue;
+
+ if (es->workers_state)
+ ExplainOpenWorker(n, es);
+
+ /*
+ * Since the worker's MemoizeState.mem_used field is unavailable to
+ * us, ExecEndMemoize will have set the
+ * MemoizeInstrumentation.mem_peak field for us. No need to do the
+ * zero checks like we did for the serial case above.
+ */
+ memPeakKb = (si->mem_peak + 1023) / 1024;
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str,
+ "Hits: " UINT64_FORMAT " Misses: " UINT64_FORMAT " Evictions: " UINT64_FORMAT " Overflows: " UINT64_FORMAT " Memory Usage: " INT64_FORMAT "kB\n",
+ si->cache_hits, si->cache_misses,
+ si->cache_evictions, si->cache_overflows,
+ memPeakKb);
+ }
+ else
+ {
+ ExplainPropertyInteger("Cache Hits", NULL,
+ si->cache_hits, es);
+ ExplainPropertyInteger("Cache Misses", NULL,
+ si->cache_misses, es);
+ ExplainPropertyInteger("Cache Evictions", NULL,
+ si->cache_evictions, es);
+ ExplainPropertyInteger("Cache Overflows", NULL,
+ si->cache_overflows, es);
+ ExplainPropertyInteger("Peak Memory Usage", "kB", memPeakKb,
+ es);
+ }
+
+ if (es->workers_state)
+ ExplainCloseWorker(n, es);
+ }
+}
+
+/*
+ * Show information on hash aggregate memory usage and batches.
+ */
+static void
+show_hashagg_info(AggState *aggstate, ExplainState *es)
+{
+ Agg *agg = (Agg *) aggstate->ss.ps.plan;
+ int64 memPeakKb = (aggstate->hash_mem_peak + 1023) / 1024;
+
+ if (agg->aggstrategy != AGG_HASHED &&
+ agg->aggstrategy != AGG_MIXED)
+ return;
+
+ if (es->format != EXPLAIN_FORMAT_TEXT)
+ {
+ if (es->costs)
+ ExplainPropertyInteger("Planned Partitions", NULL,
+ aggstate->hash_planned_partitions, es);
+
+ /*
+ * During parallel query the leader may have not helped out. We
+ * detect this by checking how much memory it used. If we find it
+ * didn't do any work then we don't show its properties.
+ */
+ if (es->analyze && aggstate->hash_mem_peak > 0)
+ {
+ ExplainPropertyInteger("HashAgg Batches", NULL,
+ aggstate->hash_batches_used, es);
+ ExplainPropertyInteger("Peak Memory Usage", "kB", memPeakKb, es);
+ ExplainPropertyInteger("Disk Usage", "kB",
+ aggstate->hash_disk_used, es);
+ }
+ }
+ else
+ {
+ bool gotone = false;
+
+ if (es->costs && aggstate->hash_planned_partitions > 0)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "Planned Partitions: %d",
+ aggstate->hash_planned_partitions);
+ gotone = true;
+ }
+
+ /*
+ * During parallel query the leader may have not helped out. We
+ * detect this by checking how much memory it used. If we find it
+ * didn't do any work then we don't show its properties.
+ */
+ if (es->analyze && aggstate->hash_mem_peak > 0)
+ {
+ if (!gotone)
+ ExplainIndentText(es);
+ else
+ appendStringInfoSpaces(es->str, 2);
+
+ appendStringInfo(es->str, "Batches: %d Memory Usage: " INT64_FORMAT "kB",
+ aggstate->hash_batches_used, memPeakKb);
+ gotone = true;
+
+ /* Only display disk usage if we spilled to disk */
+ if (aggstate->hash_batches_used > 1)
+ {
+ appendStringInfo(es->str, " Disk Usage: " UINT64_FORMAT "kB",
+ aggstate->hash_disk_used);
+ }
+ }
+
+ if (gotone)
+ appendStringInfoChar(es->str, '\n');
+ }
+
+ /* Display stats for each parallel worker */
+ if (es->analyze && aggstate->shared_info != NULL)
+ {
+ for (int n = 0; n < aggstate->shared_info->num_workers; n++)
+ {
+ AggregateInstrumentation *sinstrument;
+ uint64 hash_disk_used;
+ int hash_batches_used;
+
+ sinstrument = &aggstate->shared_info->sinstrument[n];
+ /* Skip workers that didn't do anything */
+ if (sinstrument->hash_mem_peak == 0)
+ continue;
+ hash_disk_used = sinstrument->hash_disk_used;
+ hash_batches_used = sinstrument->hash_batches_used;
+ memPeakKb = (sinstrument->hash_mem_peak + 1023) / 1024;
+
+ if (es->workers_state)
+ ExplainOpenWorker(n, es);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainIndentText(es);
+
+ appendStringInfo(es->str, "Batches: %d Memory Usage: " INT64_FORMAT "kB",
+ hash_batches_used, memPeakKb);
+
+ /* Only display disk usage if we spilled to disk */
+ if (hash_batches_used > 1)
+ appendStringInfo(es->str, " Disk Usage: " UINT64_FORMAT "kB",
+ hash_disk_used);
+ appendStringInfoChar(es->str, '\n');
+ }
+ else
+ {
+ ExplainPropertyInteger("HashAgg Batches", NULL,
+ hash_batches_used, es);
+ ExplainPropertyInteger("Peak Memory Usage", "kB", memPeakKb,
+ es);
+ ExplainPropertyInteger("Disk Usage", "kB", hash_disk_used, es);
+ }
+
+ if (es->workers_state)
+ ExplainCloseWorker(n, es);
+ }
+ }
+}
+
+/*
+ * If it's EXPLAIN ANALYZE, show exact/lossy pages for a BitmapHeapScan node
+ */
+static void
+show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
+{
+ if (es->format != EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainPropertyInteger("Exact Heap Blocks", NULL,
+ planstate->exact_pages, es);
+ ExplainPropertyInteger("Lossy Heap Blocks", NULL,
+ planstate->lossy_pages, es);
+ }
+ else
+ {
+ if (planstate->exact_pages > 0 || planstate->lossy_pages > 0)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "Heap Blocks:");
+ if (planstate->exact_pages > 0)
+ appendStringInfo(es->str, " exact=%ld", planstate->exact_pages);
+ if (planstate->lossy_pages > 0)
+ appendStringInfo(es->str, " lossy=%ld", planstate->lossy_pages);
+ appendStringInfoChar(es->str, '\n');
+ }
+ }
+}
+
+/*
+ * If it's EXPLAIN ANALYZE, show instrumentation information for a plan node
+ *
+ * "which" identifies which instrumentation counter to print
+ */
+static void
+show_instrumentation_count(const char *qlabel, int which,
+ PlanState *planstate, ExplainState *es)
+{
+ double nfiltered;
+ double nloops;
+
+ if (!es->analyze || !planstate->instrument)
+ return;
+
+ if (which == 2)
+ nfiltered = planstate->instrument->nfiltered2;
+ else
+ nfiltered = planstate->instrument->nfiltered1;
+ nloops = planstate->instrument->nloops;
+
+ /* In text mode, suppress zero counts; they're not interesting enough */
+ if (nfiltered > 0 || es->format != EXPLAIN_FORMAT_TEXT)
+ {
+ if (nloops > 0)
+ ExplainPropertyFloat(qlabel, NULL, nfiltered / nloops, 0, es);
+ else
+ ExplainPropertyFloat(qlabel, NULL, 0.0, 0, es);
+ }
+}
+
+/*
+ * Show extra information for a ForeignScan node.
+ */
+static void
+show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es)
+{
+ FdwRoutine *fdwroutine = fsstate->fdwroutine;
+
+ /* Let the FDW emit whatever fields it wants */
+ if (((ForeignScan *) fsstate->ss.ps.plan)->operation != CMD_SELECT)
+ {
+ if (fdwroutine->ExplainDirectModify != NULL)
+ fdwroutine->ExplainDirectModify(fsstate, es);
+ }
+ else
+ {
+ if (fdwroutine->ExplainForeignScan != NULL)
+ fdwroutine->ExplainForeignScan(fsstate, es);
+ }
+}
+
+/*
+ * Show initplan params evaluated at Gather or Gather Merge node.
+ */
+static void
+show_eval_params(Bitmapset *bms_params, ExplainState *es)
+{
+ int paramid = -1;
+ List *params = NIL;
+
+ Assert(bms_params);
+
+ while ((paramid = bms_next_member(bms_params, paramid)) >= 0)
+ {
+ char param[32];
+
+ snprintf(param, sizeof(param), "$%d", paramid);
+ params = lappend(params, pstrdup(param));
+ }
+
+ if (params)
+ ExplainPropertyList("Params Evaluated", params, es);
+}
+
+/*
+ * Fetch the name of an index in an EXPLAIN
+ *
+ * We allow plugins to get control here so that plans involving hypothetical
+ * indexes can be explained.
+ *
+ * Note: names returned by this function should be "raw"; the caller will
+ * apply quoting if needed. Formerly the convention was to do quoting here,
+ * but we don't want that in non-text output formats.
+ */
+static const char *
+explain_get_index_name(Oid indexId)
+{
+ const char *result;
+
+ if (explain_get_index_name_hook)
+ result = (*explain_get_index_name_hook) (indexId);
+ else
+ result = NULL;
+ if (result == NULL)
+ {
+ /* default behavior: look it up in the catalogs */
+ result = get_rel_name(indexId);
+ if (result == NULL)
+ elog(ERROR, "cache lookup failed for index %u", indexId);
+ }
+ return result;
+}
+
+/*
+ * Show buffer usage details.
+ */
+static void
+show_buffer_usage(ExplainState *es, const BufferUsage *usage, bool planning)
+{
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ bool has_shared = (usage->shared_blks_hit > 0 ||
+ usage->shared_blks_read > 0 ||
+ usage->shared_blks_dirtied > 0 ||
+ usage->shared_blks_written > 0);
+ bool has_local = (usage->local_blks_hit > 0 ||
+ usage->local_blks_read > 0 ||
+ usage->local_blks_dirtied > 0 ||
+ usage->local_blks_written > 0);
+ bool has_temp = (usage->temp_blks_read > 0 ||
+ usage->temp_blks_written > 0);
+ bool has_timing = (!INSTR_TIME_IS_ZERO(usage->blk_read_time) ||
+ !INSTR_TIME_IS_ZERO(usage->blk_write_time));
+ bool has_temp_timing = (!INSTR_TIME_IS_ZERO(usage->temp_blk_read_time) ||
+ !INSTR_TIME_IS_ZERO(usage->temp_blk_write_time));
+ bool show_planning = (planning && (has_shared ||
+ has_local || has_temp || has_timing ||
+ has_temp_timing));
+
+ if (show_planning)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "Planning:\n");
+ es->indent++;
+ }
+
+ /* Show only positive counter values. */
+ if (has_shared || has_local || has_temp)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "Buffers:");
+
+ if (has_shared)
+ {
+ appendStringInfoString(es->str, " shared");
+ if (usage->shared_blks_hit > 0)
+ appendStringInfo(es->str, " hit=%lld",
+ (long long) usage->shared_blks_hit);
+ if (usage->shared_blks_read > 0)
+ appendStringInfo(es->str, " read=%lld",
+ (long long) usage->shared_blks_read);
+ if (usage->shared_blks_dirtied > 0)
+ appendStringInfo(es->str, " dirtied=%lld",
+ (long long) usage->shared_blks_dirtied);
+ if (usage->shared_blks_written > 0)
+ appendStringInfo(es->str, " written=%lld",
+ (long long) usage->shared_blks_written);
+ if (has_local || has_temp)
+ appendStringInfoChar(es->str, ',');
+ }
+ if (has_local)
+ {
+ appendStringInfoString(es->str, " local");
+ if (usage->local_blks_hit > 0)
+ appendStringInfo(es->str, " hit=%lld",
+ (long long) usage->local_blks_hit);
+ if (usage->local_blks_read > 0)
+ appendStringInfo(es->str, " read=%lld",
+ (long long) usage->local_blks_read);
+ if (usage->local_blks_dirtied > 0)
+ appendStringInfo(es->str, " dirtied=%lld",
+ (long long) usage->local_blks_dirtied);
+ if (usage->local_blks_written > 0)
+ appendStringInfo(es->str, " written=%lld",
+ (long long) usage->local_blks_written);
+ if (has_temp)
+ appendStringInfoChar(es->str, ',');
+ }
+ if (has_temp)
+ {
+ appendStringInfoString(es->str, " temp");
+ if (usage->temp_blks_read > 0)
+ appendStringInfo(es->str, " read=%lld",
+ (long long) usage->temp_blks_read);
+ if (usage->temp_blks_written > 0)
+ appendStringInfo(es->str, " written=%lld",
+ (long long) usage->temp_blks_written);
+ }
+ appendStringInfoChar(es->str, '\n');
+ }
+
+ /* As above, show only positive counter values. */
+ if (has_timing || has_temp_timing)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "I/O Timings:");
+
+ if (has_timing)
+ {
+ appendStringInfoString(es->str, " shared");
+ if (!INSTR_TIME_IS_ZERO(usage->blk_read_time))
+ appendStringInfo(es->str, " read=%0.3f",
+ INSTR_TIME_GET_MILLISEC(usage->blk_read_time));
+ if (!INSTR_TIME_IS_ZERO(usage->blk_write_time))
+ appendStringInfo(es->str, " write=%0.3f",
+ INSTR_TIME_GET_MILLISEC(usage->blk_write_time));
+ if (has_temp_timing)
+ appendStringInfoChar(es->str, ',');
+ }
+ if (has_temp_timing)
+ {
+ appendStringInfoString(es->str, " temp");
+ if (!INSTR_TIME_IS_ZERO(usage->temp_blk_read_time))
+ appendStringInfo(es->str, " read=%0.3f",
+ INSTR_TIME_GET_MILLISEC(usage->temp_blk_read_time));
+ if (!INSTR_TIME_IS_ZERO(usage->temp_blk_write_time))
+ appendStringInfo(es->str, " write=%0.3f",
+ INSTR_TIME_GET_MILLISEC(usage->temp_blk_write_time));
+ }
+ appendStringInfoChar(es->str, '\n');
+ }
+
+ if (show_planning)
+ es->indent--;
+ }
+ else
+ {
+ ExplainPropertyInteger("Shared Hit Blocks", NULL,
+ usage->shared_blks_hit, es);
+ ExplainPropertyInteger("Shared Read Blocks", NULL,
+ usage->shared_blks_read, es);
+ ExplainPropertyInteger("Shared Dirtied Blocks", NULL,
+ usage->shared_blks_dirtied, es);
+ ExplainPropertyInteger("Shared Written Blocks", NULL,
+ usage->shared_blks_written, es);
+ ExplainPropertyInteger("Local Hit Blocks", NULL,
+ usage->local_blks_hit, es);
+ ExplainPropertyInteger("Local Read Blocks", NULL,
+ usage->local_blks_read, es);
+ ExplainPropertyInteger("Local Dirtied Blocks", NULL,
+ usage->local_blks_dirtied, es);
+ ExplainPropertyInteger("Local Written Blocks", NULL,
+ usage->local_blks_written, es);
+ ExplainPropertyInteger("Temp Read Blocks", NULL,
+ usage->temp_blks_read, es);
+ ExplainPropertyInteger("Temp Written Blocks", NULL,
+ usage->temp_blks_written, es);
+ if (track_io_timing)
+ {
+ ExplainPropertyFloat("I/O Read Time", "ms",
+ INSTR_TIME_GET_MILLISEC(usage->blk_read_time),
+ 3, es);
+ ExplainPropertyFloat("I/O Write Time", "ms",
+ INSTR_TIME_GET_MILLISEC(usage->blk_write_time),
+ 3, es);
+ ExplainPropertyFloat("Temp I/O Read Time", "ms",
+ INSTR_TIME_GET_MILLISEC(usage->temp_blk_read_time),
+ 3, es);
+ ExplainPropertyFloat("Temp I/O Write Time", "ms",
+ INSTR_TIME_GET_MILLISEC(usage->temp_blk_write_time),
+ 3, es);
+ }
+ }
+}
+
+/*
+ * Show WAL usage details.
+ */
+static void
+show_wal_usage(ExplainState *es, const WalUsage *usage)
+{
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ /* Show only positive counter values. */
+ if ((usage->wal_records > 0) || (usage->wal_fpi > 0) ||
+ (usage->wal_bytes > 0))
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "WAL:");
+
+ if (usage->wal_records > 0)
+ appendStringInfo(es->str, " records=%lld",
+ (long long) usage->wal_records);
+ if (usage->wal_fpi > 0)
+ appendStringInfo(es->str, " fpi=%lld",
+ (long long) usage->wal_fpi);
+ if (usage->wal_bytes > 0)
+ appendStringInfo(es->str, " bytes=" UINT64_FORMAT,
+ usage->wal_bytes);
+ appendStringInfoChar(es->str, '\n');
+ }
+ }
+ else
+ {
+ ExplainPropertyInteger("WAL Records", NULL,
+ usage->wal_records, es);
+ ExplainPropertyInteger("WAL FPI", NULL,
+ usage->wal_fpi, es);
+ ExplainPropertyUInteger("WAL Bytes", NULL,
+ usage->wal_bytes, es);
+ }
+}
+
+/*
+ * Add some additional details about an IndexScan or IndexOnlyScan
+ */
+static void
+ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
+ ExplainState *es)
+{
+ const char *indexname = explain_get_index_name(indexid);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (ScanDirectionIsBackward(indexorderdir))
+ appendStringInfoString(es->str, " Backward");
+ appendStringInfo(es->str, " using %s", quote_identifier(indexname));
+ }
+ else
+ {
+ const char *scandir;
+
+ switch (indexorderdir)
+ {
+ case BackwardScanDirection:
+ scandir = "Backward";
+ break;
+ case ForwardScanDirection:
+ scandir = "Forward";
+ break;
+ default:
+ scandir = "???";
+ break;
+ }
+ ExplainPropertyText("Scan Direction", scandir, es);
+ ExplainPropertyText("Index Name", indexname, es);
+ }
+}
+
+/*
+ * Show the target of a Scan node
+ */
+static void
+ExplainScanTarget(Scan *plan, ExplainState *es)
+{
+ ExplainTargetRel((Plan *) plan, plan->scanrelid, es);
+}
+
+/*
+ * Show the target of a ModifyTable node
+ *
+ * Here we show the nominal target (ie, the relation that was named in the
+ * original query). If the actual target(s) is/are different, we'll show them
+ * in show_modifytable_info().
+ */
+static void
+ExplainModifyTarget(ModifyTable *plan, ExplainState *es)
+{
+ ExplainTargetRel((Plan *) plan, plan->nominalRelation, es);
+}
+
+/*
+ * Show the target relation of a scan or modify node
+ */
+static void
+ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
+{
+ char *objectname = NULL;
+ char *namespace = NULL;
+ const char *objecttag = NULL;
+ RangeTblEntry *rte;
+ char *refname;
+
+ rte = rt_fetch(rti, es->rtable);
+ refname = (char *) list_nth(es->rtable_names, rti - 1);
+ if (refname == NULL)
+ refname = rte->eref->aliasname;
+
+ switch (nodeTag(plan))
+ {
+ case T_SeqScan:
+ case T_SampleScan:
+ case T_IndexScan:
+ case T_IndexOnlyScan:
+ case T_BitmapHeapScan:
+ case T_TidScan:
+ case T_TidRangeScan:
+ case T_ForeignScan:
+ case T_CustomScan:
+ case T_ModifyTable:
+ /* Assert it's on a real relation */
+ Assert(rte->rtekind == RTE_RELATION);
+ objectname = get_rel_name(rte->relid);
+ if (es->verbose)
+ namespace = get_namespace_name_or_temp(get_rel_namespace(rte->relid));
+ objecttag = "Relation Name";
+ break;
+ case T_FunctionScan:
+ {
+ FunctionScan *fscan = (FunctionScan *) plan;
+
+ /* Assert it's on a RangeFunction */
+ Assert(rte->rtekind == RTE_FUNCTION);
+
+ /*
+ * If the expression is still a function call of a single
+ * function, we can get the real name of the function.
+ * Otherwise, punt. (Even if it was a single function call
+ * originally, the optimizer could have simplified it away.)
+ */
+ if (list_length(fscan->functions) == 1)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) linitial(fscan->functions);
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ {
+ FuncExpr *funcexpr = (FuncExpr *) rtfunc->funcexpr;
+ Oid funcid = funcexpr->funcid;
+
+ objectname = get_func_name(funcid);
+ if (es->verbose)
+ namespace = get_namespace_name_or_temp(get_func_namespace(funcid));
+ }
+ }
+ objecttag = "Function Name";
+ }
+ break;
+ case T_TableFuncScan:
+ Assert(rte->rtekind == RTE_TABLEFUNC);
+ objectname = "xmltable";
+ objecttag = "Table Function Name";
+ break;
+ case T_ValuesScan:
+ Assert(rte->rtekind == RTE_VALUES);
+ break;
+ case T_CteScan:
+ /* Assert it's on a non-self-reference CTE */
+ Assert(rte->rtekind == RTE_CTE);
+ Assert(!rte->self_reference);
+ objectname = rte->ctename;
+ objecttag = "CTE Name";
+ break;
+ case T_NamedTuplestoreScan:
+ Assert(rte->rtekind == RTE_NAMEDTUPLESTORE);
+ objectname = rte->enrname;
+ objecttag = "Tuplestore Name";
+ break;
+ case T_WorkTableScan:
+ /* Assert it's on a self-reference CTE */
+ Assert(rte->rtekind == RTE_CTE);
+ Assert(rte->self_reference);
+ objectname = rte->ctename;
+ objecttag = "CTE Name";
+ break;
+ default:
+ break;
+ }
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfoString(es->str, " on");
+ if (namespace != NULL)
+ appendStringInfo(es->str, " %s.%s", quote_identifier(namespace),
+ quote_identifier(objectname));
+ else if (objectname != NULL)
+ appendStringInfo(es->str, " %s", quote_identifier(objectname));
+ if (objectname == NULL || strcmp(refname, objectname) != 0)
+ appendStringInfo(es->str, " %s", quote_identifier(refname));
+ }
+ else
+ {
+ if (objecttag != NULL && objectname != NULL)
+ ExplainPropertyText(objecttag, objectname, es);
+ if (namespace != NULL)
+ ExplainPropertyText("Schema", namespace, es);
+ ExplainPropertyText("Alias", refname, es);
+ }
+}
+
+/*
+ * Show extra information for a ModifyTable node
+ *
+ * We have three objectives here. First, if there's more than one target
+ * table or it's different from the nominal target, identify the actual
+ * target(s). Second, give FDWs a chance to display extra info about foreign
+ * targets. Third, show information about ON CONFLICT.
+ */
+static void
+show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
+ ExplainState *es)
+{
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ const char *operation;
+ const char *foperation;
+ bool labeltargets;
+ int j;
+ List *idxNames = NIL;
+ ListCell *lst;
+
+ switch (node->operation)
+ {
+ case CMD_INSERT:
+ operation = "Insert";
+ foperation = "Foreign Insert";
+ break;
+ case CMD_UPDATE:
+ operation = "Update";
+ foperation = "Foreign Update";
+ break;
+ case CMD_DELETE:
+ operation = "Delete";
+ foperation = "Foreign Delete";
+ break;
+ case CMD_MERGE:
+ operation = "Merge";
+ /* XXX unsupported for now, but avoid compiler noise */
+ foperation = "Foreign Merge";
+ break;
+ default:
+ operation = "???";
+ foperation = "Foreign ???";
+ break;
+ }
+
+ /* Should we explicitly label target relations? */
+ labeltargets = (mtstate->mt_nrels > 1 ||
+ (mtstate->mt_nrels == 1 &&
+ mtstate->resultRelInfo[0].ri_RangeTableIndex != node->nominalRelation));
+
+ if (labeltargets)
+ ExplainOpenGroup("Target Tables", "Target Tables", false, es);
+
+ for (j = 0; j < mtstate->mt_nrels; j++)
+ {
+ ResultRelInfo *resultRelInfo = mtstate->resultRelInfo + j;
+ FdwRoutine *fdwroutine = resultRelInfo->ri_FdwRoutine;
+
+ if (labeltargets)
+ {
+ /* Open a group for this target */
+ ExplainOpenGroup("Target Table", NULL, true, es);
+
+ /*
+ * In text mode, decorate each target with operation type, so that
+ * ExplainTargetRel's output of " on foo" will read nicely.
+ */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str,
+ fdwroutine ? foperation : operation);
+ }
+
+ /* Identify target */
+ ExplainTargetRel((Plan *) node,
+ resultRelInfo->ri_RangeTableIndex,
+ es);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ appendStringInfoChar(es->str, '\n');
+ es->indent++;
+ }
+ }
+
+ /* Give FDW a chance if needed */
+ if (!resultRelInfo->ri_usesFdwDirectModify &&
+ fdwroutine != NULL &&
+ fdwroutine->ExplainForeignModify != NULL)
+ {
+ List *fdw_private = (List *) list_nth(node->fdwPrivLists, j);
+
+ fdwroutine->ExplainForeignModify(mtstate,
+ resultRelInfo,
+ fdw_private,
+ j,
+ es);
+ }
+
+ if (labeltargets)
+ {
+ /* Undo the indentation we added in text format */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ es->indent--;
+
+ /* Close the group */
+ ExplainCloseGroup("Target Table", NULL, true, es);
+ }
+ }
+
+ /* Gather names of ON CONFLICT arbiter indexes */
+ foreach(lst, node->arbiterIndexes)
+ {
+ char *indexname = get_rel_name(lfirst_oid(lst));
+
+ idxNames = lappend(idxNames, indexname);
+ }
+
+ if (node->onConflictAction != ONCONFLICT_NONE)
+ {
+ ExplainPropertyText("Conflict Resolution",
+ node->onConflictAction == ONCONFLICT_NOTHING ?
+ "NOTHING" : "UPDATE",
+ es);
+
+ /*
+ * Don't display arbiter indexes at all when DO NOTHING variant
+ * implicitly ignores all conflicts
+ */
+ if (idxNames)
+ ExplainPropertyList("Conflict Arbiter Indexes", idxNames, es);
+
+ /* ON CONFLICT DO UPDATE WHERE qual is specially displayed */
+ if (node->onConflictWhere)
+ {
+ show_upper_qual((List *) node->onConflictWhere, "Conflict Filter",
+ &mtstate->ps, ancestors, es);
+ show_instrumentation_count("Rows Removed by Conflict Filter", 1, &mtstate->ps, es);
+ }
+
+ /* EXPLAIN ANALYZE display of actual outcome for each tuple proposed */
+ if (es->analyze && mtstate->ps.instrument)
+ {
+ double total;
+ double insert_path;
+ double other_path;
+
+ InstrEndLoop(outerPlanState(mtstate)->instrument);
+
+ /* count the number of source rows */
+ total = outerPlanState(mtstate)->instrument->ntuples;
+ other_path = mtstate->ps.instrument->ntuples2;
+ insert_path = total - other_path;
+
+ ExplainPropertyFloat("Tuples Inserted", NULL,
+ insert_path, 0, es);
+ ExplainPropertyFloat("Conflicting Tuples", NULL,
+ other_path, 0, es);
+ }
+ }
+ else if (node->operation == CMD_MERGE)
+ {
+ /* EXPLAIN ANALYZE display of tuples processed */
+ if (es->analyze && mtstate->ps.instrument)
+ {
+ double total;
+ double insert_path;
+ double update_path;
+ double delete_path;
+ double skipped_path;
+
+ InstrEndLoop(outerPlanState(mtstate)->instrument);
+
+ /* count the number of source rows */
+ total = outerPlanState(mtstate)->instrument->ntuples;
+ insert_path = mtstate->mt_merge_inserted;
+ update_path = mtstate->mt_merge_updated;
+ delete_path = mtstate->mt_merge_deleted;
+ skipped_path = total - insert_path - update_path - delete_path;
+ Assert(skipped_path >= 0);
+
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (total > 0)
+ {
+ ExplainIndentText(es);
+ appendStringInfoString(es->str, "Tuples:");
+ if (insert_path > 0)
+ appendStringInfo(es->str, " inserted=%.0f", insert_path);
+ if (update_path > 0)
+ appendStringInfo(es->str, " updated=%.0f", update_path);
+ if (delete_path > 0)
+ appendStringInfo(es->str, " deleted=%.0f", delete_path);
+ if (skipped_path > 0)
+ appendStringInfo(es->str, " skipped=%.0f", skipped_path);
+ appendStringInfoChar(es->str, '\n');
+ }
+ }
+ else
+ {
+ ExplainPropertyFloat("Tuples Inserted", NULL, insert_path, 0, es);
+ ExplainPropertyFloat("Tuples Updated", NULL, update_path, 0, es);
+ ExplainPropertyFloat("Tuples Deleted", NULL, delete_path, 0, es);
+ ExplainPropertyFloat("Tuples Skipped", NULL, skipped_path, 0, es);
+ }
+ }
+ }
+
+ if (labeltargets)
+ ExplainCloseGroup("Target Tables", "Target Tables", false, es);
+}
+
+/*
+ * Explain the constituent plans of an Append, MergeAppend,
+ * BitmapAnd, or BitmapOr node.
+ *
+ * The ancestors list should already contain the immediate parent of these
+ * plans.
+ */
+static void
+ExplainMemberNodes(PlanState **planstates, int nplans,
+ List *ancestors, ExplainState *es)
+{
+ int j;
+
+ for (j = 0; j < nplans; j++)
+ ExplainNode(planstates[j], ancestors,
+ "Member", NULL, es);
+}
+
+/*
+ * Report about any pruned subnodes of an Append or MergeAppend node.
+ *
+ * nplans indicates the number of live subplans.
+ * nchildren indicates the original number of subnodes in the Plan;
+ * some of these may have been pruned by the run-time pruning code.
+ */
+static void
+ExplainMissingMembers(int nplans, int nchildren, ExplainState *es)
+{
+ if (nplans < nchildren || es->format != EXPLAIN_FORMAT_TEXT)
+ ExplainPropertyInteger("Subplans Removed", NULL,
+ nchildren - nplans, es);
+}
+
+/*
+ * Explain a list of SubPlans (or initPlans, which also use SubPlan nodes).
+ *
+ * The ancestors list should already contain the immediate parent of these
+ * SubPlans.
+ */
+static void
+ExplainSubPlans(List *plans, List *ancestors,
+ const char *relationship, ExplainState *es)
+{
+ ListCell *lst;
+
+ foreach(lst, plans)
+ {
+ SubPlanState *sps = (SubPlanState *) lfirst(lst);
+ SubPlan *sp = sps->subplan;
+
+ /*
+ * There can be multiple SubPlan nodes referencing the same physical
+ * subplan (same plan_id, which is its index in PlannedStmt.subplans).
+ * We should print a subplan only once, so track which ones we already
+ * printed. This state must be global across the plan tree, since the
+ * duplicate nodes could be in different plan nodes, eg both a bitmap
+ * indexscan's indexqual and its parent heapscan's recheck qual. (We
+ * do not worry too much about which plan node we show the subplan as
+ * attached to in such cases.)
+ */
+ if (bms_is_member(sp->plan_id, es->printed_subplans))
+ continue;
+ es->printed_subplans = bms_add_member(es->printed_subplans,
+ sp->plan_id);
+
+ /*
+ * Treat the SubPlan node as an ancestor of the plan node(s) within
+ * it, so that ruleutils.c can find the referents of subplan
+ * parameters.
+ */
+ ancestors = lcons(sp, ancestors);
+
+ ExplainNode(sps->planstate, ancestors,
+ relationship, sp->plan_name, es);
+
+ ancestors = list_delete_first(ancestors);
+ }
+}
+
+/*
+ * Explain a list of children of a CustomScan.
+ */
+static void
+ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es)
+{
+ ListCell *cell;
+ const char *label =
+ (list_length(css->custom_ps) != 1 ? "children" : "child");
+
+ foreach(cell, css->custom_ps)
+ ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es);
+}
+
+/*
+ * Create a per-plan-node workspace for collecting per-worker data.
+ *
+ * Output related to each worker will be temporarily "set aside" into a
+ * separate buffer, which we'll merge into the main output stream once
+ * we've processed all data for the plan node. This makes it feasible to
+ * generate a coherent sub-group of fields for each worker, even though the
+ * code that produces the fields is in several different places in this file.
+ * Formatting of such a set-aside field group is managed by
+ * ExplainOpenSetAsideGroup and ExplainSaveGroup/ExplainRestoreGroup.
+ */
+static ExplainWorkersState *
+ExplainCreateWorkersState(int num_workers)
+{
+ ExplainWorkersState *wstate;
+
+ wstate = (ExplainWorkersState *) palloc(sizeof(ExplainWorkersState));
+ wstate->num_workers = num_workers;
+ wstate->worker_inited = (bool *) palloc0(num_workers * sizeof(bool));
+ wstate->worker_str = (StringInfoData *)
+ palloc0(num_workers * sizeof(StringInfoData));
+ wstate->worker_state_save = (int *) palloc(num_workers * sizeof(int));
+ return wstate;
+}
+
+/*
+ * Begin or resume output into the set-aside group for worker N.
+ */
+static void
+ExplainOpenWorker(int n, ExplainState *es)
+{
+ ExplainWorkersState *wstate = es->workers_state;
+
+ Assert(wstate);
+ Assert(n >= 0 && n < wstate->num_workers);
+
+ /* Save prior output buffer pointer */
+ wstate->prev_str = es->str;
+
+ if (!wstate->worker_inited[n])
+ {
+ /* First time through, so create the buffer for this worker */
+ initStringInfo(&wstate->worker_str[n]);
+ es->str = &wstate->worker_str[n];
+
+ /*
+ * Push suitable initial formatting state for this worker's field
+ * group. We allow one extra logical nesting level, since this group
+ * will eventually be wrapped in an outer "Workers" group.
+ */
+ ExplainOpenSetAsideGroup("Worker", NULL, true, 2, es);
+
+ /*
+ * In non-TEXT formats we always emit a "Worker Number" field, even if
+ * there's no other data for this worker.
+ */
+ if (es->format != EXPLAIN_FORMAT_TEXT)
+ ExplainPropertyInteger("Worker Number", NULL, n, es);
+
+ wstate->worker_inited[n] = true;
+ }
+ else
+ {
+ /* Resuming output for a worker we've already emitted some data for */
+ es->str = &wstate->worker_str[n];
+
+ /* Restore formatting state saved by last ExplainCloseWorker() */
+ ExplainRestoreGroup(es, 2, &wstate->worker_state_save[n]);
+ }
+
+ /*
+ * In TEXT format, prefix the first output line for this worker with
+ * "Worker N:". Then, any additional lines should be indented one more
+ * stop than the "Worker N" line is.
+ */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ if (es->str->len == 0)
+ {
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "Worker %d: ", n);
+ }
+
+ es->indent++;
+ }
+}
+
+/*
+ * End output for worker N --- must pair with previous ExplainOpenWorker call
+ */
+static void
+ExplainCloseWorker(int n, ExplainState *es)
+{
+ ExplainWorkersState *wstate = es->workers_state;
+
+ Assert(wstate);
+ Assert(n >= 0 && n < wstate->num_workers);
+ Assert(wstate->worker_inited[n]);
+
+ /*
+ * Save formatting state in case we do another ExplainOpenWorker(), then
+ * pop the formatting stack.
+ */
+ ExplainSaveGroup(es, 2, &wstate->worker_state_save[n]);
+
+ /*
+ * In TEXT format, if we didn't actually produce any output line(s) then
+ * truncate off the partial line emitted by ExplainOpenWorker. (This is
+ * to avoid bogus output if, say, show_buffer_usage chooses not to print
+ * anything for the worker.) Also fix up the indent level.
+ */
+ if (es->format == EXPLAIN_FORMAT_TEXT)
+ {
+ while (es->str->len > 0 && es->str->data[es->str->len - 1] != '\n')
+ es->str->data[--(es->str->len)] = '\0';
+
+ es->indent--;
+ }
+
+ /* Restore prior output buffer pointer */
+ es->str = wstate->prev_str;
+}
+
+/*
+ * Print per-worker info for current node, then free the ExplainWorkersState.
+ */
+static void
+ExplainFlushWorkersState(ExplainState *es)
+{
+ ExplainWorkersState *wstate = es->workers_state;
+
+ ExplainOpenGroup("Workers", "Workers", false, es);
+ for (int i = 0; i < wstate->num_workers; i++)
+ {
+ if (wstate->worker_inited[i])
+ {
+ /* This must match previous ExplainOpenSetAsideGroup call */
+ ExplainOpenGroup("Worker", NULL, true, es);
+ appendStringInfoString(es->str, wstate->worker_str[i].data);
+ ExplainCloseGroup("Worker", NULL, true, es);
+
+ pfree(wstate->worker_str[i].data);
+ }
+ }
+ ExplainCloseGroup("Workers", "Workers", false, es);
+
+ pfree(wstate->worker_inited);
+ pfree(wstate->worker_str);
+ pfree(wstate->worker_state_save);
+ pfree(wstate);
+}
+
+/*
+ * Explain a property, such as sort keys or targets, that takes the form of
+ * a list of unlabeled items. "data" is a list of C strings.
+ */
+void
+ExplainPropertyList(const char *qlabel, List *data, ExplainState *es)
+{
+ ListCell *lc;
+ bool first = true;
+
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ ExplainIndentText(es);
+ appendStringInfo(es->str, "%s: ", qlabel);
+ foreach(lc, data)
+ {
+ if (!first)
+ appendStringInfoString(es->str, ", ");
+ appendStringInfoString(es->str, (const char *) lfirst(lc));
+ first = false;
+ }
+ appendStringInfoChar(es->str, '\n');
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ ExplainXMLTag(qlabel, X_OPENING, es);
+ foreach(lc, data)
+ {
+ char *str;
+
+ appendStringInfoSpaces(es->str, es->indent * 2 + 2);
+ appendStringInfoString(es->str, "<Item>");
+ str = escape_xml((const char *) lfirst(lc));
+ appendStringInfoString(es->str, str);
+ pfree(str);
+ appendStringInfoString(es->str, "</Item>\n");
+ }
+ ExplainXMLTag(qlabel, X_CLOSING, es);
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ escape_json(es->str, qlabel);
+ appendStringInfoString(es->str, ": [");
+ foreach(lc, data)
+ {
+ if (!first)
+ appendStringInfoString(es->str, ", ");
+ escape_json(es->str, (const char *) lfirst(lc));
+ first = false;
+ }
+ appendStringInfoChar(es->str, ']');
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ ExplainYAMLLineStarting(es);
+ appendStringInfo(es->str, "%s: ", qlabel);
+ foreach(lc, data)
+ {
+ appendStringInfoChar(es->str, '\n');
+ appendStringInfoSpaces(es->str, es->indent * 2 + 2);
+ appendStringInfoString(es->str, "- ");
+ escape_yaml(es->str, (const char *) lfirst(lc));
+ }
+ break;
+ }
+}
+
+/*
+ * Explain a property that takes the form of a list of unlabeled items within
+ * another list. "data" is a list of C strings.
+ */
+void
+ExplainPropertyListNested(const char *qlabel, List *data, ExplainState *es)
+{
+ ListCell *lc;
+ bool first = true;
+
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ case EXPLAIN_FORMAT_XML:
+ ExplainPropertyList(qlabel, data, es);
+ return;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ appendStringInfoChar(es->str, '[');
+ foreach(lc, data)
+ {
+ if (!first)
+ appendStringInfoString(es->str, ", ");
+ escape_json(es->str, (const char *) lfirst(lc));
+ first = false;
+ }
+ appendStringInfoChar(es->str, ']');
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ ExplainYAMLLineStarting(es);
+ appendStringInfoString(es->str, "- [");
+ foreach(lc, data)
+ {
+ if (!first)
+ appendStringInfoString(es->str, ", ");
+ escape_yaml(es->str, (const char *) lfirst(lc));
+ first = false;
+ }
+ appendStringInfoChar(es->str, ']');
+ break;
+ }
+}
+
+/*
+ * Explain a simple property.
+ *
+ * If "numeric" is true, the value is a number (or other value that
+ * doesn't need quoting in JSON).
+ *
+ * If unit is non-NULL the text format will display it after the value.
+ *
+ * This usually should not be invoked directly, but via one of the datatype
+ * specific routines ExplainPropertyText, ExplainPropertyInteger, etc.
+ */
+static void
+ExplainProperty(const char *qlabel, const char *unit, const char *value,
+ bool numeric, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ ExplainIndentText(es);
+ if (unit)
+ appendStringInfo(es->str, "%s: %s %s\n", qlabel, value, unit);
+ else
+ appendStringInfo(es->str, "%s: %s\n", qlabel, value);
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ {
+ char *str;
+
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ ExplainXMLTag(qlabel, X_OPENING | X_NOWHITESPACE, es);
+ str = escape_xml(value);
+ appendStringInfoString(es->str, str);
+ pfree(str);
+ ExplainXMLTag(qlabel, X_CLOSING | X_NOWHITESPACE, es);
+ appendStringInfoChar(es->str, '\n');
+ }
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ escape_json(es->str, qlabel);
+ appendStringInfoString(es->str, ": ");
+ if (numeric)
+ appendStringInfoString(es->str, value);
+ else
+ escape_json(es->str, value);
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ ExplainYAMLLineStarting(es);
+ appendStringInfo(es->str, "%s: ", qlabel);
+ if (numeric)
+ appendStringInfoString(es->str, value);
+ else
+ escape_yaml(es->str, value);
+ break;
+ }
+}
+
+/*
+ * Explain a string-valued property.
+ */
+void
+ExplainPropertyText(const char *qlabel, const char *value, ExplainState *es)
+{
+ ExplainProperty(qlabel, NULL, value, false, es);
+}
+
+/*
+ * Explain an integer-valued property.
+ */
+void
+ExplainPropertyInteger(const char *qlabel, const char *unit, int64 value,
+ ExplainState *es)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), INT64_FORMAT, value);
+ ExplainProperty(qlabel, unit, buf, true, es);
+}
+
+/*
+ * Explain an unsigned integer-valued property.
+ */
+void
+ExplainPropertyUInteger(const char *qlabel, const char *unit, uint64 value,
+ ExplainState *es)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), UINT64_FORMAT, value);
+ ExplainProperty(qlabel, unit, buf, true, es);
+}
+
+/*
+ * Explain a float-valued property, using the specified number of
+ * fractional digits.
+ */
+void
+ExplainPropertyFloat(const char *qlabel, const char *unit, double value,
+ int ndigits, ExplainState *es)
+{
+ char *buf;
+
+ buf = psprintf("%.*f", ndigits, value);
+ ExplainProperty(qlabel, unit, buf, true, es);
+ pfree(buf);
+}
+
+/*
+ * Explain a bool-valued property.
+ */
+void
+ExplainPropertyBool(const char *qlabel, bool value, ExplainState *es)
+{
+ ExplainProperty(qlabel, NULL, value ? "true" : "false", true, es);
+}
+
+/*
+ * Open a group of related objects.
+ *
+ * objtype is the type of the group object, labelname is its label within
+ * a containing object (if any).
+ *
+ * If labeled is true, the group members will be labeled properties,
+ * while if it's false, they'll be unlabeled objects.
+ */
+void
+ExplainOpenGroup(const char *objtype, const char *labelname,
+ bool labeled, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ ExplainXMLTag(objtype, X_OPENING, es);
+ es->indent++;
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ if (labelname)
+ {
+ escape_json(es->str, labelname);
+ appendStringInfoString(es->str, ": ");
+ }
+ appendStringInfoChar(es->str, labeled ? '{' : '[');
+
+ /*
+ * In JSON format, the grouping_stack is an integer list. 0 means
+ * we've emitted nothing at this grouping level, 1 means we've
+ * emitted something (and so the next item needs a comma). See
+ * ExplainJSONLineEnding().
+ */
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ es->indent++;
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+
+ /*
+ * In YAML format, the grouping stack is an integer list. 0 means
+ * we've emitted nothing at this grouping level AND this grouping
+ * level is unlabeled and must be marked with "- ". See
+ * ExplainYAMLLineStarting().
+ */
+ ExplainYAMLLineStarting(es);
+ if (labelname)
+ {
+ appendStringInfo(es->str, "%s: ", labelname);
+ es->grouping_stack = lcons_int(1, es->grouping_stack);
+ }
+ else
+ {
+ appendStringInfoString(es->str, "- ");
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ }
+ es->indent++;
+ break;
+ }
+}
+
+/*
+ * Close a group of related objects.
+ * Parameters must match the corresponding ExplainOpenGroup call.
+ */
+void
+ExplainCloseGroup(const char *objtype, const char *labelname,
+ bool labeled, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ es->indent--;
+ ExplainXMLTag(objtype, X_CLOSING, es);
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ es->indent--;
+ appendStringInfoChar(es->str, '\n');
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ appendStringInfoChar(es->str, labeled ? '}' : ']');
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ es->indent--;
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+ }
+}
+
+/*
+ * Open a group of related objects, without emitting actual data.
+ *
+ * Prepare the formatting state as though we were beginning a group with
+ * the identified properties, but don't actually emit anything. Output
+ * subsequent to this call can be redirected into a separate output buffer,
+ * and then eventually appended to the main output buffer after doing a
+ * regular ExplainOpenGroup call (with the same parameters).
+ *
+ * The extra "depth" parameter is the new group's depth compared to current.
+ * It could be more than one, in case the eventual output will be enclosed
+ * in additional nesting group levels. We assume we don't need to track
+ * formatting state for those levels while preparing this group's output.
+ *
+ * There is no ExplainCloseSetAsideGroup --- in current usage, we always
+ * pop this state with ExplainSaveGroup.
+ */
+static void
+ExplainOpenSetAsideGroup(const char *objtype, const char *labelname,
+ bool labeled, int depth, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ es->indent += depth;
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ es->indent += depth;
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ if (labelname)
+ es->grouping_stack = lcons_int(1, es->grouping_stack);
+ else
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ es->indent += depth;
+ break;
+ }
+}
+
+/*
+ * Pop one level of grouping state, allowing for a re-push later.
+ *
+ * This is typically used after ExplainOpenSetAsideGroup; pass the
+ * same "depth" used for that.
+ *
+ * This should not emit any output. If state needs to be saved,
+ * save it at *state_save. Currently, an integer save area is sufficient
+ * for all formats, but we might need to revisit that someday.
+ */
+static void
+ExplainSaveGroup(ExplainState *es, int depth, int *state_save)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ es->indent -= depth;
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ es->indent -= depth;
+ *state_save = linitial_int(es->grouping_stack);
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ es->indent -= depth;
+ *state_save = linitial_int(es->grouping_stack);
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+ }
+}
+
+/*
+ * Re-push one level of grouping state, undoing the effects of ExplainSaveGroup.
+ */
+static void
+ExplainRestoreGroup(ExplainState *es, int depth, int *state_save)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ es->indent += depth;
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ es->grouping_stack = lcons_int(*state_save, es->grouping_stack);
+ es->indent += depth;
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ es->grouping_stack = lcons_int(*state_save, es->grouping_stack);
+ es->indent += depth;
+ break;
+ }
+}
+
+/*
+ * Emit a "dummy" group that never has any members.
+ *
+ * objtype is the type of the group object, labelname is its label within
+ * a containing object (if any).
+ */
+static void
+ExplainDummyGroup(const char *objtype, const char *labelname, ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ ExplainXMLTag(objtype, X_CLOSE_IMMEDIATE, es);
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ ExplainJSONLineEnding(es);
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ if (labelname)
+ {
+ escape_json(es->str, labelname);
+ appendStringInfoString(es->str, ": ");
+ }
+ escape_json(es->str, objtype);
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ ExplainYAMLLineStarting(es);
+ if (labelname)
+ {
+ escape_yaml(es->str, labelname);
+ appendStringInfoString(es->str, ": ");
+ }
+ else
+ {
+ appendStringInfoString(es->str, "- ");
+ }
+ escape_yaml(es->str, objtype);
+ break;
+ }
+}
+
+/*
+ * Emit the start-of-output boilerplate.
+ *
+ * This is just enough different from processing a subgroup that we need
+ * a separate pair of subroutines.
+ */
+void
+ExplainBeginOutput(ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ appendStringInfoString(es->str,
+ "<explain xmlns=\"http://www.postgresql.org/2009/explain\">\n");
+ es->indent++;
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ /* top-level structure is an array of plans */
+ appendStringInfoChar(es->str, '[');
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ es->indent++;
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ es->grouping_stack = lcons_int(0, es->grouping_stack);
+ break;
+ }
+}
+
+/*
+ * Emit the end-of-output boilerplate.
+ */
+void
+ExplainEndOutput(ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* nothing to do */
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ es->indent--;
+ appendStringInfoString(es->str, "</explain>");
+ break;
+
+ case EXPLAIN_FORMAT_JSON:
+ es->indent--;
+ appendStringInfoString(es->str, "\n]");
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+
+ case EXPLAIN_FORMAT_YAML:
+ es->grouping_stack = list_delete_first(es->grouping_stack);
+ break;
+ }
+}
+
+/*
+ * Put an appropriate separator between multiple plans
+ */
+void
+ExplainSeparatePlans(ExplainState *es)
+{
+ switch (es->format)
+ {
+ case EXPLAIN_FORMAT_TEXT:
+ /* add a blank line */
+ appendStringInfoChar(es->str, '\n');
+ break;
+
+ case EXPLAIN_FORMAT_XML:
+ case EXPLAIN_FORMAT_JSON:
+ case EXPLAIN_FORMAT_YAML:
+ /* nothing to do */
+ break;
+ }
+}
+
+/*
+ * Emit opening or closing XML tag.
+ *
+ * "flags" must contain X_OPENING, X_CLOSING, or X_CLOSE_IMMEDIATE.
+ * Optionally, OR in X_NOWHITESPACE to suppress the whitespace we'd normally
+ * add.
+ *
+ * XML restricts tag names more than our other output formats, eg they can't
+ * contain white space or slashes. Replace invalid characters with dashes,
+ * so that for example "I/O Read Time" becomes "I-O-Read-Time".
+ */
+static void
+ExplainXMLTag(const char *tagname, int flags, ExplainState *es)
+{
+ const char *s;
+ const char *valid = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.";
+
+ if ((flags & X_NOWHITESPACE) == 0)
+ appendStringInfoSpaces(es->str, 2 * es->indent);
+ appendStringInfoCharMacro(es->str, '<');
+ if ((flags & X_CLOSING) != 0)
+ appendStringInfoCharMacro(es->str, '/');
+ for (s = tagname; *s; s++)
+ appendStringInfoChar(es->str, strchr(valid, *s) ? *s : '-');
+ if ((flags & X_CLOSE_IMMEDIATE) != 0)
+ appendStringInfoString(es->str, " /");
+ appendStringInfoCharMacro(es->str, '>');
+ if ((flags & X_NOWHITESPACE) == 0)
+ appendStringInfoCharMacro(es->str, '\n');
+}
+
+/*
+ * Indent a text-format line.
+ *
+ * We indent by two spaces per indentation level. However, when emitting
+ * data for a parallel worker there might already be data on the current line
+ * (cf. ExplainOpenWorker); in that case, don't indent any more.
+ */
+static void
+ExplainIndentText(ExplainState *es)
+{
+ Assert(es->format == EXPLAIN_FORMAT_TEXT);
+ if (es->str->len == 0 || es->str->data[es->str->len - 1] == '\n')
+ appendStringInfoSpaces(es->str, es->indent * 2);
+}
+
+/*
+ * Emit a JSON line ending.
+ *
+ * JSON requires a comma after each property but the last. To facilitate this,
+ * in JSON format, the text emitted for each property begins just prior to the
+ * preceding line-break (and comma, if applicable).
+ */
+static void
+ExplainJSONLineEnding(ExplainState *es)
+{
+ Assert(es->format == EXPLAIN_FORMAT_JSON);
+ if (linitial_int(es->grouping_stack) != 0)
+ appendStringInfoChar(es->str, ',');
+ else
+ linitial_int(es->grouping_stack) = 1;
+ appendStringInfoChar(es->str, '\n');
+}
+
+/*
+ * Indent a YAML line.
+ *
+ * YAML lines are ordinarily indented by two spaces per indentation level.
+ * The text emitted for each property begins just prior to the preceding
+ * line-break, except for the first property in an unlabeled group, for which
+ * it begins immediately after the "- " that introduces the group. The first
+ * property of the group appears on the same line as the opening "- ".
+ */
+static void
+ExplainYAMLLineStarting(ExplainState *es)
+{
+ Assert(es->format == EXPLAIN_FORMAT_YAML);
+ if (linitial_int(es->grouping_stack) == 0)
+ {
+ linitial_int(es->grouping_stack) = 1;
+ }
+ else
+ {
+ appendStringInfoChar(es->str, '\n');
+ appendStringInfoSpaces(es->str, es->indent * 2);
+ }
+}
+
+/*
+ * YAML is a superset of JSON; unfortunately, the YAML quoting rules are
+ * ridiculously complicated -- as documented in sections 5.3 and 7.3.3 of
+ * http://yaml.org/spec/1.2/spec.html -- so we chose to just quote everything.
+ * Empty strings, strings with leading or trailing whitespace, and strings
+ * containing a variety of special characters must certainly be quoted or the
+ * output is invalid; and other seemingly harmless strings like "0xa" or
+ * "true" must be quoted, lest they be interpreted as a hexadecimal or Boolean
+ * constant rather than a string.
+ */
+static void
+escape_yaml(StringInfo buf, const char *str)
+{
+ escape_json(buf, str);
+}