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