/*------------------------------------------------------------------------- * * 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, pstate->p_sourcetext); 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/local"); 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, ""); str = escape_xml((const char *) lfirst(lc)); appendStringInfoString(es->str, str); pfree(str); appendStringInfoString(es->str, "\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, "\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, ""); 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); }