diff options
Diffstat (limited to 'contrib/pg_overexplain/pg_overexplain.c')
-rw-r--r-- | contrib/pg_overexplain/pg_overexplain.c | 763 |
1 files changed, 763 insertions, 0 deletions
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c new file mode 100644 index 00000000000..4554c3abbbf --- /dev/null +++ b/contrib/pg_overexplain/pg_overexplain.c @@ -0,0 +1,763 @@ +/*------------------------------------------------------------------------- + * + * pg_overexplain.c + * allow EXPLAIN to dump even more details + * + * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * + * contrib/pg_overexplain/pg_overexplain.c + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_class.h" +#include "commands/defrem.h" +#include "commands/explain.h" +#include "commands/explain_format.h" +#include "commands/explain_state.h" +#include "fmgr.h" +#include "parser/parsetree.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +PG_MODULE_MAGIC; + +typedef struct +{ + bool debug; + bool range_table; +} overexplain_options; + +static overexplain_options *overexplain_ensure_options(ExplainState *es); +static void overexplain_debug_handler(ExplainState *es, DefElem *opt, + ParseState *pstate); +static void overexplain_range_table_handler(ExplainState *es, DefElem *opt, + ParseState *pstate); +static void overexplain_per_node_hook(PlanState *planstate, List *ancestors, + const char *relationship, + const char *plan_name, + ExplainState *es); +static void overexplain_per_plan_hook(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv); +static void overexplain_debug(PlannedStmt *plannedstmt, ExplainState *es); +static void overexplain_range_table(PlannedStmt *plannedstmt, + ExplainState *es); +static void overexplain_alias(const char *qlabel, Alias *alias, + ExplainState *es); +static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms, + ExplainState *es); +static void overexplain_intlist(const char *qlabel, List *intlist, + ExplainState *es); + +static int es_extension_id; +static explain_per_node_hook_type prev_explain_per_node_hook; +static explain_per_plan_hook_type prev_explain_per_plan_hook; + +/* + * Initialization we do when this module is loaded. + */ +void +_PG_init(void) +{ + /* Get an ID that we can use to cache data in an ExplainState. */ + es_extension_id = GetExplainExtensionId("pg_overexplain"); + + /* Register the new EXPLAIN options implemented by this module. */ + RegisterExtensionExplainOption("debug", overexplain_debug_handler); + RegisterExtensionExplainOption("range_table", + overexplain_range_table_handler); + + /* Use the per-node and per-plan hooks to make our options do something. */ + prev_explain_per_node_hook = explain_per_node_hook; + explain_per_node_hook = overexplain_per_node_hook; + prev_explain_per_plan_hook = explain_per_plan_hook; + explain_per_plan_hook = overexplain_per_plan_hook; +} + +/* + * Get the overexplain_options structure from an ExplainState; if there is + * none, create one, attach it to the ExplainState, and return it. + */ +static overexplain_options * +overexplain_ensure_options(ExplainState *es) +{ + overexplain_options *options; + + options = GetExplainExtensionState(es, es_extension_id); + + if (options == NULL) + { + options = palloc0(sizeof(overexplain_options)); + SetExplainExtensionState(es, es_extension_id, options); + } + + return options; +} + +/* + * Parse handler for EXPLAIN (DEBUG). + */ +static void +overexplain_debug_handler(ExplainState *es, DefElem *opt, ParseState *pstate) +{ + overexplain_options *options = overexplain_ensure_options(es); + + options->debug = defGetBoolean(opt); +} + +/* + * Parse handler for EXPLAIN (RANGE_TABLE). + */ +static void +overexplain_range_table_handler(ExplainState *es, DefElem *opt, + ParseState *pstate) +{ + overexplain_options *options = overexplain_ensure_options(es); + + options->range_table = defGetBoolean(opt); +} + +/* + * Print out additional per-node information as appropriate. If the user didn't + * specify any of the options we support, do nothing; else, print whatever is + * relevant to the specified options. + */ +static void +overexplain_per_node_hook(PlanState *planstate, List *ancestors, + const char *relationship, const char *plan_name, + ExplainState *es) +{ + overexplain_options *options; + Plan *plan = planstate->plan; + + options = GetExplainExtensionState(es, es_extension_id); + if (options == NULL) + return; + + /* + * If the "debug" option was given, display miscellaneous fields from the + * "Plan" node that would not otherwise be displayed. + */ + if (options->debug) + { + /* + * Normal EXPLAIN will display "Disabled: true" if the node is + * disabled; but that is based on noticing that plan->disabled_nodes + * is higher than the sum of its children; here, we display the raw + * value, for debugging purposes. + */ + ExplainPropertyInteger("Disabled Nodes", NULL, plan->disabled_nodes, + es); + + /* + * Normal EXPLAIN will display the parallel_aware flag; here, we show + * the parallel_safe flag as well. + */ + ExplainPropertyBool("Parallel Safe", plan->parallel_safe, es); + + /* + * The plan node ID isn't normally displayed, since it is only useful + * for debugging. + */ + ExplainPropertyInteger("Plan Node ID", NULL, plan->plan_node_id, es); + + /* + * It is difficult to explain what extParam and allParam mean in plain + * language, so we simply display these fields labelled with the + * structure member name. For compactness, the text format omits the + * display of this information when the bitmapset is empty. + */ + if (es->format != EXPLAIN_FORMAT_TEXT || !bms_is_empty(plan->extParam)) + overexplain_bitmapset("extParam", plan->extParam, es); + if (es->format != EXPLAIN_FORMAT_TEXT || !bms_is_empty(plan->allParam)) + overexplain_bitmapset("allParam", plan->allParam, es); + } + + /* + * If the "range_table" option was specified, display information about + * the range table indexes for this node. + */ + if (options->range_table) + { + switch (nodeTag(plan)) + { + case T_SeqScan: + case T_SampleScan: + case T_IndexScan: + case T_IndexOnlyScan: + case T_BitmapHeapScan: + case T_TidScan: + case T_TidRangeScan: + case T_SubqueryScan: + case T_FunctionScan: + case T_TableFuncScan: + case T_ValuesScan: + case T_CteScan: + case T_NamedTuplestoreScan: + case T_WorkTableScan: + ExplainPropertyInteger("Scan RTI", NULL, + ((Scan *) plan)->scanrelid, es); + break; + case T_ForeignScan: + overexplain_bitmapset("Scan RTIs", + ((ForeignScan *) plan)->fs_base_relids, + es); + break; + case T_CustomScan: + overexplain_bitmapset("Scan RTIs", + ((CustomScan *) plan)->custom_relids, + es); + break; + case T_ModifyTable: + ExplainPropertyInteger("Nominal RTI", NULL, + ((ModifyTable *) plan)->nominalRelation, es); + ExplainPropertyInteger("Exclude Relation RTI", NULL, + ((ModifyTable *) plan)->exclRelRTI, es); + break; + case T_Append: + overexplain_bitmapset("Append RTIs", + ((Append *) plan)->apprelids, + es); + break; + case T_MergeAppend: + overexplain_bitmapset("Append RTIs", + ((MergeAppend *) plan)->apprelids, + es); + break; + default: + break; + } + } +} + +/* + * Print out additional per-query information as appropriate. Here again, if + * the user didn't specify any of the options implemented by this module, do + * nothing; otherwise, call the appropriate function for each specified + * option. + */ +static void +overexplain_per_plan_hook(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv) +{ + overexplain_options *options; + + options = GetExplainExtensionState(es, es_extension_id); + if (options == NULL) + return; + + if (options->debug) + overexplain_debug(plannedstmt, es); + + if (options->range_table) + overexplain_range_table(plannedstmt, es); +} + +/* + * Print out various details from the PlannedStmt that wouldn't otherwise + * be displayed. + * + * We don't try to print everything here. Information that would be displyed + * anyway doesn't need to be printed again here, and things with lots of + * substructure probably should be printed via separate options, or not at all. + */ +static void +overexplain_debug(PlannedStmt *plannedstmt, ExplainState *es) +{ + char *commandType = NULL; + StringInfoData flags; + + /* Even in text mode, we want to set this output apart as its own group. */ + ExplainOpenGroup("PlannedStmt", "PlannedStmt", true, es); + if (es->format == EXPLAIN_FORMAT_TEXT) + { + ExplainIndentText(es); + appendStringInfo(es->str, "PlannedStmt:\n"); + es->indent++; + } + + /* Print the command type. */ + switch (plannedstmt->commandType) + { + case CMD_UNKNOWN: + commandType = "unknown"; + break; + case CMD_SELECT: + commandType = "select"; + break; + case CMD_UPDATE: + commandType = "update"; + break; + case CMD_INSERT: + commandType = "insert"; + break; + case CMD_DELETE: + commandType = "delete"; + break; + case CMD_MERGE: + commandType = "merge"; + break; + case CMD_UTILITY: + commandType = "utility"; + break; + case CMD_NOTHING: + commandType = "nothing"; + break; + } + ExplainPropertyText("Command Type", commandType, es); + + /* Print various properties as a comma-separated list of flags. */ + initStringInfo(&flags); + if (plannedstmt->hasReturning) + appendStringInfo(&flags, ", hasReturning"); + if (plannedstmt->hasModifyingCTE) + appendStringInfo(&flags, ", hasModifyingCTE"); + if (plannedstmt->canSetTag) + appendStringInfo(&flags, ", canSetTag"); + if (plannedstmt->transientPlan) + appendStringInfo(&flags, ", transientPlan"); + if (plannedstmt->dependsOnRole) + appendStringInfo(&flags, ", dependsOnRole"); + if (plannedstmt->parallelModeNeeded) + appendStringInfo(&flags, ", parallelModeNeeded"); + if (flags.len == 0) + appendStringInfo(&flags, ", none"); + ExplainPropertyText("Flags", flags.data + 2, es); + + /* Various lists of integers. */ + overexplain_bitmapset("Subplans Needing Rewind", + plannedstmt->rewindPlanIDs, es); + overexplain_intlist("Relation OIDs", + plannedstmt->relationOids, es); + overexplain_intlist("Executor Parameter Types", + plannedstmt->paramExecTypes, es); + + /* + * Print the statement location. (If desired, we could alternatively print + * stmt_location and stmt_len as two separate fields.) + */ + if (plannedstmt->stmt_location == -1) + ExplainPropertyText("Parse Location", "Unknown", es); + else if (plannedstmt->stmt_len == 0) + ExplainPropertyText("Parse Location", + psprintf("%d to end", plannedstmt->stmt_location), + es); + else + ExplainPropertyText("Parse Location", + psprintf("%d for %d bytes", + plannedstmt->stmt_location, + plannedstmt->stmt_len), + es); + + /* Done with this group. */ + if (es->format == EXPLAIN_FORMAT_TEXT) + es->indent--; + ExplainCloseGroup("PlannedStmt", "PlannedStmt", true, es); +} + +/* + * Provide detailed information about the contents of the PlannedStmt's + * range table. + */ +static void +overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) +{ + Index rti; + + /* Open group, one entry per RangeTblEntry */ + ExplainOpenGroup("Range Table", "Range Table", false, es); + + /* Iterate over the range table */ + for (rti = 1; rti <= list_length(plannedstmt->rtable); ++rti) + { + RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable); + char *kind = NULL; + char *relkind; + + /* NULL entries are possible; skip them */ + if (rte == NULL) + continue; + + /* Translate rtekind to a string */ + switch (rte->rtekind) + { + case RTE_RELATION: + kind = "relation"; + break; + case RTE_SUBQUERY: + kind = "subquery"; + break; + case RTE_JOIN: + kind = "join"; + break; + case RTE_FUNCTION: + kind = "function"; + break; + case RTE_TABLEFUNC: + kind = "tablefunc"; + break; + case RTE_VALUES: + kind = "values"; + break; + case RTE_CTE: + kind = "cte"; + break; + case RTE_NAMEDTUPLESTORE: + kind = "namedtuplestore"; + break; + case RTE_RESULT: + kind = "result"; + break; + case RTE_GROUP: + kind = "group"; + break; + } + + /* Begin group for this specific RTE */ + ExplainOpenGroup("Range Table Entry", NULL, true, es); + + /* + * In text format, the summary line displays the range table index and + * rtekind, plus indications if rte->inh and/or rte->inFromCl are set. + * In other formats, we display those as separate properties. + */ + if (es->format == EXPLAIN_FORMAT_TEXT) + { + ExplainIndentText(es); + appendStringInfo(es->str, "RTI %u (%s%s%s):\n", rti, kind, + rte->inh ? ", inherited" : "", + rte->inFromCl ? ", in-from-clause" : ""); + es->indent++; + } + else + { + ExplainPropertyUInteger("RTI", NULL, rti, es); + ExplainPropertyText("Kind", kind, es); + ExplainPropertyBool("Inherited", rte->inh, es); + ExplainPropertyBool("In From Clause", rte->inFromCl, es); + } + + /* rte->alias is optional; rte->eref is requested */ + if (rte->alias != NULL) + overexplain_alias("Alias", rte->alias, es); + overexplain_alias("Eref", rte->eref, es); + + /* + * We adhere to the usual EXPLAIN convention that schema names are + * displayed only in verbose mode, and we emit nothing if there is no + * relation OID. + */ + if (rte->relid != 0) + { + const char *relname; + const char *qualname; + + relname = quote_identifier(get_rel_name(rte->relid)); + + if (es->verbose) + { + Oid nspoid = get_rel_namespace(rte->relid); + char *nspname; + + nspname = get_namespace_name_or_temp(nspoid); + qualname = psprintf("%s.%s", quote_identifier(nspname), + relname); + } + else + qualname = relname; + + ExplainPropertyText("Relation", qualname, es); + } + + /* Translate relkind, if any, to a string */ + switch (rte->relkind) + { + case RELKIND_RELATION: + relkind = "relation"; + break; + case RELKIND_INDEX: + relkind = "index"; + break; + case RELKIND_SEQUENCE: + relkind = "sequence"; + break; + case RELKIND_TOASTVALUE: + relkind = "toastvalue"; + break; + case RELKIND_VIEW: + relkind = "view"; + break; + case RELKIND_MATVIEW: + relkind = "matview"; + break; + case RELKIND_COMPOSITE_TYPE: + relkind = "composite_type"; + break; + case RELKIND_FOREIGN_TABLE: + relkind = "foreign_table"; + break; + case RELKIND_PARTITIONED_TABLE: + relkind = "parititioned_table"; + break; + case RELKIND_PARTITIONED_INDEX: + relkind = "parititioned_index"; + break; + case '\0': + relkind = NULL; + break; + default: + relkind = psprintf("%c", rte->relkind); + break; + } + + /* If there is a relkind, show it */ + if (relkind != NULL) + ExplainPropertyText("Relation Kind", relkind, es); + + /* If there is a lock mode, show it */ + if (rte->rellockmode != 0) + ExplainPropertyText("Relation Lock Mode", + GetLockmodeName(DEFAULT_LOCKMETHOD, + rte->rellockmode), es); + + /* + * If there is a perminfoindex, show it. We don't try to display + * information from the RTEPermissionInfo node here because they are + * just indexes plannedstmt->permInfos which could be separately + * dumped if someone wants to add EXPLAIN (PERMISSIONS) or similar. + */ + if (rte->perminfoindex != 0) + ExplainPropertyInteger("Permission Info Index", NULL, + rte->perminfoindex, es); + + /* + * add_rte_to_flat_rtable will clear rte->tablesample and + * rte->subquery in the finished plan, so skip those fields. + * + * However, the security_barrier flag is not shown by the core code, + * so let's print it here. + */ + if (es->format != EXPLAIN_FORMAT_TEXT || rte->security_barrier) + ExplainPropertyBool("Security Barrier", rte->security_barrier, es); + + /* + * If this is a join, print out the fields that are specifically valid + * for joins. + */ + if (rte->rtekind == RTE_JOIN) + { + char *jointype; + + switch (rte->jointype) + { + case JOIN_INNER: + jointype = "Inner"; + break; + case JOIN_LEFT: + jointype = "Left"; + break; + case JOIN_FULL: + jointype = "Full"; + break; + case JOIN_RIGHT: + jointype = "Right"; + break; + case JOIN_SEMI: + jointype = "Semi"; + break; + case JOIN_ANTI: + jointype = "Anti"; + break; + case JOIN_RIGHT_SEMI: + jointype = "Right Semi"; + break; + case JOIN_RIGHT_ANTI: + jointype = "Right Anti"; + break; + default: + jointype = "???"; + break; + } + + /* Join type */ + ExplainPropertyText("Join Type", jointype, es); + + /* # of JOIN USING columns */ + if (es->format != EXPLAIN_FORMAT_TEXT || rte->joinmergedcols != 0) + ExplainPropertyInteger("JOIN USING Columns", NULL, + rte->joinmergedcols, es); + + /* + * add_rte_to_flat_rtable will clear joinaliasvars, joinleftcols, + * joinrightcols, and join_using_alias here, so skip those fields. + */ + } + + /* + * add_rte_to_flat_rtable will clear functions, tablefunc, and + * values_lists, but we can display funcordinality. + */ + if (rte->rtekind == RTE_FUNCTION) + ExplainPropertyBool("WITH ORDINALITY", rte->funcordinality, es); + + /* + * If this is a CTE, print out CTE-related properties. + */ + if (rte->rtekind == RTE_CTE) + { + ExplainPropertyText("CTE Name", rte->ctename, es); + ExplainPropertyUInteger("CTE Levels Up", NULL, rte->ctelevelsup, + es); + ExplainPropertyBool("CTE Self-Reference", rte->self_reference, es); + } + + /* + * add_rte_to_flat_rtable will clear coltypes, coltypemods, and + * colcollations, so skip those fields. + * + * If this is an ephemeral named relation, print out ENR-related + * properties. + */ + if (rte->rtekind == RTE_NAMEDTUPLESTORE) + { + ExplainPropertyText("ENR Name", rte->enrname, es); + ExplainPropertyFloat("ENR Tuples", NULL, rte->enrtuples, 0, es); + } + + /* + * add_rte_to_flat_rtable will clear groupexprs and securityQuals, so + * skip that field. We have handled inFromCl above, so the only thing + * left to handle here is rte->lateral. + */ + if (es->format != EXPLAIN_FORMAT_TEXT || rte->lateral) + ExplainPropertyBool("Lateral", rte->lateral, es); + + /* Done with this RTE */ + if (es->format == EXPLAIN_FORMAT_TEXT) + es->indent--; + ExplainCloseGroup("Range Table Entry", NULL, true, es); + } + + /* Print PlannedStmt fields that contain RTIs. */ + if (es->format != EXPLAIN_FORMAT_TEXT || + !bms_is_empty(plannedstmt->unprunableRelids)) + overexplain_bitmapset("Unprunable RTIs", plannedstmt->unprunableRelids, + es); + if (es->format != EXPLAIN_FORMAT_TEXT || + plannedstmt->resultRelations != NIL) + overexplain_intlist("Result RTIs", plannedstmt->resultRelations, es); + + /* Close group, we're all done */ + ExplainCloseGroup("Range Table", "Range Table", false, es); +} + +/* + * Emit a text property describing the contents of an Alias. + * + * Column lists can be quite long here, so perhaps we should have an option + * to limit the display length by # of columsn or # of characters, but for + * now, just display everything. + */ +static void +overexplain_alias(const char *qlabel, Alias *alias, ExplainState *es) +{ + StringInfoData buf; + bool first = true; + + Assert(alias != NULL); + + initStringInfo(&buf); + appendStringInfo(&buf, "%s (", quote_identifier(alias->aliasname)); + + foreach_node(String, cn, alias->colnames) + { + appendStringInfo(&buf, "%s%s", + first ? "" : ", ", + quote_identifier(cn->sval)); + first = false; + } + + appendStringInfoChar(&buf, ')'); + ExplainPropertyText(qlabel, buf.data, es); + pfree(buf.data); +} + +/* + * Emit a text property describing the contents of a bitmapset -- either a + * space-separated list of integer members, or the word "none" if the bitmapset + * is empty. + */ +static void +overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es) +{ + int x = -1; + + StringInfoData buf; + + if (bms_is_empty(bms)) + { + ExplainPropertyText(qlabel, "none", es); + return; + } + + initStringInfo(&buf); + while ((x = bms_next_member(bms, x)) >= 0) + appendStringInfo(&buf, " %d", x); + Assert(buf.data[0] == ' '); + ExplainPropertyText(qlabel, buf.data + 1, es); + pfree(buf.data); +} + +/* + * Emit a text property describing the contents of a list of integers, OIDs, + * or XIDs -- either a space-separated list of integer members, or the word + * "none" if the list is empty. + */ +static void +overexplain_intlist(const char *qlabel, List *list, ExplainState *es) +{ + StringInfoData buf; + + initStringInfo(&buf); + + if (list == NIL) + { + ExplainPropertyText(qlabel, "none", es); + return; + } + + if (IsA(list, IntList)) + { + foreach_int(i, list) + appendStringInfo(&buf, " %d", i); + } + else if (IsA(list, OidList)) + { + foreach_oid(o, list) + appendStringInfo(&buf, " %u", o); + } + else if (IsA(list, XidList)) + { + foreach_xid(x, list) + appendStringInfo(&buf, " %u", x); + } + else + { + appendStringInfo(&buf, " not an integer list"); + Assert(false); + } + + if (buf.len > 0) + ExplainPropertyText(qlabel, buf.data + 1, es); + + pfree(buf.data); +} |