diff --git a/contrib/Makefile b/contrib/Makefile index 952855d9b61..2f0a88d3f77 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -33,6 +33,7 @@ SUBDIRS = \ pg_buffercache \ pg_freespacemap \ pg_logicalinspect \ + pg_overexplain \ pg_prewarm \ pg_stat_statements \ pg_surgery \ diff --git a/contrib/meson.build b/contrib/meson.build index 1ba73ebd67a..ed30ee7d639 100644 --- a/contrib/meson.build +++ b/contrib/meson.build @@ -47,6 +47,7 @@ subdir('pg_buffercache') subdir('pgcrypto') subdir('pg_freespacemap') subdir('pg_logicalinspect') +subdir('pg_overexplain') subdir('pg_prewarm') subdir('pgrowlocks') subdir('pg_stat_statements') diff --git a/contrib/pg_overexplain/.gitignore b/contrib/pg_overexplain/.gitignore new file mode 100644 index 00000000000..5dcb3ff9723 --- /dev/null +++ b/contrib/pg_overexplain/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/contrib/pg_overexplain/Makefile b/contrib/pg_overexplain/Makefile new file mode 100644 index 00000000000..f1c39ecebc1 --- /dev/null +++ b/contrib/pg_overexplain/Makefile @@ -0,0 +1,21 @@ +# contrib/pg_overexplain/Makefile + +MODULE_big = pg_overexplain +OBJS = \ + $(WIN32RES) \ + pg_overexplain.o + +PGFILEDESC = "pg_overexplain - allow EXPLAIN to dump even more details" + +REGRESS = pg_overexplain + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/pg_overexplain +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out new file mode 100644 index 00000000000..122f1e5da73 --- /dev/null +++ b/contrib/pg_overexplain/expected/pg_overexplain.out @@ -0,0 +1,483 @@ +-- These tests display internal details that would not be stable under +-- debug_parallel_query, so make sure that option is disabled. +SET debug_parallel_query = off; +-- These options do not exist, so these queries should all fail. +EXPLAIN (DEBUFF) SELECT 1; +ERROR: unrecognized EXPLAIN option "debuff" +LINE 1: EXPLAIN (DEBUFF) SELECT 1; + ^ +EXPLAIN (DEBUG) SELECT 1; +ERROR: unrecognized EXPLAIN option "debug" +LINE 1: EXPLAIN (DEBUG) SELECT 1; + ^ +EXPLAIN (RANGE_TABLE) SELECT 1; +ERROR: unrecognized EXPLAIN option "range_table" +LINE 1: EXPLAIN (RANGE_TABLE) SELECT 1; + ^ +-- Load the module that creates the options. +LOAD 'pg_overexplain'; +-- The first option still does not exist, but the others do. +EXPLAIN (DEBUFF) SELECT 1; +ERROR: unrecognized EXPLAIN option "debuff" +LINE 1: EXPLAIN (DEBUFF) SELECT 1; + ^ +EXPLAIN (DEBUG) SELECT 1; + QUERY PLAN +------------------------------------------ + Result (cost=0.00..0.01 rows=1 width=4) + Disabled Nodes: 0 + Parallel Safe: false + Plan Node ID: 0 + PlannedStmt: + Command Type: select + Flags: canSetTag + Subplans Needing Rewind: none + Relation OIDs: none + Executor Parameter Types: none + Parse Location: 16 for 8 bytes +(11 rows) + +EXPLAIN (RANGE_TABLE) SELECT 1; + QUERY PLAN +------------------------------------------ + Result (cost=0.00..0.01 rows=1 width=4) + RTI 1 (result): + Eref: "*RESULT*" () +(3 rows) + +-- Create a partitioned table. +CREATE TABLE vegetables (id serial, name text, genus text) +PARTITION BY LIST (genus); +CREATE TABLE daucus PARTITION OF vegetables FOR VALUES IN ('daucus'); +CREATE TABLE brassica PARTITION OF vegetables FOR VALUES IN ('brassica'); +INSERT INTO vegetables (name, genus) + VALUES ('carrot', 'daucus'), ('bok choy', 'brassica'), + ('brocooli', 'brassica'), ('cauliflower', 'brassica'), + ('cabbage', 'brassica'), ('kohlrabi', 'brassica'), + ('rutabaga', 'brassica'), ('turnip', 'brassica'); +VACUUM ANALYZE vegetables; +-- We filter relation OIDs out of the test output in order to avoid +-- test instability. This is currently only needed for EXPLAIN (DEBUG), not +-- EXPLAIN (RANGE_TABLE). +CREATE FUNCTION explain_filter(text) RETURNS SETOF text +LANGUAGE plpgsql AS +$$ +DECLARE + ln text; +BEGIN + FOR ln IN EXECUTE $1 + LOOP + ln := regexp_replace(ln, 'Relation OIDs:( \m\d+\M)+', + 'Relation OIDs: NNN...', 'g'); + ln := regexp_replace(ln, '( ?\m\d+\M)+', + 'NNN...', 'g'); + RETURN NEXT ln; + END LOOP; +END; +$$; +-- Test with both options together and an aggregate. +SELECT explain_filter($$ +EXPLAIN (DEBUG, RANGE_TABLE, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); + explain_filter +----------------------------------------------------- + GroupAggregate + Group Key: vegetables.genus + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 0 + -> Sort + Sort Key: vegetables.genus, vegetables.name + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 1 + -> Append + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 2 + Append RTIs: 1 + -> Seq Scan on brassica vegetables_1 + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 3 + Scan RTI: 3 + -> Seq Scan on daucus vegetables_2 + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 4 + Scan RTI: 4 + PlannedStmt: + Command Type: select + Flags: canSetTag + Subplans Needing Rewind: none + Relation OIDs: NNN... + Executor Parameter Types: none + Parse Location: 0 to end + RTI 1 (relation, inherited, in-from-clause): + Eref: vegetables (id, name, genus) + Relation: vegetables + Relation Kind: parititioned_table + Relation Lock Mode: AccessShareLock + Permission Info Index: 1 + RTI 2 (group): + Eref: "*GROUP*" (genus) + RTI 3 (relation, in-from-clause): + Alias: vegetables (id, name, genus) + Eref: vegetables (id, name, genus) + Relation: brassica + Relation Kind: relation + Relation Lock Mode: AccessShareLock + RTI 4 (relation, in-from-clause): + Alias: vegetables (id, name, genus) + Eref: vegetables (id, name, genus) + Relation: daucus + Relation Kind: relation + Relation Lock Mode: AccessShareLock + Unprunable RTIs: 1 3 4 +(53 rows) + +-- Test a different output format. +SELECT explain_filter($$ +EXPLAIN (DEBUG, RANGE_TABLE, FORMAT XML, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); + explain_filter +--------------------------------------------------------------------- + + + + + + + Aggregate + + Sorted + + Simple + + false + + false + + false + + + + vegetables.genus + + + + 0 + + true + + 0 + + none + + none + + + + + + Sort + + Outer + + false + + false + + false + + + + vegetables.genus + + vegetables.name + + + + 0 + + true + + 1 + + none + + none + + + + + + Append + + Outer + + false + + false + + false + + 0 + + true + + 2 + + none + + none + + 1 + + 0 + + + + + + Seq Scan + + Member+ + false + + false + + brassica + + vegetables_1 + + false + + 0 + + true + + 3 + + none + + none + + 3 + + + + + + Seq Scan + + Member+ + false + + false + + daucus + + vegetables_2 + + false + + 0 + + true + + 4 + + none + + none + + 4 + + + + + + + + + + + + + + + + + + select + + canSetTag + + none + + NNN... + + none + + 0 to end + + + + + + + + 1 + + relation + + true + + true + + vegetables (id, name, genus) + + vegetables + + parititioned_table + + AccessShareLock + + 1 + + false + + false + + + + + + 2 + + group + + false + + false + + "*GROUP*" (genus) + + false + + false + + + + + + 3 + + relation + + false + + true + + vegetables (id, name, genus) + + vegetables (id, name, genus) + + brassica + + relation + + AccessShareLock + + false + + false + + + + + + 4 + + relation + + false + + true + + vegetables (id, name, genus) + + vegetables (id, name, genus) + + daucus + + relation + + AccessShareLock + + false + + false + + + + 1 3 4 + + none + + + + + + +(1 row) + +-- Test just the DEBUG option. Verify that it shows information about +-- disabled nodes, parallel safety, and the parallelModeNeeded flag. +SET enable_seqscan = false; +SET debug_parallel_query = true; +SELECT explain_filter($$ +EXPLAIN (DEBUG, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); + explain_filter +----------------------------------------------------------- + Gather + Workers Planned: 1 + Single Copy: true + Disabled Nodes: 0 + Parallel Safe: false + Plan Node ID: 0 + -> GroupAggregate + Group Key: vegetables.genus + Disabled Nodes: 2 + Parallel Safe: true + Plan Node ID: 1 + -> Sort + Sort Key: vegetables.genus, vegetables.name + Disabled Nodes: 2 + Parallel Safe: true + Plan Node ID: 2 + -> Append + Disabled Nodes: 2 + Parallel Safe: true + Plan Node ID: 3 + -> Seq Scan on brassica vegetables_1 + Disabled: true + Disabled Nodes: 1 + Parallel Safe: true + Plan Node ID: 4 + -> Seq Scan on daucus vegetables_2 + Disabled: true + Disabled Nodes: 1 + Parallel Safe: true + Plan Node ID: 5 + PlannedStmt: + Command Type: select + Flags: canSetTag, parallelModeNeeded + Subplans Needing Rewind: none + Relation OIDs: NNN... + Executor Parameter Types: none + Parse Location: 0 to end +(37 rows) + +RESET debug_parallel_query; +RESET enable_seqscan; +-- Test the DEBUG option with a non-SELECT query, and also verify that the +-- hasReturning flag is shown. +SELECT explain_filter($$ +EXPLAIN (DEBUG, COSTS OFF) +INSERT INTO vegetables (name, genus) + VALUES ('Brotero''s carrot', 'brassica') RETURNING id +$$); + explain_filter +---------------------------------- + Insert on vegetables + Disabled Nodes: 0 + Parallel Safe: false + Plan Node ID: 0 + -> Result + Disabled Nodes: 0 + Parallel Safe: false + Plan Node ID: 1 + PlannedStmt: + Command Type: insert + Flags: hasReturning, canSetTag + Subplans Needing Rewind: none + Relation OIDs: NNN... + Executor Parameter Types: 0 + Parse Location: 0 to end +(15 rows) + +-- Create an index, and then attempt to force a nested loop with inner index +-- scan so that we can see parameter-related information. Also, let's try +-- actually running the query, but try to suppress potentially variable output. +CREATE INDEX ON vegetables (id); +ANALYZE vegetables; +SET enable_hashjoin = false; +SET enable_material = false; +SET enable_mergejoin = false; +SET enable_seqscan = false; +SELECT explain_filter($$ +EXPLAIN (BUFFERS OFF, COSTS OFF, SUMMARY OFF, TIMING OFF, ANALYZE, DEBUG) +SELECT * FROM vegetables v1, vegetables v2 WHERE v1.id = v2.id; +$$); + explain_filter +------------------------------------------------------------------------------------------ + Nested Loop (actual rows=8.00 loops=1) + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 0 + -> Append (actual rows=8.00 loops=1) + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 1 + -> Index Scan using brassica_id_idx on brassica v1_1 (actual rows=7.00 loops=1) + Index Searches: 1 + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 2 + -> Index Scan using daucus_id_idx on daucus v1_2 (actual rows=1.00 loops=1) + Index Searches: 1 + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 3 + -> Append (actual rows=1.00 loops=8) + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 4 + extParam: 0 + allParam: 0 + -> Index Scan using brassica_id_idx on brassica v2_1 (actual rows=0.88 loops=8) + Index Cond: (id = v1.id) + Index Searches: 8 + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 5 + extParam: 0 + allParam: 0 + -> Index Scan using daucus_id_idx on daucus v2_2 (actual rows=0.12 loops=8) + Index Cond: (id = v1.id) + Index Searches: 8 + Disabled Nodes: 0 + Parallel Safe: true + Plan Node ID: 6 + extParam: 0 + allParam: 0 + PlannedStmt: + Command Type: select + Flags: canSetTag + Subplans Needing Rewind: none + Relation OIDs: NNN... + Executor Parameter Types: 23 + Parse Location: 75 for 62 bytes +(47 rows) + +RESET enable_hashjoin; +RESET enable_material; +RESET enable_mergejoin; +RESET enable_seqscan; +-- Test the RANGE_TABLE option with a case that allows partition pruning. +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM vegetables WHERE genus = 'daucus'; + QUERY PLAN +---------------------------------------------- + Seq Scan on daucus vegetables + Filter: (genus = 'daucus'::text) + Scan RTI: 2 + RTI 1 (relation, inherited, in-from-clause): + Eref: vegetables (id, name, genus) + Relation: vegetables + Relation Kind: parititioned_table + Relation Lock Mode: AccessShareLock + Permission Info Index: 1 + RTI 2 (relation, in-from-clause): + Alias: vegetables (id, name, genus) + Eref: vegetables (id, name, genus) + Relation: daucus + Relation Kind: relation + Relation Lock Mode: AccessShareLock + Unprunable RTIs: 1 2 +(16 rows) + +-- Also test a case that involves a write. +EXPLAIN (RANGE_TABLE, COSTS OFF) +INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica'); + QUERY PLAN +---------------------------------------- + Insert on vegetables + Nominal RTI: 1 + Exclude Relation RTI: 0 + -> Result + RTI 1 (relation): + Eref: vegetables (id, name, genus) + Relation: vegetables + Relation Kind: parititioned_table + Relation Lock Mode: RowExclusiveLock + Permission Info Index: 1 + RTI 2 (result): + Eref: "*RESULT*" () + Unprunable RTIs: 1 + Result RTIs: 1 +(14 rows) + diff --git a/contrib/pg_overexplain/meson.build b/contrib/pg_overexplain/meson.build new file mode 100644 index 00000000000..6f52d1e51bc --- /dev/null +++ b/contrib/pg_overexplain/meson.build @@ -0,0 +1,28 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +pg_overexplain_sources = files( + 'pg_overexplain.c', +) + +if host_system == 'windows' + pg_overexplain_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'pg_overexplain', + '--FILEDESC', 'pg_overexplain - allow EXPLAIN to dump even more details',]) +endif + +pg_overexplain = shared_module('pg_overexplain', + pg_overexplain_sources, + kwargs: contrib_mod_args, +) +contrib_targets += pg_overexplain + +tests += { + 'name': 'pg_overexplain', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'pg_overexplain', + ], + }, +} 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); +} diff --git a/contrib/pg_overexplain/sql/pg_overexplain.sql b/contrib/pg_overexplain/sql/pg_overexplain.sql new file mode 100644 index 00000000000..ae9082b1e85 --- /dev/null +++ b/contrib/pg_overexplain/sql/pg_overexplain.sql @@ -0,0 +1,105 @@ +-- These tests display internal details that would not be stable under +-- debug_parallel_query, so make sure that option is disabled. +SET debug_parallel_query = off; + +-- These options do not exist, so these queries should all fail. +EXPLAIN (DEBUFF) SELECT 1; +EXPLAIN (DEBUG) SELECT 1; +EXPLAIN (RANGE_TABLE) SELECT 1; + +-- Load the module that creates the options. +LOAD 'pg_overexplain'; + +-- The first option still does not exist, but the others do. +EXPLAIN (DEBUFF) SELECT 1; +EXPLAIN (DEBUG) SELECT 1; +EXPLAIN (RANGE_TABLE) SELECT 1; + +-- Create a partitioned table. +CREATE TABLE vegetables (id serial, name text, genus text) +PARTITION BY LIST (genus); +CREATE TABLE daucus PARTITION OF vegetables FOR VALUES IN ('daucus'); +CREATE TABLE brassica PARTITION OF vegetables FOR VALUES IN ('brassica'); +INSERT INTO vegetables (name, genus) + VALUES ('carrot', 'daucus'), ('bok choy', 'brassica'), + ('brocooli', 'brassica'), ('cauliflower', 'brassica'), + ('cabbage', 'brassica'), ('kohlrabi', 'brassica'), + ('rutabaga', 'brassica'), ('turnip', 'brassica'); +VACUUM ANALYZE vegetables; + +-- We filter relation OIDs out of the test output in order to avoid +-- test instability. This is currently only needed for EXPLAIN (DEBUG), not +-- EXPLAIN (RANGE_TABLE). +CREATE FUNCTION explain_filter(text) RETURNS SETOF text +LANGUAGE plpgsql AS +$$ +DECLARE + ln text; +BEGIN + FOR ln IN EXECUTE $1 + LOOP + ln := regexp_replace(ln, 'Relation OIDs:( \m\d+\M)+', + 'Relation OIDs: NNN...', 'g'); + ln := regexp_replace(ln, '( ?\m\d+\M)+', + 'NNN...', 'g'); + RETURN NEXT ln; + END LOOP; +END; +$$; + +-- Test with both options together and an aggregate. +SELECT explain_filter($$ +EXPLAIN (DEBUG, RANGE_TABLE, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); + +-- Test a different output format. +SELECT explain_filter($$ +EXPLAIN (DEBUG, RANGE_TABLE, FORMAT XML, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); + +-- Test just the DEBUG option. Verify that it shows information about +-- disabled nodes, parallel safety, and the parallelModeNeeded flag. +SET enable_seqscan = false; +SET debug_parallel_query = true; +SELECT explain_filter($$ +EXPLAIN (DEBUG, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); +RESET debug_parallel_query; +RESET enable_seqscan; + +-- Test the DEBUG option with a non-SELECT query, and also verify that the +-- hasReturning flag is shown. +SELECT explain_filter($$ +EXPLAIN (DEBUG, COSTS OFF) +INSERT INTO vegetables (name, genus) + VALUES ('Brotero''s carrot', 'brassica') RETURNING id +$$); + +-- Create an index, and then attempt to force a nested loop with inner index +-- scan so that we can see parameter-related information. Also, let's try +-- actually running the query, but try to suppress potentially variable output. +CREATE INDEX ON vegetables (id); +ANALYZE vegetables; +SET enable_hashjoin = false; +SET enable_material = false; +SET enable_mergejoin = false; +SET enable_seqscan = false; +SELECT explain_filter($$ +EXPLAIN (BUFFERS OFF, COSTS OFF, SUMMARY OFF, TIMING OFF, ANALYZE, DEBUG) +SELECT * FROM vegetables v1, vegetables v2 WHERE v1.id = v2.id; +$$); +RESET enable_hashjoin; +RESET enable_material; +RESET enable_mergejoin; +RESET enable_seqscan; + +-- Test the RANGE_TABLE option with a case that allows partition pruning. +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM vegetables WHERE genus = 'daucus'; + +-- Also test a case that involves a write. +EXPLAIN (RANGE_TABLE, COSTS OFF) +INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica'); diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index 7c381949a53..24b706b29ad 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -155,6 +155,7 @@ CREATE EXTENSION extension_name; &pgcrypto; &pgfreespacemap; &pglogicalinspect; + &pgoverexplain; &pgprewarm; &pgrowlocks; &pgstatstatements; diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 25fb99cee69..fef9584f908 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -145,6 +145,7 @@ + diff --git a/doc/src/sgml/pgoverexplain.sgml b/doc/src/sgml/pgoverexplain.sgml new file mode 100644 index 00000000000..102bd275aed --- /dev/null +++ b/doc/src/sgml/pgoverexplain.sgml @@ -0,0 +1,186 @@ + + + + pg_overexplain — allow EXPLAIN to dump even more details + + + pg_overexplain + + + + The pg_overexplain extends EXPLAIN + with new options that provide additional output. It is mostly intended to + assist with debugging of and development of the planner, rather than for + general use. Since this module displays internal details of planner data + structures, it may be necessary to refer to the source code to make sense + of the output. Furthermore, the output is likely to change whenever (and as + often as) those data structures change. + + + + EXPLAIN (DEBUG) + + + The DEBUG option displays miscellaneous information from + the plan tree that is not normally shown because it is not expected to be + of general interest. For each individual plan node, it will display the + following fields. See Plan in + nodes/plannodes.h for additional documentation of these + fields. + + + + + + Disabled Nodes. Normal EXPLAIN + determines whether a node is disabled by checking whether the node's + count of disabled nodes is larger than the sum of the counts for the + underlying nodes. This option shows the raw counter value. + + + + + + Parallel Safe. Indicates whether it would be safe for + a plan tree node to appear beneath a Gather or + Gather Merge node, regardless of whether it is + actually below such a node. + + + + + + Plan Node ID. An internal ID number that should be + unique for every node in the plan tree. It is used to coordinate parallel + query activity. + + + + + + extParam and allParam. Information + about which numbered parameters affect this plan node or its children. In + text mode, these fields are only displayed if they are non-empty sets. + + + + + + Once per query, the DEBUG option will display the + following fields. See PlannedStmt in + nodes/plannodes.h for additional detail. + + + + + + Command Type. For example, select + or update. + + + + + + Flags. A comma-separated list of Boolean structure + member names from the PlannedStmt that are set to + true. It covers the following structure members: + hasReturning, hasModifyingCTE, + canSetTag, transientPlan, + dependsOnRole, parallelModeNeeded. + + + + + + Subplans Needing Rewind. Integer IDs of subplans that + may need to be rewound by the executor. + + + + + + Relation OIDs. OIDs of relations upon which this plan + depends. + + + + + + Executor Parameter Types. Type OID for each executor parameter + (e.g. when a nested loop is chosen and a parameter is used to pass a value down + to an inner index scan). Does not include parameters supplied to a prepared + statement by the user. + + + + + + Parse Location. Location within the query string + supplied to the planner where this query's text can be found. May be + Unknown in some contexts. Otherwise, may be + NNN to end for some integer NNN or + NNN for MMM bytes for some integers + NNN and MMM. + + + + + + + EXPLAIN (RANGE_TABLE) + + + The RANGE_TABLE option displays information from the + plan tree specifically concerning the query's range table. Range table + entries correspond roughly to items appearing in the query's + FROM clause, but with numerous exceptions. For example, + subqueries that are proved unnecessary may be deleted from the range table + entirely, while inheritance expansion adds range table entries for child + tables that are not named directly in the query. + + + + Range table entries are generally referenced within the query plan by a + range table index, or RTI. Plan nodes that reference one or more RTIs will + be labelled accordingly, using one of the following fields: Scan + RTI, Nominal RTI, Exclude Relation + RTI, Append RTIs. + + + + In addition, the query as a whole may maintain lists of range table indexes + that are needed for various purposes. These lists will be displayed once + per query, labelled as appropriate as Unprunable RTIs or + Result RTIs. In text mode, these fields are only + displayed if they are non-empty sets. + + + + Finally, but most importantly, the RANGE_TABLE option + will display a dump of the query's entire range table. Each range table + entry is labelled with the appropriate range table index, the kind of range + table entry (e.g. relation, + subquery, or join), followed by the + contents of various range table entry fields that are not normally part of + EXPLAIN output. Some of these fields are only displayed + for certain kinds of range table entries. For example, + Eref is displayed for all types of range table entries, + but CTE Name is displayed only for range table entries + of type cte. + + + + For more information about range table entries, see the definition of + RangeTblEntry in nodes/plannodes.h. + + + + + Author + + + Robert Haas rhaas@postgresql.org + + + + diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ff8d9ff0777..78e22a14f62 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -4278,3 +4278,4 @@ z_streamp zic_t ExplainExtensionOption ExplainOptionHandler +overexplain_options