| /*------------------------------------------------------------------------- |
| * |
| * explain.c |
| * Explain query execution plans |
| * |
| * Portions Copyright (c) 2005-2010, Greenplum inc |
| * Portions Copyright (c) 2012-Present VMware, Inc. or its affiliates. |
| * Portions Copyright (c) 1996-2021, 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 "catalog/pg_inherits.h" |
| #include "commands/createas.h" |
| #include "commands/defrem.h" |
| #include "commands/prepare.h" |
| #include "commands/queue.h" |
| #include "executor/execUtils.h" |
| #include "executor/hashjoin.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 "tcop/utility.h" |
| #include "utils/builtins.h" |
| #include "utils/guc_tables.h" |
| #include "utils/json.h" |
| #include "utils/lsyscache.h" |
| #include "utils/metrics_utils.h" |
| #include "utils/rel.h" |
| #include "utils/resgroup.h" |
| #include "utils/ruleutils.h" |
| #include "utils/snapmgr.h" |
| #include "utils/tuplesort.h" |
| #include "utils/typcache.h" |
| #include "utils/xml.h" |
| |
| #include "cdb/cdbgang.h" |
| #include "cdb/cdbvars.h" |
| #include "optimizer/clauses.h" |
| #include "optimizer/tlist.h" |
| #include "optimizer/optimizer.h" |
| #include "optimizer/orca.h" |
| |
| #ifdef USE_ORCA |
| extern char *SerializeDXLPlan(Query *parse); |
| #endif |
| |
| |
| /* 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); |
| |
| #ifdef USE_ORCA |
| static void ExplainDXL(Query *query, ExplainState *es, |
| const char *queryString, |
| ParamListInfo params); |
| #endif |
| |
| 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_tuple_split_keys(TupleSplitState *tstate, 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_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_windowagg_keys(WindowAggState *waggstate, List *ancestors, ExplainState *es); |
| static void show_aggref_info(List *targetList, const char *qlabel, ExplainState *es); |
| static void show_incremental_sort_info(IncrementalSortState *incrsortstate, |
| ExplainState *es); |
| static void show_hash_info(HashState *hashstate, ExplainState *es); |
| static void show_runtime_filter_info(RuntimeFilterState *rfstate, |
| ExplainState *es); |
| static void show_pushdown_runtime_filter_info(PlanState *planstate, |
| ExplainState *es); |
| static void show_memoize_info(MemoizeState *mstate, List *ancestors, |
| ExplainState *es); |
| static void show_hashagg_info(AggState *hashstate, 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 void show_join_pruning_info(List *join_prune_ids, 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, SliceTable *sliceTable); |
| 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 ExplainPropertyStringInfo(const char *qlabel, ExplainState *es, |
| const char *fmt,...) |
| pg_attribute_printf(3, 4); |
| 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); |
| static int countLeafPartTables(Oid relId); |
| |
| static void Explainlocus(ExplainState *es, CdbLocusType locustype, int parallel); |
| |
| /* Include the Greenplum EXPLAIN extensions */ |
| #include "explain_gp.c" |
| |
| |
| /* |
| * 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, "locus") == 0) |
| es->locus = 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, "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 if (strcmp(opt->defname, "dxl") == 0) |
| es->dxl = defGetBoolean(opt); |
| else if (strcmp(opt->defname, "slicetable") == 0) |
| es->slicetable = defGetBoolean(opt); |
| else |
| ereport(ERROR, |
| (errcode(ERRCODE_SYNTAX_ERROR), |
| errmsg("unrecognized EXPLAIN option \"%s\"", |
| opt->defname), |
| parser_errposition(pstate, opt->location))); |
| } |
| |
| 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"))); |
| |
| /* if the summary was not set explicitly, set default value */ |
| es->summary = (summary_set) ? es->summary : es->analyze; |
| |
| if (explain_memory_verbosity >= EXPLAIN_MEMORY_VERBOSITY_DETAIL) |
| es->memory_detail = true; |
| |
| 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; |
| } |
| |
| #ifdef USE_ORCA |
| /* |
| * ExplainDXL - |
| * print out the execution plan for one Query in DXL format |
| * this function implicitly uses optimizer |
| */ |
| static void |
| ExplainDXL(Query *query, ExplainState *es, const char *queryString, |
| ParamListInfo params) |
| { |
| MemoryContext oldcxt = CurrentMemoryContext; |
| bool save_enumerate; |
| char *dxl = NULL; |
| PlannerInfo *root; |
| PlannerGlobal *glob; |
| Query *pqueryCopy; |
| |
| save_enumerate = optimizer_enumerate_plans; |
| |
| /* Do the EXPLAIN. */ |
| |
| /* enable plan enumeration before calling optimizer */ |
| optimizer_enumerate_plans = true; |
| |
| /* |
| * Initialize a dummy PlannerGlobal struct. ORCA doesn't use it, but the |
| * pre- and post-processing steps do. |
| */ |
| glob = makeNode(PlannerGlobal); |
| glob->subplans = NIL; |
| glob->subroots = NIL; |
| glob->rewindPlanIDs = NULL; |
| glob->transientPlan = false; |
| glob->oneoffPlan = false; |
| glob->share.shared_inputs = NULL; |
| glob->share.shared_input_count = 0; |
| glob->share.motStack = NIL; |
| glob->share.qdShares = NULL; |
| /* these will be filled in below, in the pre- and post-processing steps */ |
| glob->finalrtable = NIL; |
| glob->relationOids = NIL; |
| glob->invalItems = NIL; |
| |
| root = makeNode(PlannerInfo); |
| root->parse = query; |
| root->glob = glob; |
| root->query_level = 1; |
| root->planner_cxt = CurrentMemoryContext; |
| root->wt_param_id = -1; |
| |
| /* create a local copy to hand to the optimizer */ |
| pqueryCopy = (Query *) copyObject(query); |
| |
| /* |
| * Pre-process the Query tree before calling optimizer. |
| * |
| * Constant folding will add dependencies to functions or relations in |
| * glob->invalItems, for any functions that are inlined or eliminated |
| * away. (We will find dependencies to other objects later, after planning). |
| */ |
| pqueryCopy = fold_constants(root, pqueryCopy, params, GPOPT_MAX_FOLDED_CONSTANT_SIZE); |
| |
| /* |
| * If any Query in the tree mixes window functions and aggregates, we need to |
| * transform it such that the grouped query appears as a subquery |
| */ |
| pqueryCopy = (Query *) transformGroupedWindows((Node *) pqueryCopy, NULL); |
| |
| |
| /* optimize query using optimizer and get generated plan in DXL format */ |
| dxl = SerializeDXLPlan(pqueryCopy); |
| |
| /* restore old value of enumerate plans GUC */ |
| optimizer_enumerate_plans = save_enumerate; |
| |
| if (dxl == NULL) |
| elog(NOTICE, "Optimizer failed to produce plan"); |
| else |
| { |
| appendStringInfoString(es->str, dxl); |
| appendStringInfoChar(es->str, '\n'); /* separator line */ |
| pfree(dxl); |
| } |
| |
| /* Free the memory we used. */ |
| MemoryContextSwitchTo(oldcxt); |
| } |
| #endif |
| |
| /* |
| * 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) |
| { |
| #ifdef USE_ORCA |
| if (es->dxl) |
| { |
| ExplainDXL(query, es, queryString, params); |
| return; |
| } |
| #endif |
| |
| /* 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); |
| |
| /* |
| * GPDB_92_MERGE_FIXME: it really should be an optimizer's responsibility |
| * to correctly set the into-clause and into-policy of the PlannedStmt. |
| */ |
| if (into != NULL) |
| plan->intoClause = copyObject(into); |
| |
| /* 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), cursorOptions); |
| } |
| } |
| |
| /* |
| * 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) |
| { |
| if(ctas->into && ctas->into->dynamicTbl) |
| ExplainDummyGroup("CREATE DYNAMIC TABLE", NULL, es); |
| else |
| 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, |
| int cursorOptions) |
| { |
| 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; |
| |
| if (es->analyze) |
| instrument_option |= INSTRUMENT_CDB; |
| |
| if (es->memory_detail) |
| instrument_option |= INSTRUMENT_MEMORY_DETAIL; |
| |
| /* |
| * 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; |
| |
| // GPDB_14_MERGE_FIXME: fix intoClause in optimizer |
| plannedstmt->intoClause = copyObject(into); |
| /* Create a QueryDesc for the query */ |
| queryDesc = CreateQueryDesc(plannedstmt, queryString, |
| GetActiveSnapshot(), InvalidSnapshot, |
| dest, params, queryEnv, instrument_option); |
| |
| /* GPDB hook for collecting query info */ |
| if (query_info_collect_hook) |
| (*query_info_collect_hook)(METRICS_QUERY_SUBMIT, queryDesc); |
| |
| /* Allocate workarea for summary stats. */ |
| if (es->analyze) |
| { |
| /* Attach workarea to QueryDesc so ExecSetParamPlan() can find it. */ |
| queryDesc->showstatctx = cdbexplain_showExecStatsBegin(queryDesc, |
| starttime); |
| } |
| |
| /* Select execution options */ |
| if (es->analyze) |
| eflags = 0; /* default run-to-completion flags */ |
| else |
| eflags = EXEC_FLAG_EXPLAIN_ONLY; |
| if (into) |
| eflags |= GetIntoRelEFlags(into); |
| |
| check_and_unassign_from_resgroup(queryDesc->plannedstmt); |
| queryDesc->plannedstmt->query_mem = |
| ResourceManagerGetQueryMemoryLimit(queryDesc->plannedstmt); |
| |
| /* 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, 0L, true); |
| |
| /* run cleanup too */ |
| ExecutorFinish(queryDesc); |
| |
| /* Wait for completion of all qExec processes. */ |
| if (queryDesc->estate->dispatcherState && queryDesc->estate->dispatcherState->primaryResults) |
| { |
| cdbdisp_checkDispatchResult(queryDesc->estate->dispatcherState, DISPATCH_WAIT_NONE); |
| /* |
| * If some QE throw errors, we might not receive stats from QEs, |
| * In ExecutorEnd we will reThrow QE's error, In this situation, |
| * there is no need to execute ExplainPrintPlan. reThrow error in advance. |
| */ |
| ErrorData *qeError = NULL; |
| cdbdisp_getDispatchResults(queryDesc->estate->dispatcherState, &qeError); |
| if (qeError) |
| { |
| FlushErrorState(); |
| ThrowErrorData(qeError); |
| } |
| } |
| |
| /* 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); |
| |
| if (cursorOptions & CURSOR_OPT_PARALLEL_RETRIEVE) |
| ExplainParallelRetrieveCursor(es, queryDesc); |
| |
| /* |
| * 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 && 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) |
| plannedstmt->queryId, es); |
| } |
| |
| /* 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 slice table */ |
| if (es->slicetable) |
| ExplainPrintSliceTable(es, queryDesc); |
| |
| /* Print info about runtime of triggers */ |
| if (es->analyze) |
| ExplainPrintTriggers(es, queryDesc); |
| |
| /* |
| * Display per-slice and whole-query statistics. |
| */ |
| if (es->analyze) |
| cdbexplain_showExecStatsEnd(queryDesc->plannedstmt, queryDesc->showstatctx, |
| queryDesc->estate, es); |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| { |
| ExplainOpenGroup("Settings", "Settings", true, es); |
| |
| if (queryDesc->plannedstmt->planGen == PLANGEN_PLANNER) |
| ExplainPropertyStringInfo("Optimizer", es, "Postgres query optimizer"); |
| #ifdef USE_ORCA |
| else |
| ExplainPropertyStringInfo("Optimizer", es, "GPORCA"); |
| #endif |
| |
| ExplainCloseGroup("Settings", "Settings", true, es); |
| } |
| |
| /* |
| * 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, PlanGenerator planGen) |
| { |
| int num; |
| struct config_generic **gucs; |
| |
| /* request an array of relevant settings */ |
| gucs = get_explain_guc_options(&num, es->verbose, es->settings); |
| |
| if (es->format != EXPLAIN_FORMAT_TEXT) |
| { |
| ExplainOpenGroup("Settings", "Settings", true, es); |
| |
| if (planGen == PLANGEN_PLANNER) |
| ExplainPropertyStringInfo("Optimizer", es, "Postgres query optimizer"); |
| #ifdef USE_ORCA |
| else |
| ExplainPropertyStringInfo("Optimizer", es, "GPORCA"); |
| #endif |
| |
| 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; |
| |
| 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) |
| { |
| EState *estate = queryDesc->estate; |
| 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; |
| es->showstatctx = queryDesc->showstatctx; |
| |
| /* CDB: Find slice table entry for the root slice. */ |
| es->currentSlice = getCurrentSlice(estate, LocallyExecutingSliceIndex(estate)); |
| |
| /* |
| * Get local stats if root slice was executed here in the qDisp, as long |
| * as we haven't already gathered the statistics. This can happen when an |
| * executor hook generates EXPLAIN output. |
| */ |
| if (es->analyze && !es->showstatctx->stats_gathered) |
| { |
| if (Gp_role != GP_ROLE_EXECUTE && (!es->currentSlice || sliceRunsOnQD(es->currentSlice))) |
| cdbexplain_localExecStats(queryDesc->planstate, es->showstatctx); |
| |
| /* Fill in the plan's Instrumentation with stats from qExecs. */ |
| if (estate->dispatcherState && estate->dispatcherState->primaryResults) |
| cdbexplain_recvExecStats(queryDesc->planstate, |
| estate->dispatcherState->primaryResults, |
| LocallyExecutingSliceIndex(estate), |
| es->showstatctx); |
| } |
| |
| 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 force_parallel_mode=regress to get the |
| * same results as running the same tests with force_parallel_mode=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, queryDesc->plannedstmt->planGen); |
| } |
| |
| |
| /* |
| * ExplainPrintSliceTable - |
| * convert the MPP slice table text and append it to es->str |
| */ |
| void |
| ExplainPrintSliceTable(ExplainState *es, QueryDesc *queryDesc) |
| { |
| SliceTable *sliceTable = queryDesc->estate->es_sliceTable; |
| int numSlices = (sliceTable ? sliceTable->numSlices : 0); |
| |
| ExplainOpenGroup("Slice Table", "Slice Table", false, es); |
| |
| for (int i = 0; i < numSlices; i++) |
| { |
| ExecSlice *slice = &sliceTable->slices[i]; |
| const char *gangType = "???"; |
| |
| switch (slice->gangType) |
| { |
| case GANGTYPE_UNALLOCATED: |
| gangType = "Dispatcher"; |
| break; |
| case GANGTYPE_ENTRYDB_READER: |
| gangType = "Entry DB Reader"; |
| break; |
| case GANGTYPE_SINGLETON_READER: |
| gangType = "Singleton Reader"; |
| break; |
| case GANGTYPE_PRIMARY_READER: |
| gangType = "Reader"; |
| break; |
| case GANGTYPE_PRIMARY_WRITER: |
| gangType = "Primary Writer"; |
| break; |
| } |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| { |
| appendStringInfo(es->str, "Slice %d: %s; root %d; parent %d; gang size %d", |
| i, |
| gangType, |
| slice->rootIndex, |
| slice->parentIndex, |
| list_length(slice->segments)); |
| if (slice->gangType == GANGTYPE_SINGLETON_READER) |
| appendStringInfo(es->str, "; segment %d", linitial_int(slice->segments)); |
| appendStringInfoString(es->str, "\n"); |
| } |
| else |
| { |
| ExplainOpenGroup("Slice", NULL, true, es); |
| ExplainPropertyInteger("Slice ID", NULL, i, es); |
| ExplainPropertyText("Gang Type", gangType, es); |
| ExplainPropertyInteger("Root", NULL, slice->rootIndex, es); |
| ExplainPropertyInteger("Parent", NULL, slice->parentIndex, es); |
| ExplainPropertyInteger("Gang Size", NULL, list_length(slice->segments), es); |
| if (slice->gangType == GANGTYPE_SINGLETON_READER) |
| ExplainPropertyInteger("Segment", NULL, linitial_int(slice->segments), es); |
| ExplainCloseGroup("Slice", NULL, true, es); |
| } |
| } |
| |
| ExplainCloseGroup("Slice Table", "Slice Table", false, 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); |
| } |
| |
| /* |
| * 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=%.ld\n", |
| 1000.0 * instr->total, instr->ntuples); |
| else |
| appendStringInfo(es->str, ": calls=%.ld\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); |
| } |
| |
| static void |
| show_dispatch_info(ExecSlice *slice, ExplainState *es, Plan *plan) |
| { |
| int segments; |
| |
| /* |
| * In non-parallel query, there is no slice information. |
| */ |
| if (!slice) |
| return; |
| |
| switch (slice->gangType) |
| { |
| case GANGTYPE_UNALLOCATED: |
| case GANGTYPE_ENTRYDB_READER: |
| segments = 0; |
| break; |
| |
| case GANGTYPE_PRIMARY_WRITER: |
| case GANGTYPE_PRIMARY_READER: |
| case GANGTYPE_SINGLETON_READER: |
| { |
| segments = list_length(slice->segments); |
| break; |
| } |
| |
| default: |
| segments = 0; /* keep compiler happy */ |
| Assert(false); |
| break; |
| } |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| { |
| if (segments == 0) |
| appendStringInfo(es->str, " (slice%d)", slice->sliceIndex); |
| else if (slice->primaryGang && gp_log_gang >= GPVARS_VERBOSITY_DEBUG) |
| /* |
| * In gpdb 5 there was a unique gang_id for each gang, this was |
| * retired since gpdb 6, so we use the qe identifier from the first |
| * segment of the gang to identify each gang. |
| */ |
| appendStringInfo(es->str, " (slice%d; gang%d; segments: %d)", |
| slice->sliceIndex, |
| slice->primaryGang->db_descriptors[0]->identifier, |
| segments); |
| else |
| appendStringInfo(es->str, " (slice%d; segments: %d)", |
| slice->sliceIndex, segments); |
| } |
| else |
| { |
| ExplainPropertyInteger("Slice", NULL, slice->sliceIndex, es); |
| if (slice->primaryGang && gp_log_gang >= GPVARS_VERBOSITY_DEBUG) |
| ExplainPropertyInteger("Gang", NULL, slice->primaryGang->db_descriptors[0]->identifier, es); |
| ExplainPropertyInteger("Segments", NULL, segments, es); |
| ExplainPropertyText("Gang Type", gangTypeToString(slice->gangType), es); |
| } |
| } |
| |
| /* |
| * 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_DynamicBitmapHeapScan: |
| 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: |
| case T_DynamicSeqScan: |
| case T_DynamicIndexScan: |
| case T_DynamicIndexOnlyScan: |
| case T_ShareInputScan: |
| *rels_used = bms_add_member(*rels_used, |
| ((Scan *) plan)->scanrelid); |
| break; |
| case T_ForeignScan: |
| case T_DynamicForeignScan: |
| *rels_used = bms_add_members(*rels_used, |
| ((ForeignScan *) plan)->fs_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. |
| * |
| * es->parentPlanState points to the parent planstate node and can be used by |
| * PartitionSelector to deparse its printablePredicate. (This is passed in |
| * ExplainState rather than as a normal argument, to avoid changing the |
| * function signature from upstream.) |
| */ |
| static void |
| ExplainNode(PlanState *planstate, List *ancestors, |
| const char *relationship, const char *plan_name, |
| ExplainState *es) |
| { |
| Plan *plan = planstate->plan; |
| PlanState *parentplanstate; |
| ExecSlice *save_currentSlice = es->currentSlice; /* save */ |
| 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; |
| bool skip_outer=false; |
| char *skip_outer_msg = NULL; |
| int motion_recv; |
| int motion_snd; |
| ExecSlice *parentSlice = NULL; |
| |
| /* Remember who called us. */ |
| parentplanstate = es->parentPlanState; |
| es->parentPlanState = planstate; |
| |
| /* |
| * If this is a Motion node, we're descending into a new slice. |
| */ |
| if (IsA(plan, Motion)) |
| { |
| Motion *pMotion = (Motion *) plan; |
| SliceTable *sliceTable = planstate->state->es_sliceTable; |
| |
| if (sliceTable) |
| { |
| es->currentSlice = &sliceTable->slices[pMotion->motionID]; |
| parentSlice = es->currentSlice->parentIndex == -1 ? NULL : |
| &sliceTable->slices[es->currentSlice->parentIndex]; |
| } |
| } |
| |
| /* |
| * 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; |
| 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_Sequence: |
| pname = sname = "Sequence"; |
| break; |
| case T_BitmapAnd: |
| pname = sname = "BitmapAnd"; |
| break; |
| case T_BitmapOr: |
| pname = sname = "BitmapOr"; |
| break; |
| case T_NestLoop: |
| pname = sname = "Nested Loop"; |
| if (((NestLoop *)plan)->shared_outer) |
| { |
| skip_outer = true; |
| skip_outer_msg = "See first subplan of Hash Join"; |
| } |
| 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_DynamicSeqScan: |
| pname = sname = "Dynamic 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_DynamicIndexScan: |
| pname = sname = "Dynamic Index Scan"; |
| break; |
| case T_DynamicIndexOnlyScan: |
| pname = sname = "Dynamic Index Only Scan"; |
| break; |
| case T_IndexOnlyScan: |
| pname = sname = "Index Only Scan"; |
| break; |
| case T_BitmapIndexScan: |
| pname = sname = "Bitmap Index Scan"; |
| break; |
| case T_DynamicBitmapIndexScan: |
| pname = sname = "Dynamic Bitmap Index Scan"; |
| break; |
| case T_BitmapHeapScan: |
| /* |
| * We print "Bitmap Heap Scan", even for AO tables. It's a bit |
| * confusing, but that's what the plan node is called, regardless |
| * of the table type. |
| */ |
| pname = sname = "Bitmap Heap Scan"; |
| break; |
| case T_DynamicBitmapHeapScan: |
| pname = sname = "Dynamic 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_ShareInputScan: |
| pname = sname = "Shared 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_DynamicForeignScan: |
| sname = "Dynamic Foreign Scan"; |
| switch (((ForeignScan *)((DynamicForeignScan *) plan))->operation) |
| { |
| case CMD_SELECT: |
| pname = "Dynamic Foreign Scan"; |
| operation = "Select"; |
| break; |
| case CMD_INSERT: |
| pname = "Dynamic Foreign Insert"; |
| operation = "Insert"; |
| break; |
| case CMD_UPDATE: |
| pname = "Dynamic Foreign Update"; |
| operation = "Update"; |
| break; |
| case CMD_DELETE: |
| pname = "Dynamic 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_TupleSplit: |
| pname = sname = "TupleSplit"; |
| 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"; |
| |
| if (agg->streaming) |
| pname = psprintf("Streaming %s", pname); |
| } |
| break; |
| case T_WindowAgg: |
| pname = sname = "WindowAgg"; |
| break; |
| case T_TableFunctionScan: |
| pname = sname = "Table Function Scan"; |
| 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_RuntimeFilter: |
| pname = sname = "RuntimeFilter"; |
| break; |
| case T_Limit: |
| pname = sname = "Limit"; |
| break; |
| case T_Hash: |
| pname = sname = "Hash"; |
| break; |
| case T_Motion: |
| { |
| Motion *pMotion = (Motion *) plan; |
| |
| Assert(plan->lefttree); |
| |
| motion_snd = list_length(es->currentSlice->segments); |
| |
| motion_recv = parentSlice == NULL ? 1 : list_length(parentSlice->segments); |
| |
| switch (pMotion->motionType) |
| { |
| case MOTIONTYPE_GATHER: |
| sname = "Gather Motion"; |
| motion_recv = 1; |
| break; |
| case MOTIONTYPE_GATHER_SINGLE: |
| sname = "Explicit Gather Motion"; |
| motion_recv = 1; |
| break; |
| case MOTIONTYPE_HASH: |
| sname = "Redistribute Motion"; |
| break; |
| case MOTIONTYPE_BROADCAST: |
| sname = "Broadcast Motion"; |
| break; |
| case MOTIONTYPE_BROADCAST_WORKERS: |
| sname = "Broadcast Workers Motion"; |
| break; |
| case MOTIONTYPE_EXPLICIT: |
| sname = "Explicit Redistribute Motion"; |
| break; |
| default: |
| sname = "???"; |
| motion_recv = -1; |
| break; |
| } |
| |
| pname = psprintf("%s %d:%d", sname, motion_snd, motion_recv); |
| } |
| break; |
| case T_SplitUpdate: |
| pname = sname = "Split"; |
| break; |
| case T_AssertOp: |
| pname = sname = "Assert"; |
| break; |
| case T_PartitionSelector: |
| pname = sname = "Partition Selector"; |
| 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", plan_name); |
| |
| /* |
| * If this SubPlan is being dispatched separately, show slice |
| * information after the plan name. Currently, we do this for |
| * Init Plans. |
| * |
| * Note: If the top node was a Motion node, we print the slice |
| * *above* the Motion here. We will print the slice below the |
| * Motion, below. |
| */ |
| if (es->subplanDispatchedSeparately) |
| show_dispatch_info(save_currentSlice, es, plan); |
| appendStringInfoChar(es->str, '\n'); |
| 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); |
| |
| /* |
| * Print information about the current slice. In order to not make |
| * the output too verbose, only print it at the slice boundaries, |
| * ie. at Motion nodes. (We already switched the "current slice" |
| * to the slice below the Motion.) |
| */ |
| if (IsA(plan, Motion)) |
| show_dispatch_info(es->currentSlice, es, plan); |
| |
| es->indent++; |
| } |
| else |
| { |
| ExplainPropertyText("Node Type", sname, es); |
| if (nodeTag(plan) == T_Motion) |
| { |
| ExplainPropertyInteger("Senders", NULL, motion_snd, es); |
| ExplainPropertyInteger("Receivers", NULL, motion_recv, 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); |
| |
| show_dispatch_info(es->currentSlice, es, plan); |
| ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es); |
| ExplainPropertyBool("Async Capable", plan->async_capable, es); |
| } |
| |
| switch (nodeTag(plan)) |
| { |
| case T_SeqScan: |
| case T_DynamicSeqScan: |
| case T_SampleScan: |
| case T_BitmapHeapScan: |
| case T_DynamicBitmapHeapScan: |
| case T_TidScan: |
| case T_TidRangeScan: |
| case T_SubqueryScan: |
| case T_FunctionScan: |
| case T_TableFunctionScan: |
| case T_TableFuncScan: |
| case T_ValuesScan: |
| case T_CteScan: |
| case T_WorkTableScan: |
| ExplainScanTarget((Scan *) plan, es); |
| break; |
| case T_ForeignScan: |
| case T_DynamicForeignScan: |
| 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_DynamicIndexScan: |
| { |
| DynamicIndexScan *dynamicIndexScan = (DynamicIndexScan *) plan; |
| Oid indexoid = dynamicIndexScan->indexscan.indexid; |
| const char *indexname = |
| explain_get_index_name(indexoid); |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| appendStringInfo(es->str, " on %s", indexname); |
| else |
| ExplainPropertyText("Index Name", indexname, es); |
| |
| ExplainScanTarget((Scan *) plan, es); |
| } |
| break; |
| case T_DynamicIndexOnlyScan: |
| { |
| DynamicIndexOnlyScan *dynamicIndexScan = (DynamicIndexOnlyScan *) plan; |
| Oid indexoid = dynamicIndexScan->indexscan.indexid; |
| const char *indexname = |
| explain_get_index_name(indexoid); |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| appendStringInfo(es->str, " on %s", indexname); |
| else |
| ExplainPropertyText("Index Name", indexname, es); |
| |
| ExplainScanTarget((Scan *) plan, es); |
| } |
| break; |
| case T_DynamicBitmapIndexScan: |
| { |
| BitmapIndexScan *bitmapindexscan = (BitmapIndexScan *) plan; |
| Oid indexoid = bitmapindexscan->indexid; |
| const char *indexname = |
| explain_get_index_name(indexoid); |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| appendStringInfo(es->str, " on %s", 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_LASJ_NOTIN: |
| jointype = "Left Anti Semi (Not-In)"; |
| 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; |
| case T_ShareInputScan: |
| { |
| ShareInputScan *sisc = (ShareInputScan *) plan; |
| int slice_id = -1; |
| |
| if (es->currentSlice) |
| slice_id = es->currentSlice->sliceIndex; |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| appendStringInfo(es->str, " (share slice:id %d:%d)", |
| slice_id, sisc->share_id); |
| else |
| { |
| ExplainPropertyInteger("Share ID", NULL, sisc->share_id, es); |
| ExplainPropertyInteger("Slice ID", NULL, slice_id, es); |
| } |
| } |
| break; |
| case T_PartitionSelector: |
| { |
| PartitionSelector *ps = (PartitionSelector *) plan; |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| { |
| appendStringInfo(es->str, " (selector id: $%d)", ps->paramid); |
| } |
| else |
| { |
| ExplainPropertyInteger("Selector ID", NULL, ps->paramid, 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); |
| } |
| } |
| |
| if (ResManagerPrintOperatorMemoryLimits()) |
| appendStringInfo(es->str, " (operatorMem: "UINT64_FORMAT"kB)", PlanStateOperatorMemKB(planstate)); |
| /* |
| * 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); |
| |
| /* GPDB_90_MERGE_FIXME: In GPDB, these are printed differently. But does that work |
| * with the new XML/YAML EXPLAIN output */ |
| 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); |
| |
| /* explain(locus) doesn't support Orca yet */ |
| if (es->locus && !optimizer) |
| { |
| Explainlocus(es, plan->locustype, plan->parallel); |
| } |
| |
| /* 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: |
| case T_DynamicIndexScan: |
| 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); |
| if (IsA(plan, DynamicIndexScan)) { |
| char *buf; |
| Oid relid; |
| relid = rt_fetch(((DynamicIndexScan *)plan) |
| ->indexscan.scan.scanrelid, |
| es->rtable)->relid; |
| buf = psprintf("(out of %d)", countLeafPartTables(relid)); |
| ExplainPropertyInteger( |
| "Number of partitions to scan", buf, |
| list_length(((DynamicIndexScan *)plan)->partOids), |
| es); |
| } |
| break; |
| case T_IndexOnlyScan: |
| case T_DynamicIndexOnlyScan: |
| 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); |
| if (IsA(plan, DynamicIndexOnlyScan)) { |
| char *buf; |
| Oid relid; |
| relid = rt_fetch(((DynamicIndexOnlyScan *)plan) |
| ->indexscan.scan.scanrelid, |
| es->rtable)->relid; |
| buf = psprintf("(out of %d)", countLeafPartTables(relid)); |
| ExplainPropertyInteger( |
| "Number of partitions to scan", buf, |
| list_length(((DynamicIndexOnlyScan *)plan)->partOids), |
| es); |
| } |
| break; |
| case T_BitmapIndexScan: |
| case T_DynamicBitmapIndexScan: |
| show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig, |
| "Index Cond", planstate, ancestors, es); |
| break; |
| case T_BitmapHeapScan: |
| case T_DynamicBitmapHeapScan: |
| { |
| List *bitmapqualorig; |
| |
| if (IsA(plan, DynamicBitmapHeapScan)) { |
| char *buf; |
| Oid relid; |
| relid = rt_fetch(((DynamicBitmapHeapScan *)plan) |
| ->bitmapheapscan.scan.scanrelid, |
| es->rtable)->relid; |
| buf = psprintf("(out of %d)", countLeafPartTables(relid)); |
| ExplainPropertyInteger( |
| "Number of partitions to scan", buf, |
| list_length( |
| ((DynamicBitmapHeapScan *)plan)->partOids), |
| es); |
| } |
| bitmapqualorig = ((BitmapHeapScan *) plan)->bitmapqualorig; |
| |
| show_scan_qual(bitmapqualorig, |
| "Recheck Cond", planstate, ancestors, es); |
| |
| if (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: |
| if (gp_enable_runtime_filter_pushdown && IsA(planstate, SeqScanState)) |
| show_pushdown_runtime_filter_info(planstate, es); |
| /* FALLTHROUGH */ |
| case T_DynamicSeqScan: |
| case T_ValuesScan: |
| case T_CteScan: |
| case T_NamedTuplestoreScan: |
| case T_WorkTableScan: |
| case T_SubqueryScan: |
| if (IsA(plan, DynamicSeqScan)) { |
| if (gp_enable_runtime_filter_pushdown) |
| show_pushdown_runtime_filter_info(planstate, es); |
| |
| char *buf; |
| Oid relid; |
| relid = rt_fetch(((DynamicSeqScan *)plan) |
| ->seqscan.scanrelid, |
| es->rtable)->relid; |
| buf = psprintf("(out of %d)", countLeafPartTables(relid)); |
| ExplainPropertyInteger( |
| "Number of partitions to scan", buf, |
| list_length(((DynamicSeqScan *)plan)->partOids),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_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_DynamicForeignScan: |
| 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); |
| if (IsA(plan, DynamicForeignScan)) |
| { |
| char *buf; |
| Oid relid; |
| relid = rt_fetch(((DynamicForeignScan *)plan) |
| ->foreignscan.scan.scanrelid, |
| es->rtable)->relid; |
| buf = psprintf("(out of %d)", countLeafPartTables(relid)); |
| ExplainPropertyInteger( |
| "Number of partitions to scan", buf, |
| list_length(((DynamicForeignScan *)plan)->partOids),es); |
| // TODO: Maybe add show_foreignscan_info here? We'd need to populate the planstate |
| } |
| else |
| { |
| 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: |
| { |
| HashJoin *hash_join = (HashJoin *) plan; |
| /* |
| * In the case of an "IS NOT DISTINCT" condition, we display |
| * hashqualclauses instead of hashclauses. |
| */ |
| List *cond_to_show = hash_join->hashclauses; |
| if (list_length(hash_join->hashqualclauses) > 0) |
| cond_to_show = hash_join->hashqualclauses; |
| |
| show_upper_qual(cond_to_show, |
| "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_TupleSplit: |
| show_tuple_split_keys((TupleSplitState *)planstate, ancestors, es); |
| break; |
| case T_Agg: |
| show_agg_keys(castNode(AggState, planstate), ancestors, es); |
| show_upper_qual(plan->qual, "Filter", planstate, ancestors, es); |
| show_aggref_info(plan->targetlist, "AggRefs(TargetList)", es); |
| show_hashagg_info((AggState *) planstate, es); |
| if (plan->qual) |
| show_instrumentation_count("Rows Removed by Filter", 1, |
| planstate, es); |
| break; |
| #if 0 /* Group node has been disabled in GPDB */ |
| 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; |
| #endif |
| case T_WindowAgg: |
| show_windowagg_keys((WindowAggState *) planstate, ancestors, es); |
| break; |
| case T_TableFunctionScan: |
| show_scan_qual(plan->qual, "Filter", planstate, ancestors, es); |
| /* TODO: Partitioning and ordering information */ |
| break; |
| case T_Unique: |
| show_motion_keys(planstate, |
| NIL, |
| ((Unique *) plan)->numCols, |
| ((Unique *) plan)->uniqColIdx, |
| "Group Key", |
| ancestors, 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_RuntimeFilter: |
| show_runtime_filter_info(castNode(RuntimeFilterState, planstate), |
| es); |
| break; |
| case T_Motion: |
| { |
| Motion *pMotion = (Motion *) plan; |
| |
| if (pMotion->sendSorted || pMotion->motionType == MOTIONTYPE_HASH) |
| show_motion_keys(planstate, |
| pMotion->hashExprs, |
| pMotion->numSortCols, |
| pMotion->sortColIdx, |
| "Merge Key", |
| ancestors, es); |
| if (pMotion->motionType == MOTIONTYPE_HASH && |
| pMotion->numHashSegments != motion_recv) |
| { |
| AssertImply(pMotion->senderSliceInfo && pMotion->senderSliceInfo->parallel_workers <= 1, |
| pMotion->numHashSegments < motion_recv); |
| ExplainPropertyInteger("Hash Module", NULL, |
| pMotion->numHashSegments, es); |
| } |
| } |
| break; |
| case T_AssertOp: |
| show_upper_qual(plan->qual, "Assert Cond", planstate, ancestors, es); |
| break; |
| case T_Append: |
| show_join_pruning_info(((Append *) plan)->join_prune_paramids, es); |
| break; |
| case T_Memoize: |
| show_memoize_info(castNode(MemoizeState, planstate), ancestors, |
| es); |
| break; |
| default: |
| break; |
| } |
| |
| /* Show executor statistics */ |
| if (planstate->instrument && planstate->instrument->need_cdb) |
| cdbexplain_showExecStats(planstate, es); |
| |
| /* |
| * 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, Sequence) || |
| 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 (plan->initPlan) |
| ExplainSubPlans(planstate->initPlan, ancestors, "InitPlan", es, planstate->state->es_sliceTable); |
| |
| /* lefttree */ |
| if (outerPlan(plan) && !skip_outer) |
| { |
| ExplainNode(outerPlanState(planstate), ancestors, |
| "Outer", NULL, es); |
| } |
| else if (skip_outer) |
| { |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| appendStringInfo(es->str, " -> "); |
| appendStringInfoString(es->str, skip_outer_msg); |
| appendStringInfo(es->str, "\n"); |
| } |
| |
| /* 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_Sequence: |
| ExplainMemberNodes(((SequenceState *) planstate)->subplans, |
| ((SequenceState *) planstate)->numSubplans, |
| 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, NULL); |
| |
| /* 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); |
| |
| es->currentSlice = save_currentSlice; |
| } |
| |
| /* |
| * 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; |
| const char *SortKeystr; |
| |
| SortKeystr = "Sort Key"; |
| |
| show_sort_group_keys((PlanState *) sortstate, SortKeystr, |
| plan->numCols, 0, plan->sortColIdx, |
| plan->sortOperators, plan->collations, |
| plan->nullsFirst, |
| ancestors, es); |
| } |
| |
| static void |
| show_aggref_info(List *targetList, const char *qlabel, ExplainState *es) { |
| StringInfoData buf; |
| ListCell *lc; |
| bool already_got_one = false; |
| |
| if (!Debug_print_aggref_in_explain) { |
| return; |
| } |
| |
| initStringInfo(&buf); |
| appendStringInfoString(&buf, "["); |
| |
| foreach (lc, targetList) |
| { |
| if (!IsA(lfirst(lc),TargetEntry)) { |
| continue; |
| } |
| |
| TargetEntry *te = (TargetEntry *) lfirst(lc); |
| if (!IsA(te->expr, Aggref)) |
| { |
| continue; |
| } |
| |
| Aggref *aggref = (Aggref *) te->expr; |
| if (already_got_one) { |
| appendStringInfo(&buf, ", (%d, %d)", aggref->aggno, aggref->aggtransno); |
| } else { |
| appendStringInfo(&buf, "(%d, %d)", aggref->aggno, aggref->aggtransno); |
| already_got_one = true; |
| } |
| } |
| appendStringInfoString(&buf, "]"); |
| |
| ExplainPropertyText(qlabel, buf.data, es); |
| } |
| |
| static void |
| show_windowagg_keys(WindowAggState *waggstate, List *ancestors, ExplainState *es) |
| { |
| WindowAgg *window = (WindowAgg *) waggstate->ss.ps.plan; |
| |
| /* The key columns refer to the tlist of the child plan */ |
| ancestors = lcons(window, ancestors); |
| if ( window->partNumCols > 0 ) |
| { |
| show_sort_group_keys((PlanState *) outerPlanState(waggstate), "Partition By", |
| window->partNumCols, 0, window->partColIdx, |
| NULL, NULL, NULL, |
| ancestors, es); |
| } |
| |
| show_sort_group_keys((PlanState *) outerPlanState(waggstate), "Order By", |
| window->ordNumCols, 0, window->ordColIdx, |
| NULL, NULL, NULL, |
| ancestors, es); |
| ancestors = list_delete_first(ancestors); |
| |
| /* XXX don't show framing for now */ |
| } |
| |
| |
| |
| /* |
| * 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 Split key for an SplitTuple |
| */ |
| static void |
| show_tuple_split_keys(TupleSplitState *tstate, List *ancestors, |
| ExplainState *es) |
| { |
| TupleSplit *plan = (TupleSplit *)tstate->ss.ps.plan; |
| |
| ancestors = lcons(tstate, ancestors); |
| |
| List *context; |
| bool useprefix; |
| List *result = NIL; |
| /* Set up deparsing context */ |
| context = set_deparse_context_plan(es->deparse_cxt, |
| (Plan *) plan, |
| ancestors); |
| useprefix = (list_length(es->rtable) > 1 || es->verbose); |
| |
| StringInfoData buf; |
| initStringInfo(&buf); |
| |
| ListCell *lc; |
| foreach(lc, plan->dqa_expr_lst) |
| { |
| DQAExpr *dqa_expr = (DQAExpr *)lfirst(lc); |
| result = lappend(result, |
| deparse_expression((Node *) dqa_expr, context, |
| useprefix, true)); |
| } |
| ExplainPropertyList("Split by Col", result, es); |
| |
| if (plan->numCols > 0) |
| show_sort_group_keys(outerPlanState(tstate), "Group Key", |
| plan->numCols, 0, plan->grpColIdx, |
| NULL, NULL, NULL, |
| ancestors, es); |
| |
| ancestors = list_delete_first(ancestors); |
| } |
| |
| /* |
| * 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. |
| */ |
| #if 0 |
| 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); |
| } |
| #endif |
| |
| /* |
| * 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); |
| |
| /* |
| * GPDB_90_MERGE_FIXME: handle rollup times printing |
| * if (rollup_gs_times > 1) |
| * appendStringInfo(es->str, " (%d times)", rollup_gs_times); |
| */ |
| 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 |
| * |
| * GPDB_90_MERGE_FIXME: The sort statistics are stored quite differently from |
| * upstream, it would be nice to rewrite this to avoid looping over all the |
| * sort methods and instead have a _get_stats() function as in upstream. |
| */ |
| static void |
| show_sort_info(SortState *sortstate, ExplainState *es) |
| { |
| CdbExplain_NodeSummary *ns; |
| int i; |
| |
| if (!es->analyze) |
| return; |
| |
| ns = ((PlanState *) sortstate)->instrument->cdbNodeSummary; |
| if (!ns) |
| return; |
| |
| for (i = 0; i < NUM_SORT_METHOD; i++) |
| { |
| CdbExplain_Agg *agg; |
| const char *sortMethod; |
| const char *spaceType; |
| int j; |
| |
| /* |
| * Memory and disk usage statistics are saved separately in GPDB so |
| * need to pull out the one in question first |
| */ |
| for (j = 0; j < NUM_SORT_SPACE_TYPE; j++) |
| { |
| agg = &ns->sortSpaceUsed[j][i]; |
| |
| if (agg->vcnt > 0) |
| break; |
| } |
| /* |
| * If the current sort method in question hasn't been used, skip to |
| * next one |
| */ |
| if (j >= NUM_SORT_SPACE_TYPE) |
| continue; |
| |
| sortMethod = tuplesort_method_name(i); |
| spaceType = tuplesort_space_type_name(j); |
| |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| { |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| appendStringInfo(es->str, "Sort Method: %s %s: " INT64_FORMAT "kB\n", |
| sortMethod, spaceType, (long) agg->vsum); |
| if (es->verbose) |
| { |
| appendStringInfo(es->str, " Max Memory: " INT64_FORMAT "kB Avg Memory: " INT64_FORMAT "kB (%d segments)\n", |
| (long) agg->vmax, |
| (long) (agg->vsum / agg->vcnt), |
| agg->vcnt); |
| } |
| } |
| else |
| { |
| ExplainPropertyText("Sort Method", sortMethod, es); |
| ExplainPropertyInteger("Sort Space Used", "kB", agg->vsum, es); |
| ExplainPropertyText("Sort Space Type", spaceType, es); |
| if (es->verbose) |
| { |
| ExplainPropertyInteger("Sort Max Segment Memory", "kB", agg->vmax, es); |
| ExplainPropertyInteger("Sort Avg Segment Memory", "kB", (agg->vsum / agg->vcnt), es); |
| ExplainPropertyInteger("Sort Segments", NULL, agg->vcnt, 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); |
| } |
| } |
| } |
| |
| static void |
| show_runtime_filter_info(RuntimeFilterState *rfstate, ExplainState *es) |
| { |
| if (es->analyze) |
| { |
| if (rfstate->bf != NULL) |
| ExplainPropertyUInteger("Bloom Bits", NULL, |
| bloom_total_bits(rfstate->bf), es); |
| } |
| } |
| |
| /* |
| * 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 *seperator = ""; |
| 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, seperator); |
| |
| appendStringInfoString(&keystr, deparse_expression(expr, context, |
| useprefix, false)); |
| seperator = ", "; |
| } |
| |
| 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 |
| appendStringInfoString(es->str, " "); |
| |
| 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 (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); |
| } |
| } |
| |
| /* |
| * If it's EXPLAIN ANALYZE, show instrumentation information with pushdown |
| * runtime filter. |
| */ |
| static void |
| show_pushdown_runtime_filter_info(PlanState *planstate, ExplainState *es) |
| { |
| Assert(gp_enable_runtime_filter_pushdown && |
| (IsA(planstate, SeqScanState) || IsA(planstate, DynamicSeqScanState))); |
| |
| if (!es->analyze || !planstate->instrument) |
| return; |
| |
| if (planstate->instrument->prf_work) |
| { |
| ExplainPropertyFloat("Rows Removed by Pushdown Runtime Filter", |
| NULL, planstate->instrument->nfilteredPRF, 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); |
| } |
| |
| static void |
| show_join_pruning_info(List *join_prune_ids, ExplainState *es) |
| { |
| List *params = NIL; |
| ListCell *lc; |
| |
| if (!join_prune_ids) |
| return; |
| |
| foreach(lc, join_prune_ids) |
| { |
| int paramid = lfirst_int(lc); |
| char param[32]; |
| |
| snprintf(param, sizeof(param), "$%d", paramid); |
| params = lappend(params, pstrdup(param)); |
| } |
| |
| ExplainPropertyList("Partition Selectors", 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 show_planning = (planning && (has_shared || |
| has_local || has_temp || has_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) |
| { |
| ExplainIndentText(es); |
| appendStringInfoString(es->str, "I/O Timings:"); |
| 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)); |
| 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); |
| } |
| } |
| } |
| |
| /* |
| * 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 NoMovementScanDirection: |
| scandir = "NoMovement"; |
| 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; |
| int dynamicScanId = 0; |
| |
| 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_DynamicSeqScan: |
| case T_SampleScan: |
| case T_IndexScan: |
| case T_DynamicIndexScan: |
| case T_DynamicIndexOnlyScan: |
| case T_IndexOnlyScan: |
| case T_BitmapHeapScan: |
| case T_DynamicBitmapHeapScan: |
| case T_TidScan: |
| case T_TidRangeScan: |
| case T_ForeignScan: |
| case T_DynamicForeignScan: |
| 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(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(get_func_namespace(funcid)); |
| } |
| } |
| objecttag = "Function Name"; |
| } |
| break; |
| case T_TableFunctionScan: |
| { |
| TableFunctionScan *fscan = (TableFunctionScan *) plan; |
| |
| /* Assert it's on a RangeFunction */ |
| Assert(rte->rtekind == RTE_TABLEFUNCTION); |
| |
| /* |
| * Unlike in a FunctionScan, in a TableFunctionScan the call |
| * should always be a function call of a single function. |
| * Get the real name of the function. |
| */ |
| { |
| RangeTblFunction *rtfunc = fscan->function; |
| |
| 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(get_func_namespace(funcid)); |
| } |
| } |
| objecttag = "Function Name"; |
| |
| /* might be nice to add order by and scatter by info, if it's a TableFunctionScan */ |
| } |
| 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)); |
| |
| if (dynamicScanId != 0) |
| appendStringInfo(es->str, " (dynamic scan id: %d)", |
| dynamicScanId); |
| } |
| else |
| { |
| if (objecttag != NULL && objectname != NULL) |
| ExplainPropertyText(objecttag, objectname, es); |
| if (namespace != NULL) |
| ExplainPropertyText("Schema", namespace, es); |
| ExplainPropertyText("Alias", refname, es); |
| |
| if (dynamicScanId != 0) |
| ExplainPropertyInteger("Dynamic Scan Id", NULL, dynamicScanId, 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; |
| 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); |
| } |
| } |
| |
| 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, |
| SliceTable *sliceTable) |
| { |
| ListCell *lst; |
| ExecSlice *saved_slice = es->currentSlice; |
| |
| foreach(lst, plans) |
| { |
| SubPlanState *sps = (SubPlanState *) lfirst(lst); |
| SubPlan *sp = sps->subplan; |
| int qDispSliceId; |
| |
| if (es->pstmt->subplan_sliceIds) |
| qDispSliceId = es->pstmt->subplan_sliceIds[sp->plan_id - 1]; |
| else |
| qDispSliceId = -1; |
| |
| /* |
| * 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); |
| |
| /* Subplan might have its own root slice */ |
| if (sliceTable && qDispSliceId > 0) |
| { |
| es->currentSlice = &sliceTable->slices[qDispSliceId]; |
| es->subplanDispatchedSeparately = true; |
| } |
| else |
| es->subplanDispatchedSeparately = false; |
| |
| if (sps->planstate == NULL) |
| { |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| appendStringInfo(es->str, " -> "); |
| appendStringInfo(es->str, "UNUSED %s", sp->plan_name); |
| appendStringInfo(es->str, "\n"); |
| } |
| else |
| { |
| /* |
| * 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); |
| } |
| } |
| |
| es->currentSlice = saved_slice; |
| } |
| |
| /* |
| * Explain a list of children of a CustomScan. |
| */ |
| static void |
| ExplainCustomChildren(CustomScanState *css, List *ancestors, ExplainState *es) |
| { |
| ListCell *cell; |
| const char *label = |
| (list_length(css->custom_ps) != 1 ? "children" : "child"); |
| |
| foreach(cell, css->custom_ps) |
| ExplainNode((PlanState *) lfirst(cell), ancestors, label, NULL, es); |
| } |
| |
| /* |
| * Create a per-plan-node workspace for collecting per-worker data. |
| * |
| * Output related to each worker will be temporarily "set aside" into a |
| * separate buffer, which we'll merge into the main output stream once |
| * we've processed all data for the plan node. This makes it feasible to |
| * generate a coherent sub-group of fields for each worker, even though the |
| * code that produces the fields is in several different places in this file. |
| * Formatting of such a set-aside field group is managed by |
| * ExplainOpenSetAsideGroup and ExplainSaveGroup/ExplainRestoreGroup. |
| */ |
| static ExplainWorkersState * |
| ExplainCreateWorkersState(int num_workers) |
| { |
| ExplainWorkersState *wstate; |
| |
| wstate = (ExplainWorkersState *) palloc(sizeof(ExplainWorkersState)); |
| wstate->num_workers = num_workers; |
| wstate->worker_inited = (bool *) palloc0(num_workers * sizeof(bool)); |
| wstate->worker_str = (StringInfoData *) |
| palloc0(num_workers * sizeof(StringInfoData)); |
| wstate->worker_state_save = (int *) palloc(num_workers * sizeof(int)); |
| return wstate; |
| } |
| |
| /* |
| * Begin or resume output into the set-aside group for worker N. |
| */ |
| static void |
| ExplainOpenWorker(int n, ExplainState *es) |
| { |
| ExplainWorkersState *wstate = es->workers_state; |
| |
| Assert(wstate); |
| Assert(n >= 0 && n < wstate->num_workers); |
| |
| /* Save prior output buffer pointer */ |
| wstate->prev_str = es->str; |
| |
| if (!wstate->worker_inited[n]) |
| { |
| /* First time through, so create the buffer for this worker */ |
| initStringInfo(&wstate->worker_str[n]); |
| es->str = &wstate->worker_str[n]; |
| |
| /* |
| * Push suitable initial formatting state for this worker's field |
| * group. We allow one extra logical nesting level, since this group |
| * will eventually be wrapped in an outer "Workers" group. |
| */ |
| ExplainOpenSetAsideGroup("Worker", NULL, true, 2, es); |
| |
| /* |
| * In non-TEXT formats we always emit a "Worker Number" field, even if |
| * there's no other data for this worker. |
| */ |
| if (es->format != EXPLAIN_FORMAT_TEXT) |
| ExplainPropertyInteger("Worker Number", NULL, n, es); |
| |
| wstate->worker_inited[n] = true; |
| } |
| else |
| { |
| /* Resuming output for a worker we've already emitted some data for */ |
| es->str = &wstate->worker_str[n]; |
| |
| /* Restore formatting state saved by last ExplainCloseWorker() */ |
| ExplainRestoreGroup(es, 2, &wstate->worker_state_save[n]); |
| } |
| |
| /* |
| * In TEXT format, prefix the first output line for this worker with |
| * "Worker N:". Then, any additional lines should be indented one more |
| * stop than the "Worker N" line is. |
| */ |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| { |
| if (es->str->len == 0) |
| { |
| ExplainIndentText(es); |
| appendStringInfo(es->str, "Worker %d: ", n); |
| } |
| |
| es->indent++; |
| } |
| } |
| |
| /* |
| * End output for worker N --- must pair with previous ExplainOpenWorker call |
| */ |
| static void |
| ExplainCloseWorker(int n, ExplainState *es) |
| { |
| ExplainWorkersState *wstate = es->workers_state; |
| |
| Assert(wstate); |
| Assert(n >= 0 && n < wstate->num_workers); |
| Assert(wstate->worker_inited[n]); |
| |
| /* |
| * Save formatting state in case we do another ExplainOpenWorker(), then |
| * pop the formatting stack. |
| */ |
| ExplainSaveGroup(es, 2, &wstate->worker_state_save[n]); |
| |
| /* |
| * In TEXT format, if we didn't actually produce any output line(s) then |
| * truncate off the partial line emitted by ExplainOpenWorker. (This is |
| * to avoid bogus output if, say, show_buffer_usage chooses not to print |
| * anything for the worker.) Also fix up the indent level. |
| */ |
| if (es->format == EXPLAIN_FORMAT_TEXT) |
| { |
| while (es->str->len > 0 && es->str->data[es->str->len - 1] != '\n') |
| es->str->data[--(es->str->len)] = '\0'; |
| |
| es->indent--; |
| } |
| |
| /* Restore prior output buffer pointer */ |
| es->str = wstate->prev_str; |
| } |
| |
| /* |
| * Print per-worker info for current node, then free the ExplainWorkersState. |
| */ |
| static void |
| ExplainFlushWorkersState(ExplainState *es) |
| { |
| ExplainWorkersState *wstate = es->workers_state; |
| |
| ExplainOpenGroup("Workers", "Workers", false, es); |
| for (int i = 0; i < wstate->num_workers; i++) |
| { |
| if (wstate->worker_inited[i]) |
| { |
| /* This must match previous ExplainOpenSetAsideGroup call */ |
| ExplainOpenGroup("Worker", NULL, true, es); |
| appendStringInfoString(es->str, wstate->worker_str[i].data); |
| ExplainCloseGroup("Worker", NULL, true, es); |
| |
| pfree(wstate->worker_str[i].data); |
| } |
| } |
| ExplainCloseGroup("Workers", "Workers", false, es); |
| |
| pfree(wstate->worker_inited); |
| pfree(wstate->worker_str); |
| pfree(wstate->worker_state_save); |
| pfree(wstate); |
| } |
| |
| /* |
| * Explain a property, such as sort keys or targets, that takes the form of |
| * a list of unlabeled items. "data" is a list of C strings. |
| */ |
| void |
| ExplainPropertyList(const char *qlabel, List *data, ExplainState *es) |
| { |
| ListCell *lc; |
| bool first = true; |
| |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| ExplainIndentText(es); |
| appendStringInfo(es->str, "%s: ", qlabel); |
| foreach(lc, data) |
| { |
| if (!first) |
| appendStringInfoString(es->str, ", "); |
| appendStringInfoString(es->str, (const char *) lfirst(lc)); |
| first = false; |
| } |
| appendStringInfoChar(es->str, '\n'); |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| ExplainXMLTag(qlabel, X_OPENING, es); |
| foreach(lc, data) |
| { |
| char *str; |
| |
| appendStringInfoSpaces(es->str, es->indent * 2 + 2); |
| appendStringInfoString(es->str, "<Item>"); |
| str = escape_xml((const char *) lfirst(lc)); |
| appendStringInfoString(es->str, str); |
| pfree(str); |
| appendStringInfoString(es->str, "</Item>\n"); |
| } |
| ExplainXMLTag(qlabel, X_CLOSING, es); |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| ExplainJSONLineEnding(es); |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| escape_json(es->str, qlabel); |
| appendStringInfoString(es->str, ": ["); |
| foreach(lc, data) |
| { |
| if (!first) |
| appendStringInfoString(es->str, ", "); |
| escape_json(es->str, (const char *) lfirst(lc)); |
| first = false; |
| } |
| appendStringInfoChar(es->str, ']'); |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| ExplainYAMLLineStarting(es); |
| appendStringInfo(es->str, "%s: ", qlabel); |
| foreach(lc, data) |
| { |
| appendStringInfoChar(es->str, '\n'); |
| appendStringInfoSpaces(es->str, es->indent * 2 + 2); |
| appendStringInfoString(es->str, "- "); |
| escape_yaml(es->str, (const char *) lfirst(lc)); |
| } |
| break; |
| } |
| } |
| |
| /* |
| * Explain a property that takes the form of a list of unlabeled items within |
| * another list. "data" is a list of C strings. |
| */ |
| void |
| ExplainPropertyListNested(const char *qlabel, List *data, ExplainState *es) |
| { |
| ListCell *lc; |
| bool first = true; |
| |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| case EXPLAIN_FORMAT_XML: |
| ExplainPropertyList(qlabel, data, es); |
| return; |
| |
| case EXPLAIN_FORMAT_JSON: |
| ExplainJSONLineEnding(es); |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| appendStringInfoChar(es->str, '['); |
| foreach(lc, data) |
| { |
| if (!first) |
| appendStringInfoString(es->str, ", "); |
| escape_json(es->str, (const char *) lfirst(lc)); |
| first = false; |
| } |
| appendStringInfoChar(es->str, ']'); |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| ExplainYAMLLineStarting(es); |
| appendStringInfoString(es->str, "- ["); |
| foreach(lc, data) |
| { |
| if (!first) |
| appendStringInfoString(es->str, ", "); |
| escape_yaml(es->str, (const char *) lfirst(lc)); |
| first = false; |
| } |
| appendStringInfoChar(es->str, ']'); |
| break; |
| } |
| } |
| |
| /* |
| * Explain a simple property. |
| * |
| * If "numeric" is true, the value is a number (or other value that |
| * doesn't need quoting in JSON). |
| * |
| * If unit is non-NULL the text format will display it after the value. |
| * |
| * This usually should not be invoked directly, but via one of the datatype |
| * specific routines ExplainPropertyText, ExplainPropertyInteger, etc. |
| */ |
| static void |
| ExplainProperty(const char *qlabel, const char *unit, const char *value, |
| bool numeric, ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| ExplainIndentText(es); |
| if (unit) |
| appendStringInfo(es->str, "%s: %s %s\n", qlabel, value, unit); |
| else |
| appendStringInfo(es->str, "%s: %s\n", qlabel, value); |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| { |
| char *str; |
| |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| ExplainXMLTag(qlabel, X_OPENING | X_NOWHITESPACE, es); |
| str = escape_xml(value); |
| appendStringInfoString(es->str, str); |
| pfree(str); |
| ExplainXMLTag(qlabel, X_CLOSING | X_NOWHITESPACE, es); |
| appendStringInfoChar(es->str, '\n'); |
| } |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| ExplainJSONLineEnding(es); |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| escape_json(es->str, qlabel); |
| appendStringInfoString(es->str, ": "); |
| if (numeric) |
| appendStringInfoString(es->str, value); |
| else |
| escape_json(es->str, value); |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| ExplainYAMLLineStarting(es); |
| appendStringInfo(es->str, "%s: ", qlabel); |
| if (numeric) |
| appendStringInfoString(es->str, value); |
| else |
| escape_yaml(es->str, value); |
| break; |
| } |
| } |
| |
| static void |
| ExplainPropertyStringInfo(const char *qlabel, ExplainState *es, const char *fmt,...) |
| { |
| StringInfoData buf; |
| |
| initStringInfo(&buf); |
| |
| for (;;) |
| { |
| va_list args; |
| int needed; |
| |
| /* Try to format the data. */ |
| va_start(args, fmt); |
| needed = appendStringInfoVA(&buf, fmt, args); |
| va_end(args); |
| |
| if (needed == 0) |
| break; |
| |
| /* Double the buffer size and try again. */ |
| enlargeStringInfo(&buf, needed); |
| } |
| |
| ExplainPropertyText(qlabel, buf.data, es); |
| pfree(buf.data); |
| } |
| |
| /* |
| * Explain a string-valued property. |
| */ |
| void |
| ExplainPropertyText(const char *qlabel, const char *value, ExplainState *es) |
| { |
| ExplainProperty(qlabel, NULL, value, false, es); |
| } |
| |
| /* |
| * Explain an integer-valued property. |
| */ |
| void |
| ExplainPropertyInteger(const char *qlabel, const char *unit, int64 value, |
| ExplainState *es) |
| { |
| char buf[32]; |
| |
| snprintf(buf, sizeof(buf), INT64_FORMAT, value); |
| ExplainProperty(qlabel, unit, buf, true, es); |
| } |
| |
| /* |
| * Explain an unsigned integer-valued property. |
| */ |
| void |
| ExplainPropertyUInteger(const char *qlabel, const char *unit, uint64 value, |
| ExplainState *es) |
| { |
| char buf[32]; |
| |
| snprintf(buf, sizeof(buf), UINT64_FORMAT, value); |
| ExplainProperty(qlabel, unit, buf, true, es); |
| } |
| |
| /* |
| * Explain a float-valued property, using the specified number of |
| * fractional digits. |
| */ |
| void |
| ExplainPropertyFloat(const char *qlabel, const char *unit, double value, |
| int ndigits, ExplainState *es) |
| { |
| char *buf; |
| |
| buf = psprintf("%.*f", ndigits, value); |
| ExplainProperty(qlabel, unit, buf, true, es); |
| pfree(buf); |
| } |
| |
| /* |
| * Explain a bool-valued property. |
| */ |
| void |
| ExplainPropertyBool(const char *qlabel, bool value, ExplainState *es) |
| { |
| ExplainProperty(qlabel, NULL, value ? "true" : "false", true, es); |
| } |
| |
| /* |
| * Open a group of related objects. |
| * |
| * objtype is the type of the group object, labelname is its label within |
| * a containing object (if any). |
| * |
| * If labeled is true, the group members will be labeled properties, |
| * while if it's false, they'll be unlabeled objects. |
| */ |
| void |
| ExplainOpenGroup(const char *objtype, const char *labelname, |
| bool labeled, ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| ExplainXMLTag(objtype, X_OPENING, es); |
| es->indent++; |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| ExplainJSONLineEnding(es); |
| appendStringInfoSpaces(es->str, 2 * es->indent); |
| if (labelname) |
| { |
| escape_json(es->str, labelname); |
| appendStringInfoString(es->str, ": "); |
| } |
| appendStringInfoChar(es->str, labeled ? '{' : '['); |
| |
| /* |
| * In JSON format, the grouping_stack is an integer list. 0 means |
| * we've emitted nothing at this grouping level, 1 means we've |
| * emitted something (and so the next item needs a comma). See |
| * ExplainJSONLineEnding(). |
| */ |
| es->grouping_stack = lcons_int(0, es->grouping_stack); |
| es->indent++; |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| |
| /* |
| * In YAML format, the grouping stack is an integer list. 0 means |
| * we've emitted nothing at this grouping level AND this grouping |
| * level is unlabeled and must be marked with "- ". See |
| * ExplainYAMLLineStarting(). |
| */ |
| ExplainYAMLLineStarting(es); |
| if (labelname) |
| { |
| appendStringInfo(es->str, "%s: ", labelname); |
| es->grouping_stack = lcons_int(1, es->grouping_stack); |
| } |
| else |
| { |
| appendStringInfoString(es->str, "- "); |
| es->grouping_stack = lcons_int(0, es->grouping_stack); |
| } |
| es->indent++; |
| break; |
| } |
| } |
| |
| /* |
| * Close a group of related objects. |
| * Parameters must match the corresponding ExplainOpenGroup call. |
| */ |
| void |
| ExplainCloseGroup(const char *objtype, const char *labelname, |
| bool labeled, ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| es->indent--; |
| ExplainXMLTag(objtype, X_CLOSING, es); |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| es->indent--; |
| appendStringInfoChar(es->str, '\n'); |
| appendStringInfoSpaces(es->str, 2 * es->indent); |
| appendStringInfoChar(es->str, labeled ? '}' : ']'); |
| es->grouping_stack = list_delete_first(es->grouping_stack); |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| es->indent--; |
| es->grouping_stack = list_delete_first(es->grouping_stack); |
| break; |
| } |
| } |
| |
| /* |
| * Open a group of related objects, without emitting actual data. |
| * |
| * Prepare the formatting state as though we were beginning a group with |
| * the identified properties, but don't actually emit anything. Output |
| * subsequent to this call can be redirected into a separate output buffer, |
| * and then eventually appended to the main output buffer after doing a |
| * regular ExplainOpenGroup call (with the same parameters). |
| * |
| * The extra "depth" parameter is the new group's depth compared to current. |
| * It could be more than one, in case the eventual output will be enclosed |
| * in additional nesting group levels. We assume we don't need to track |
| * formatting state for those levels while preparing this group's output. |
| * |
| * There is no ExplainCloseSetAsideGroup --- in current usage, we always |
| * pop this state with ExplainSaveGroup. |
| */ |
| static void |
| ExplainOpenSetAsideGroup(const char *objtype, const char *labelname, |
| bool labeled, int depth, ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| es->indent += depth; |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| es->grouping_stack = lcons_int(0, es->grouping_stack); |
| es->indent += depth; |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| if (labelname) |
| es->grouping_stack = lcons_int(1, es->grouping_stack); |
| else |
| es->grouping_stack = lcons_int(0, es->grouping_stack); |
| es->indent += depth; |
| break; |
| } |
| } |
| |
| /* |
| * Pop one level of grouping state, allowing for a re-push later. |
| * |
| * This is typically used after ExplainOpenSetAsideGroup; pass the |
| * same "depth" used for that. |
| * |
| * This should not emit any output. If state needs to be saved, |
| * save it at *state_save. Currently, an integer save area is sufficient |
| * for all formats, but we might need to revisit that someday. |
| */ |
| static void |
| ExplainSaveGroup(ExplainState *es, int depth, int *state_save) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| es->indent -= depth; |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| es->indent -= depth; |
| *state_save = linitial_int(es->grouping_stack); |
| es->grouping_stack = list_delete_first(es->grouping_stack); |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| es->indent -= depth; |
| *state_save = linitial_int(es->grouping_stack); |
| es->grouping_stack = list_delete_first(es->grouping_stack); |
| break; |
| } |
| } |
| |
| /* |
| * Re-push one level of grouping state, undoing the effects of ExplainSaveGroup. |
| */ |
| static void |
| ExplainRestoreGroup(ExplainState *es, int depth, int *state_save) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| es->indent += depth; |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| es->grouping_stack = lcons_int(*state_save, es->grouping_stack); |
| es->indent += depth; |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| es->grouping_stack = lcons_int(*state_save, es->grouping_stack); |
| es->indent += depth; |
| break; |
| } |
| } |
| |
| /* |
| * Emit a "dummy" group that never has any members. |
| * |
| * objtype is the type of the group object, labelname is its label within |
| * a containing object (if any). |
| */ |
| static void |
| ExplainDummyGroup(const char *objtype, const char *labelname, ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| ExplainXMLTag(objtype, X_CLOSE_IMMEDIATE, es); |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| ExplainJSONLineEnding(es); |
| appendStringInfoSpaces(es->str, 2 * es->indent); |
| if (labelname) |
| { |
| escape_json(es->str, labelname); |
| appendStringInfoString(es->str, ": "); |
| } |
| escape_json(es->str, objtype); |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| ExplainYAMLLineStarting(es); |
| if (labelname) |
| { |
| escape_yaml(es->str, labelname); |
| appendStringInfoString(es->str, ": "); |
| } |
| else |
| { |
| appendStringInfoString(es->str, "- "); |
| } |
| escape_yaml(es->str, objtype); |
| break; |
| } |
| } |
| |
| /* |
| * Emit the start-of-output boilerplate. |
| * |
| * This is just enough different from processing a subgroup that we need |
| * a separate pair of subroutines. |
| */ |
| void |
| ExplainBeginOutput(ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| appendStringInfoString(es->str, |
| "<explain xmlns=\"http://www.postgresql.org/2009/explain\">\n"); |
| es->indent++; |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| /* top-level structure is an array of plans */ |
| appendStringInfoChar(es->str, '['); |
| es->grouping_stack = lcons_int(0, es->grouping_stack); |
| es->indent++; |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| es->grouping_stack = lcons_int(0, es->grouping_stack); |
| break; |
| } |
| } |
| |
| /* |
| * Emit the end-of-output boilerplate. |
| */ |
| void |
| ExplainEndOutput(ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* nothing to do */ |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| es->indent--; |
| appendStringInfoString(es->str, "</explain>"); |
| break; |
| |
| case EXPLAIN_FORMAT_JSON: |
| es->indent--; |
| appendStringInfoString(es->str, "\n]"); |
| es->grouping_stack = list_delete_first(es->grouping_stack); |
| break; |
| |
| case EXPLAIN_FORMAT_YAML: |
| es->grouping_stack = list_delete_first(es->grouping_stack); |
| break; |
| } |
| } |
| |
| /* |
| * Put an appropriate separator between multiple plans |
| */ |
| void |
| ExplainSeparatePlans(ExplainState *es) |
| { |
| switch (es->format) |
| { |
| case EXPLAIN_FORMAT_TEXT: |
| /* add a blank line */ |
| appendStringInfoChar(es->str, '\n'); |
| break; |
| |
| case EXPLAIN_FORMAT_XML: |
| case EXPLAIN_FORMAT_JSON: |
| case EXPLAIN_FORMAT_YAML: |
| /* nothing to do */ |
| break; |
| } |
| } |
| |
| /* |
| * Emit opening or closing XML tag. |
| * |
| * "flags" must contain X_OPENING, X_CLOSING, or X_CLOSE_IMMEDIATE. |
| * Optionally, OR in X_NOWHITESPACE to suppress the whitespace we'd normally |
| * add. |
| * |
| * XML restricts tag names more than our other output formats, eg they can't |
| * contain white space or slashes. Replace invalid characters with dashes, |
| * so that for example "I/O Read Time" becomes "I-O-Read-Time". |
| */ |
| static void |
| ExplainXMLTag(const char *tagname, int flags, ExplainState *es) |
| { |
| const char *s; |
| const char *valid = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_."; |
| |
| if ((flags & X_NOWHITESPACE) == 0) |
| appendStringInfoSpaces(es->str, 2 * es->indent); |
| appendStringInfoCharMacro(es->str, '<'); |
| if ((flags & X_CLOSING) != 0) |
| appendStringInfoCharMacro(es->str, '/'); |
| for (s = tagname; *s; s++) |
| appendStringInfoChar(es->str, strchr(valid, *s) ? *s : '-'); |
| if ((flags & X_CLOSE_IMMEDIATE) != 0) |
| appendStringInfoString(es->str, " /"); |
| appendStringInfoCharMacro(es->str, '>'); |
| if ((flags & X_NOWHITESPACE) == 0) |
| appendStringInfoCharMacro(es->str, '\n'); |
| } |
| |
| /* |
| * Indent a text-format line. |
| * |
| * We indent by two spaces per indentation level. However, when emitting |
| * data for a parallel worker there might already be data on the current line |
| * (cf. ExplainOpenWorker); in that case, don't indent any more. |
| */ |
| static void |
| ExplainIndentText(ExplainState *es) |
| { |
| Assert(es->format == EXPLAIN_FORMAT_TEXT); |
| if (es->str->len == 0 || es->str->data[es->str->len - 1] == '\n') |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| } |
| |
| /* |
| * Emit a JSON line ending. |
| * |
| * JSON requires a comma after each property but the last. To facilitate this, |
| * in JSON format, the text emitted for each property begins just prior to the |
| * preceding line-break (and comma, if applicable). |
| */ |
| static void |
| ExplainJSONLineEnding(ExplainState *es) |
| { |
| Assert(es->format == EXPLAIN_FORMAT_JSON); |
| if (linitial_int(es->grouping_stack) != 0) |
| appendStringInfoChar(es->str, ','); |
| else |
| linitial_int(es->grouping_stack) = 1; |
| appendStringInfoChar(es->str, '\n'); |
| } |
| |
| /* |
| * Indent a YAML line. |
| * |
| * YAML lines are ordinarily indented by two spaces per indentation level. |
| * The text emitted for each property begins just prior to the preceding |
| * line-break, except for the first property in an unlabeled group, for which |
| * it begins immediately after the "- " that introduces the group. The first |
| * property of the group appears on the same line as the opening "- ". |
| */ |
| static void |
| ExplainYAMLLineStarting(ExplainState *es) |
| { |
| Assert(es->format == EXPLAIN_FORMAT_YAML); |
| if (linitial_int(es->grouping_stack) == 0) |
| { |
| linitial_int(es->grouping_stack) = 1; |
| } |
| else |
| { |
| appendStringInfoChar(es->str, '\n'); |
| appendStringInfoSpaces(es->str, es->indent * 2); |
| } |
| } |
| |
| /* |
| * YAML is a superset of JSON; unfortunately, the YAML quoting rules are |
| * ridiculously complicated -- as documented in sections 5.3 and 7.3.3 of |
| * http://yaml.org/spec/1.2/spec.html -- so we chose to just quote everything. |
| * Empty strings, strings with leading or trailing whitespace, and strings |
| * containing a variety of special characters must certainly be quoted or the |
| * output is invalid; and other seemingly harmless strings like "0xa" or |
| * "true" must be quoted, lest they be interpreted as a hexadecimal or Boolean |
| * constant rather than a string. |
| */ |
| static void |
| escape_yaml(StringInfo buf, const char *str) |
| { |
| escape_json(buf, str); |
| } |
| |
| /* |
| * Explainlocus |
| * Show locus type and parallel workers(if it's > 1) of plan node. |
| */ |
| static void |
| Explainlocus(ExplainState *es, CdbLocusType locustype, int parallel) |
| { |
| char* locus = NULL; |
| switch (locustype) |
| { |
| case CdbLocusType_Null: |
| locus = "NULL"; |
| break; |
| case CdbLocusType_Entry: |
| locus = "Entry"; |
| break; |
| case CdbLocusType_SingleQE: |
| locus = "SingleQE"; |
| break; |
| case CdbLocusType_General: |
| locus = "General"; |
| break; |
| case CdbLocusType_SegmentGeneral: |
| locus = "SegmentGeneral"; |
| break; |
| case CdbLocusType_SegmentGeneralWorkers: |
| locus = "SegmentGeneralWorkers"; |
| break; |
| case CdbLocusType_OuterQuery: |
| locus = "OuterQuery"; |
| break; |
| case CdbLocusType_Replicated: |
| locus = "Replicated"; |
| break; |
| case CdbLocusType_ReplicatedWorkers: |
| locus = "ReplicatedWorkers"; |
| break; |
| case CdbLocusType_Hashed: |
| locus = "Hashed"; |
| break; |
| case CdbLocusType_HashedOJ: |
| locus = "HashedOJ"; |
| break; |
| case CdbLocusType_Strewn: |
| locus = "Strewn"; |
| break; |
| case CdbLocusType_HashedWorkers: |
| locus = "HashedWorkers"; |
| break; |
| default: |
| locus = "unknown"; |
| break; |
| } |
| |
| ExplainPropertyText("Locus", locus, es); |
| if (parallel > 1) |
| ExplainPropertyInteger("Parallel Workers", NULL, parallel, es); |
| } |
| |
| /* |
| * Return the number of leaf parts of the partitioned table with the given oid |
| */ |
| static int |
| countLeafPartTables(Oid relid) { |
| List *partitions; |
| partitions = find_all_inheritors(relid, NoLock, NULL); |
| Assert(list_length(partitions) > 0); |
| |
| /* find_all_inheritors returns a list of relation OIDs including the |
| * parent relId, so length of the list minus one gives total leaf |
| * partitions. |
| */ |
| return (list_length(partitions) -1); |
| } |