From a01827ed49c7a5bc78daa942947d5b758d2b9e83 Mon Sep 17 00:00:00 2001
From: rafaelthca <rafaelthca@gmail.com>
Date: Sat, 29 Mar 2025 14:19:46 -0300
Subject: [PATCH v14] Proposal for progressive explains

This proposal introduces a feature to print execution plans of active
queries in an in-memory shared hash object so that other sessions can
visualize via new view pg_stat_progress_explain.

Plans are only printed if new GUC parameter progressive_explain is
enabled.

When new GUC progressive_explain_interval is set to 0 the plan will be
printed only at query start. If set to any other value the QueryDesc
will be adjusted adding instrumentation flags. In that case the plan
will be printed on a fixed interval controlled by progressive_explain_interval
including all instrumentation stats computed so far (per node rows and
execution time).

New view:
- pg_stat_progress_explain
  - datid: OID of the database
  - datname: Name of the database
  - pid: PID of the process running the query
  - last_update: timestamp when plan was last printed
  - query_plan: the actual plan (limited read privileges)

New GUCs:
- progressive_explain: if progressive plans are printed for local
session.
  - type: bool
  - default: off
  - context: user
- progressive_explain_interval: interval between each explain print.
  - type: int
  - default: 0
  - min: 0
  - context: user
- progressive_explain_format: format used to print the plans.
  - type: enum
  - default: text
  - values: [TEXT, XML, JSON, or YAML]
  - context: user
- progressive_explain_settings: controls whether information about
modified configuration is added to the printed plan.
  - type: bool
  - default: off
  - context: user
- progressive_explain_verbose: controls whether verbose details are
added to the printed plan.
  - type: bool
  - default: off
  - context: user
- progressive_explain_buffers: controls whether buffers details are
added to the printed plan.
  - type: bool
  - default: off
  - context: user
- progressive_explain_timing: controls whether per node timing details
are added to the printed plan.
  - type: bool
  - default: true
  - context: user
- progressive_explain_wal: controls whether WAL record generation
details are added to the printed plan.
  - type: bool
  - default: off
  - context: user
- progressive_explain_costs: controls whether estimated startup and
total cost of each plan noded is added to the printed plan.
  - type: bool
  - default: true
  - context: user
---
 contrib/auto_explain/auto_explain.c           |  10 +-
 doc/src/sgml/config.sgml                      | 156 ++++++
 doc/src/sgml/monitoring.sgml                  |  82 +++
 doc/src/sgml/perform.sgml                     |  97 ++++
 src/backend/access/transam/xact.c             |  15 +
 src/backend/catalog/system_views.sql          |  10 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/explain.c                | 220 +++++---
 src/backend/commands/explain_format.c         |  12 +
 src/backend/commands/explain_progressive.c    | 493 ++++++++++++++++++
 src/backend/commands/meson.build              |   1 +
 src/backend/executor/execMain.c               |  21 +
 src/backend/executor/execProcnode.c           |  16 +-
 src/backend/executor/instrument.c             |  20 +-
 src/backend/storage/ipc/ipci.c                |   7 +
 src/backend/storage/lmgr/lwlock.c             |   1 +
 src/backend/tcop/pquery.c                     |   3 +
 .../utils/activity/wait_event_names.txt       |   1 +
 src/backend/utils/init/postinit.c             |  10 +
 src/backend/utils/misc/guc_tables.c           | 110 ++++
 src/backend/utils/misc/postgresql.conf.sample |  13 +
 src/include/catalog/pg_proc.dat               |  10 +
 src/include/commands/explain_progressive.h    |  50 ++
 src/include/commands/explain_state.h          |   9 +
 src/include/executor/execdesc.h               |   1 +
 src/include/executor/instrument.h             |   1 +
 src/include/nodes/execnodes.h                 |   6 +
 src/include/storage/lwlock.h                  |   1 +
 src/include/storage/lwlocklist.h              |   1 +
 src/include/utils/guc.h                       |  10 +
 src/include/utils/timeout.h                   |   1 +
 .../test_misc/t/008_progressive_explain.pl    | 128 +++++
 src/test/regress/expected/rules.out           |   7 +
 src/tools/pgindent/typedefs.list              |   2 +
 34 files changed, 1441 insertions(+), 85 deletions(-)
 create mode 100644 src/backend/commands/explain_progressive.c
 create mode 100644 src/include/commands/explain_progressive.h
 create mode 100644 src/test/modules/test_misc/t/008_progressive_explain.pl

diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c
index cd6625020a7..0d28ae2ffe1 100644
--- a/contrib/auto_explain/auto_explain.c
+++ b/contrib/auto_explain/auto_explain.c
@@ -42,14 +42,6 @@ static int	auto_explain_log_level = LOG;
 static bool auto_explain_log_nested_statements = false;
 static double auto_explain_sample_rate = 1;
 
-static const struct config_enum_entry format_options[] = {
-	{"text", EXPLAIN_FORMAT_TEXT, false},
-	{"xml", EXPLAIN_FORMAT_XML, false},
-	{"json", EXPLAIN_FORMAT_JSON, false},
-	{"yaml", EXPLAIN_FORMAT_YAML, false},
-	{NULL, 0, false}
-};
-
 static const struct config_enum_entry loglevel_options[] = {
 	{"debug5", DEBUG5, false},
 	{"debug4", DEBUG4, false},
@@ -191,7 +183,7 @@ _PG_init(void)
 							 NULL,
 							 &auto_explain_log_format,
 							 EXPLAIN_FORMAT_TEXT,
-							 format_options,
+							 explain_format_options,
 							 PGC_SUSET,
 							 0,
 							 NULL,
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 65ab95be370..c96206e3275 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8682,6 +8682,162 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-progressive-explain" xreflabel="progressive_explain">
+      <term><varname>progressive_explain</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>progressive_explain</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Determines whether progressive explains are enabled and how
+        they are executed; see <xref linkend="using-explain-progressive"/>.
+        Possible values are <literal>off</literal>, <literal>explain</literal>
+        and <literal>analyze</literal>. The default is <literal>off</literal>.
+        When set to <literal>explain</literal> the plan will be printed only
+        once after <xref linkend="guc-progressive-explain-min-duration"/>. If
+        set to <literal>analyze</literal>, instrumentation flags are enabled,
+        causing the plan to be printed on a fixed interval controlled by
+        <xref linkend="guc-progressive-explain-interval"/> including all
+        instrumentation stats computed so far.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-min-duration" xreflabel="progressive_explain_min_duration">
+      <term><varname>progressive_explain_min_duration</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_min_duration</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Sets the threshold (in milliseconds) until progressive explain is
+        printed for the first time. The default is <literal>1s</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-interval" xreflabel="progressive_explain_interval">
+      <term><varname>progressive_explain_interval</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_interval</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Sets the interval (in milliseconds) between each instrumented
+        progressive explain. The default is <literal>1s</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-buffers" xreflabel="progressive_explain_buffers">
+      <term><varname>progressive_explain_buffers</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_buffers</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls whether information on buffer usage is added to
+        progressive explains. Equivalent to the BUFFERS option of
+        EXPLAIN. The default is <literal>off</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-timing" xreflabel="progressive_explain_timing">
+      <term><varname>progressive_explain_timing</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_timing</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls whether information on per node timing is added
+        to progressive explains. Equivalent to the TIMING option of
+        EXPLAIN. The default is <literal>off</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-wal" xreflabel="progressive_explain_wal">
+      <term><varname>progressive_explain_wal</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_wal</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls whether information on WAL record generation is
+        added to progressive explains. Equivalent to the WAL option of
+        EXPLAIN. The default is <literal>off</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-verbose" xreflabel="progressive_explain_verbose">
+      <term><varname>progressive_explain_verbose</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_verbose</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls whether verbose details are added to progressive explains.
+        Equivalent to the VERBOSE option of EXPLAIN. The default is
+        <literal>off</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-settings" xreflabel="progressive_explain_settings">
+      <term><varname>progressive_explain_settings</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_settings</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls whether information on modified configuration is added to
+        progressive explains. Equivalent to the SETTINGS option of EXPLAIN.
+        The default is <literal>off</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-costs" xreflabel="progressive_explain_costs">
+      <term><varname>progressive_explain_costs</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_costs</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls whether information on the estimated startup and total cost of
+        each plan node is added to progressive explains. Equivalent to the COSTS
+        option of EXPLAIN.
+        The default is <literal>off</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-progressive-explain-format" xreflabel="progressive_explain_format">
+      <term><varname>progressive_explain_format</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>progressive_explain_format</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Selects the EXPLAIN output format to be used with progressive
+        explains. Equivalent to the FORMAT option of EXPLAIN. The default
+        is <literal>text</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      </variablelist>
     </sect2>
 
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bacc09cb8af..e5497271eaa 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -6842,6 +6842,88 @@ FROM pg_stat_get_backend_idset() AS backendid;
 
  </sect2>
 
+<sect2 id="explain-progress-reporting">
+  <title>EXPLAIN Progress Reporting</title>
+
+  <indexterm>
+   <primary>pg_stat_progress_explain</primary>
+  </indexterm>
+
+  <para>
+   Whenever a client backend or parallel worker is running a query with
+   <xref linkend="guc-progressive-explain"/> enabled, the
+   <structname>pg_stat_progress_explain</structname> view  will contain a
+   corresponding row with query plan details; see
+   <xref linkend="using-explain-progressive"/>. The table below describe the
+   information that will be reported.
+  </para>
+
+  <table id="pg-stat-progress-explain-view" xreflabel="pg_stat_progress_explain">
+   <title><structname>pg_stat_progress_explain</structname> View</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>datid</structfield> <type>oid</type>
+      </para>
+      <para>
+       OID of the database this backend is connected to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>datname</structfield> <type>name</type>
+      </para>
+      <para>
+       Name of the database this backend is connected to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pid</structfield> <type>integer</type>
+      </para>
+      <para>
+       Process ID of a client backend or parallel worker.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_update</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Timestamp when plan was last printed.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>query_plan</structfield> <type>text</type>
+      </para>
+      <para>
+       The progressive explain text.
+      </para></entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+
+ </sect2>
+
  </sect1>
 
  <sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 387baac7e8c..04a78f29df9 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1169,6 +1169,103 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    </para>
   </sect2>
 
+  <sect2 id="using-explain-progressive">
+   <title>Progressive <command>EXPLAIN</command></title>
+
+   <para>
+    The query plan created by the planner for any given active query can
+    be visualized by any session via <xref linkend="pg-stat-progress-explain-view"/>
+    view when <xref linkend="guc-progressive-explain"/> is enabled in the
+    client backend or parallel worker executing query and after min duration
+    specified by <xref linkend="guc-progressive-explain-min-duration"/> has
+    passed. Settings <xref linkend="guc-progressive-explain-timing"/>,
+    <xref linkend="guc-progressive-explain-buffers"/> and
+    <xref linkend="guc-progressive-explain-wal"/> control which additional
+    instrumentaton details are collected and included in the output while
+    <xref linkend="guc-progressive-explain-format"/>,
+    <xref linkend="guc-progressive-explain-verbose"/>,
+    <xref linkend="guc-progressive-explain-settings"/> and
+    <xref linkend="guc-progressive-explain-costs"/>
+    define how the plan is printed and which details are added there.
+   </para>
+
+   <para>
+    When <xref linkend="guc-progressive-explain"/> is set to <literal>explain</literal>
+    the plan will be printed once at the beginning of the query.
+   </para>
+
+   <para>
+<screen>
+SET progressive_explain = 'explain';
+SET
+
+SELECT * FROM test t1 INNER JOIN test t2 ON t1.c1=t2.c1;
+</screen>
+   </para>
+   <para>
+<screen>
+SELECT * FROM pg_stat_progress_explain;
+datid | datname  |  pid  |          last_update          |                                  query_plan
+-------+----------+-------+-------------------------------+------------------------------------------------------------------------------
+    5 | postgres | 73972 | 2025-03-13 23:41:01.606324-03 | Hash Join  (cost=327879.85..878413.95 rows=9999860 width=18)                +
+      |          |       |                               |   Hash Cond: (t1.c1 = t2.c1)                                                +
+      |          |       |                               |   ->  Seq Scan on test t1  (cost=0.00..154053.60 rows=9999860 width=9)      +
+      |          |       |                               |   ->  Hash  (cost=154053.60..154053.60 rows=9999860 width=9)                +
+      |          |       |                               |         ->  Seq Scan on test t2  (cost=0.00..154053.60 rows=9999860 width=9)+
+      |          |       |                               |
+(1 row)
+</screen>
+   </para>
+
+   <para>
+    Setting <xref linkend="guc-progressive-explain"/> to <literal>analyze</literal>
+    will enable instrumentation and the detailed plan is printed on a fixed interval
+    controlled by <xref linkend="guc-progressive-explain-interval"/>, including
+    per node accumulated row count and other statistics.
+   </para>
+
+   <para>
+    Progressive explains include additional information per node to help analyzing
+    execution progress:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       current: the plan node currently being processed.
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       never executed: a plan node not processed yet.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+   <para>
+<screen>
+SET progressive_explain = 'analyze';
+SET
+
+SELECT * FROM test t1 INNER JOIN test t2 ON t1.c1=t2.c1;
+</screen>
+   </para>
+   <para>
+<screen>
+SELECT * FROM pg_stat_progress_explain;
+datid | datname  |  pid  |          last_update          |                                                              query_plan
+-------+----------+-------+-------------------------------+---------------------------------------------------------------------------------------------------------------------------------------
+    5 | postgres | 73972 | 2025-03-13 23:41:53.951884-03 | Hash Join  (cost=327879.85..878413.95 rows=9999860 width=18) (actual time=1581.469..2963.158 rows=64862.00 loops=1)                  +
+      |          |       |                               |   Hash Cond: (t1.c1 = t2.c1)                                                                                                         +
+      |          |       |                               |   ->  Seq Scan on test t1  (cost=0.00..154053.60 rows=9999860 width=9) (actual time=0.079..486.702 rows=8258962.00 loops=1) (current)+
+      |          |       |                               |   ->  Hash  (cost=154053.60..154053.60 rows=9999860 width=9) (actual time=1580.933..1580.933 rows=10000000.00 loops=1)               +
+      |          |       |                               |         ->  Seq Scan on test t2  (cost=0.00..154053.60 rows=9999860 width=9) (actual time=0.004..566.961 rows=10000000.00 loops=1)   +
+      |          |       |                               |
+(1 row)
+</screen>
+   </para>
+
+  </sect2>
+
  </sect1>
 
  <sect1 id="planner-stats">
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index b885513f765..28294d802e1 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -36,6 +36,7 @@
 #include "catalog/pg_enum.h"
 #include "catalog/storage.h"
 #include "commands/async.h"
+#include "commands/explain_progressive.h"
 #include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "common/pg_prng.h"
@@ -2423,6 +2424,12 @@ CommitTransaction(void)
 	/* Clean up the type cache */
 	AtEOXact_TypeCache();
 
+	/*
+	 * If progressive explain wasn't properly cleaned after query ended
+	 * perform the cleanup and warn about leaked resources.
+	 */
+	AtEOXact_ProgressiveExplain(true);
+
 	/*
 	 * Make catalog changes visible to all backends.  This has to happen after
 	 * relcache references are dropped (see comments for
@@ -2993,6 +3000,7 @@ AbortTransaction(void)
 		AtEOXact_PgStat(false, is_parallel_worker);
 		AtEOXact_ApplyLauncher(false);
 		AtEOXact_LogicalRepWorkers(false);
+		AtEOXact_ProgressiveExplain(false);
 		pgstat_report_xact_timestamp(0);
 	}
 
@@ -5193,6 +5201,12 @@ CommitSubTransaction(void)
 	AtEOSubXact_PgStat(true, s->nestingLevel);
 	AtSubCommit_Snapshot(s->nestingLevel);
 
+	/*
+	 * If progressive explain wasn't properly cleaned after subxact ended
+	 * perform the cleanup and warn about leaked resources.
+	 */
+	AtEOSubXact_ProgressiveExplain(true, s->nestingLevel);
+
 	/*
 	 * We need to restore the upper transaction's read-only state, in case the
 	 * upper is read-write while the child is read-only; GUC will incorrectly
@@ -5361,6 +5375,7 @@ AbortSubTransaction(void)
 		AtEOSubXact_HashTables(false, s->nestingLevel);
 		AtEOSubXact_PgStat(false, s->nestingLevel);
 		AtSubAbort_Snapshot(s->nestingLevel);
+		AtEOSubXact_ProgressiveExplain(false, s->nestingLevel);
 	}
 
 	/*
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 31d269b7ee0..767735c1a2c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1334,6 +1334,16 @@ CREATE VIEW pg_stat_progress_copy AS
     FROM pg_stat_get_progress_info('COPY') AS S
         LEFT JOIN pg_database D ON S.datid = D.oid;
 
+CREATE VIEW pg_stat_progress_explain AS
+    SELECT
+            S.datid AS datid,
+            D.datname AS datname,
+            S.pid,
+            S.last_update,
+            S.query_plan
+    FROM pg_stat_progress_explain() AS S
+        LEFT JOIN pg_database AS D ON (S.datid = D.oid);
+
 CREATE VIEW pg_user_mappings AS
     SELECT
         U.oid       AS umid,
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index cb2fbdc7c60..e10224b2cd2 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -36,6 +36,7 @@ OBJS = \
 	explain.o \
 	explain_dr.o \
 	explain_format.o \
+	explain_progressive.o \
 	explain_state.o \
 	extension.o \
 	foreigncmds.o \
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ef8aa489af8..179a1f8792b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -20,6 +20,7 @@
 #include "commands/explain.h"
 #include "commands/explain_dr.h"
 #include "commands/explain_format.h"
+#include "commands/explain_progressive.h"
 #include "commands/explain_state.h"
 #include "commands/prepare.h"
 #include "foreign/fdwapi.h"
@@ -139,7 +140,7 @@ static void show_indexsearches_info(PlanState *planstate, ExplainState *es);
 static void show_tidbitmap_info(BitmapHeapScanState *planstate,
 								ExplainState *es);
 static void show_instrumentation_count(const char *qlabel, int which,
-									   PlanState *planstate, ExplainState *es);
+									   Instrumentation *instr, ExplainState *es);
 static void show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es);
 static const char *explain_get_index_name(Oid indexId);
 static bool peek_buffer_usage(ExplainState *es, const BufferUsage *usage);
@@ -596,6 +597,15 @@ ExplainOnePlan(PlannedStmt *plannedstmt, CachedPlan *cplan,
 		/* We can't run ExecutorEnd 'till we're done printing the stats... */
 		totaltime += elapsed_time(&starttime);
 	}
+	else
+	{
+		/*
+		 * Handle progressive explain cleanup manually if enabled as it is not
+		 * called as part of ExecutorFinish.
+		 */
+		if (progressive_explain)
+			ProgressiveExplainFinish(queryDesc);
+	}
 
 	/* grab serialization metrics before we destroy the DestReceiver */
 	if (es->serialize != EXPLAIN_SERIALIZE_NONE)
@@ -1371,6 +1381,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	const char *partialmode = NULL;
 	const char *operation = NULL;
 	const char *custom_name = NULL;
+	Instrumentation *local_instr = NULL;
 	ExplainWorkersState *save_workers_state = es->workers_state;
 	int			save_indent = es->indent;
 	bool		haschildren;
@@ -1834,53 +1845,90 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	 * instrumentation results the user didn't ask for.  But we do the
 	 * InstrEndLoop call anyway, if possible, to reduce the number of cases
 	 * auto_explain has to contend with.
+	 *
+	 * For regular explains instrumentation clean up is called directly in the
+	 * main instrumentation objects. Progressive explains need to clone
+	 * instrumentation object and forcibly end the loop in nodes that may be
+	 * running.
 	 */
 	if (planstate->instrument)
-		InstrEndLoop(planstate->instrument);
-
-	if (es->analyze &&
-		planstate->instrument && planstate->instrument->nloops > 0)
 	{
-		double		nloops = planstate->instrument->nloops;
-		double		startup_ms = 1000.0 * planstate->instrument->startup / nloops;
-		double		total_ms = 1000.0 * planstate->instrument->total / nloops;
-		double		rows = planstate->instrument->ntuples / nloops;
-
-		if (es->format == EXPLAIN_FORMAT_TEXT)
+		/* Progressive explain. Use auxiliary instrumentation object */
+		if (es->progressive)
 		{
-			appendStringInfo(es->str, " (actual ");
-
-			if (es->timing)
-				appendStringInfo(es->str, "time=%.3f..%.3f ", startup_ms, total_ms);
+			local_instr = es->pe_local_instr;
+			*local_instr = *planstate->instrument;
 
-			appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
+			/* Force end loop even if node is in progress */
+			InstrEndLoopForce(local_instr);
 		}
+		/* Use main instrumentation */
 		else
 		{
-			if (es->timing)
-			{
-				ExplainPropertyFloat("Actual Startup Time", "ms", startup_ms,
-									 3, es);
-				ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
-									 3, es);
-			}
-			ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
-			ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+			local_instr = planstate->instrument;
+			InstrEndLoop(local_instr);
 		}
 	}
-	else if (es->analyze)
+
+	/*
+	 * Additional query execution details should only be included if
+	 * instrumentation is enabled and, if progressive explain is enabled, it
+	 * is configured to update the plan more than once.
+	 */
+	if (es->analyze &&
+		(!es->progressive ||
+		 (es->progressive && progressive_explain_interval > 0)))
 	{
-		if (es->format == EXPLAIN_FORMAT_TEXT)
-			appendStringInfoString(es->str, " (never executed)");
+		if (local_instr && local_instr->nloops > 0)
+		{
+			double		nloops = local_instr->nloops;
+			double		startup_ms = 1000.0 * local_instr->startup / nloops;
+			double		total_ms = 1000.0 * local_instr->total / nloops;
+			double		rows = local_instr->ntuples / nloops;
+
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				appendStringInfo(es->str, " (actual ");
+
+				if (es->timing)
+					appendStringInfo(es->str, "time=%.3f..%.3f ", startup_ms, total_ms);
+
+				appendStringInfo(es->str, "rows=%.2f loops=%.0f)", rows, nloops);
+
+				if (es->progressive && planstate == es->pe_curr_node)
+					appendStringInfo(es->str, " (current)");
+			}
+			else
+			{
+				if (es->timing)
+				{
+					ExplainPropertyFloat("Actual Startup Time", "ms", startup_ms,
+										 3, es);
+					ExplainPropertyFloat("Actual Total Time", "ms", total_ms,
+										 3, es);
+				}
+				ExplainPropertyFloat("Actual Rows", NULL, rows, 2, es);
+				ExplainPropertyFloat("Actual Loops", NULL, nloops, 0, es);
+
+				/* Progressive explain. Add current node flag is applicable */
+				if (es->progressive && planstate == es->pe_curr_node)
+					ExplainPropertyBool("Current", true, es);
+			}
+		}
 		else
 		{
-			if (es->timing)
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+				appendStringInfoString(es->str, " (never executed)");
+			else
 			{
-				ExplainPropertyFloat("Actual Startup Time", "ms", 0.0, 3, es);
-				ExplainPropertyFloat("Actual Total Time", "ms", 0.0, 3, es);
+				if (es->timing)
+				{
+					ExplainPropertyFloat("Actual Startup Time", "ms", 0.0, 3, es);
+					ExplainPropertyFloat("Actual Total Time", "ms", 0.0, 3, es);
+				}
+				ExplainPropertyFloat("Actual Rows", NULL, 0.0, 0, es);
+				ExplainPropertyFloat("Actual Loops", NULL, 0.0, 0, es);
 			}
-			ExplainPropertyFloat("Actual Rows", NULL, 0.0, 0, es);
-			ExplainPropertyFloat("Actual Loops", NULL, 0.0, 0, es);
 		}
 	}
 
@@ -1970,13 +2018,13 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						   "Index Cond", planstate, ancestors, es);
 			if (((IndexScan *) plan)->indexqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
-										   planstate, es);
+										   local_instr, es);
 			show_scan_qual(((IndexScan *) plan)->indexorderbyorig,
 						   "Order By", planstate, ancestors, es);
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_indexsearches_info(planstate, es);
 			break;
 		case T_IndexOnlyScan:
@@ -1984,16 +2032,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						   "Index Cond", planstate, ancestors, es);
 			if (((IndexOnlyScan *) plan)->recheckqual)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
-										   planstate, es);
+										   local_instr, es);
 			show_scan_qual(((IndexOnlyScan *) plan)->indexorderby,
 						   "Order By", planstate, ancestors, es);
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			if (es->analyze)
 				ExplainPropertyFloat("Heap Fetches", NULL,
-									 planstate->instrument->ntuples2, 0, es);
+									 local_instr->ntuples2, 0, es);
 			show_indexsearches_info(planstate, es);
 			break;
 		case T_BitmapIndexScan:
@@ -2006,11 +2054,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						   "Recheck Cond", planstate, ancestors, es);
 			if (((BitmapHeapScan *) plan)->bitmapqualorig)
 				show_instrumentation_count("Rows Removed by Index Recheck", 2,
-										   planstate, es);
+										   local_instr, es);
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
 			break;
 		case T_SampleScan:
@@ -2027,7 +2075,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			if (IsA(plan, CteScan))
 				show_ctescan_info(castNode(CteScanState, planstate), es);
 			break;
@@ -2038,7 +2086,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
 					show_instrumentation_count("Rows Removed by Filter", 1,
-											   planstate, es);
+											   local_instr, es);
 				ExplainPropertyInteger("Workers Planned", NULL,
 									   gather->num_workers, es);
 
@@ -2062,7 +2110,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
 					show_instrumentation_count("Rows Removed by Filter", 1,
-											   planstate, es);
+											   local_instr, es);
 				ExplainPropertyInteger("Workers Planned", NULL,
 									   gm->num_workers, es);
 
@@ -2096,7 +2144,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			break;
 		case T_TableFuncScan:
 			if (es->verbose)
@@ -2110,7 +2158,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_table_func_scan_info(castNode(TableFuncScanState,
 											   planstate), es);
 			break;
@@ -2128,7 +2176,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
 					show_instrumentation_count("Rows Removed by Filter", 1,
-											   planstate, es);
+											   local_instr, es);
 			}
 			break;
 		case T_TidRangeScan:
@@ -2145,14 +2193,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
 					show_instrumentation_count("Rows Removed by Filter", 1,
-											   planstate, es);
+											   local_instr, es);
 			}
 			break;
 		case T_ForeignScan:
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_foreignscan_info((ForeignScanState *) planstate, es);
 			break;
 		case T_CustomScan:
@@ -2162,7 +2210,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
 					show_instrumentation_count("Rows Removed by Filter", 1,
-											   planstate, es);
+											   local_instr, es);
 				if (css->methods->ExplainCustomScan)
 					css->methods->ExplainCustomScan(css, ancestors, es);
 			}
@@ -2172,11 +2220,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 							"Join Filter", planstate, ancestors, es);
 			if (((NestLoop *) plan)->join.joinqual)
 				show_instrumentation_count("Rows Removed by Join Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 2,
-										   planstate, es);
+										   local_instr, es);
 			break;
 		case T_MergeJoin:
 			show_upper_qual(((MergeJoin *) plan)->mergeclauses,
@@ -2185,11 +2233,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 							"Join Filter", planstate, ancestors, es);
 			if (((MergeJoin *) plan)->join.joinqual)
 				show_instrumentation_count("Rows Removed by Join Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 2,
-										   planstate, es);
+										   local_instr, es);
 			break;
 		case T_HashJoin:
 			show_upper_qual(((HashJoin *) plan)->hashclauses,
@@ -2198,11 +2246,11 @@ ExplainNode(PlanState *planstate, List *ancestors,
 							"Join Filter", planstate, ancestors, es);
 			if (((HashJoin *) plan)->join.joinqual)
 				show_instrumentation_count("Rows Removed by Join Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 2,
-										   planstate, es);
+										   local_instr, es);
 			break;
 		case T_Agg:
 			show_agg_keys(castNode(AggState, planstate), ancestors, es);
@@ -2210,7 +2258,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_hashagg_info((AggState *) planstate, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			break;
 		case T_WindowAgg:
 			show_window_def(castNode(WindowAggState, planstate), ancestors, es);
@@ -2219,7 +2267,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			show_windowagg_info(castNode(WindowAggState, planstate), es);
 			break;
 		case T_Group:
@@ -2227,7 +2275,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			break;
 		case T_Sort:
 			show_sort_keys(castNode(SortState, planstate), ancestors, es);
@@ -2249,7 +2297,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
 				show_instrumentation_count("Rows Removed by Filter", 1,
-										   planstate, es);
+										   local_instr, es);
 			break;
 		case T_ModifyTable:
 			show_modifytable_info(castNode(ModifyTableState, planstate), ancestors,
@@ -2294,10 +2342,10 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	}
 
 	/* Show buffer/WAL usage */
-	if (es->buffers && planstate->instrument)
-		show_buffer_usage(es, &planstate->instrument->bufusage);
-	if (es->wal && planstate->instrument)
-		show_wal_usage(es, &planstate->instrument->walusage);
+	if (es->buffers && local_instr)
+		show_buffer_usage(es, &local_instr->bufusage);
+	if (es->wal && local_instr)
+		show_wal_usage(es, &local_instr->walusage);
 
 	/* Prepare per-worker buffer/WAL usage */
 	if (es->workers_state && (es->buffers || es->wal) && es->verbose)
@@ -3975,19 +4023,19 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es)
  */
 static void
 show_instrumentation_count(const char *qlabel, int which,
-						   PlanState *planstate, ExplainState *es)
+						   Instrumentation *instr, ExplainState *es)
 {
 	double		nfiltered;
 	double		nloops;
 
-	if (!es->analyze || !planstate->instrument)
+	if (!es->analyze || !instr)
 		return;
 
 	if (which == 2)
-		nfiltered = planstate->instrument->nfiltered2;
+		nfiltered = instr->nfiltered2;
 	else
-		nfiltered = planstate->instrument->nfiltered1;
-	nloops = planstate->instrument->nloops;
+		nfiltered = instr->nfiltered1;
+	nloops = instr->nloops;
 
 	/* In text mode, suppress zero counts; they're not interesting enough */
 	if (nfiltered > 0 || es->format != EXPLAIN_FORMAT_TEXT)
@@ -4668,7 +4716,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 		{
 			show_upper_qual((List *) node->onConflictWhere, "Conflict Filter",
 							&mtstate->ps, ancestors, es);
-			show_instrumentation_count("Rows Removed by Conflict Filter", 1, &mtstate->ps, es);
+			show_instrumentation_count("Rows Removed by Conflict Filter", 1, (&mtstate->ps)->instrument, es);
 		}
 
 		/* EXPLAIN ANALYZE display of actual outcome for each tuple proposed */
@@ -4677,11 +4725,24 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 			double		total;
 			double		insert_path;
 			double		other_path;
+			Instrumentation *local_instr;
 
-			InstrEndLoop(outerPlanState(mtstate)->instrument);
+			/* Progressive explain. Use auxiliary instrumentation object */
+			if (es->progressive)
+			{
+				local_instr = es->pe_local_instr;
+				*local_instr = *outerPlanState(mtstate)->instrument;
+				/* Force end loop even if node is in progress */
+				InstrEndLoopForce(local_instr);
+			}
+			else
+			{
+				local_instr = outerPlanState(mtstate)->instrument;
+				InstrEndLoop(local_instr);
+			}
 
 			/* count the number of source rows */
-			total = outerPlanState(mtstate)->instrument->ntuples;
+			total = local_instr->ntuples;
 			other_path = mtstate->ps.instrument->ntuples2;
 			insert_path = total - other_path;
 
@@ -4701,11 +4762,24 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 			double		update_path;
 			double		delete_path;
 			double		skipped_path;
+			Instrumentation *local_instr;
 
-			InstrEndLoop(outerPlanState(mtstate)->instrument);
+			/* Progressive explain. Use auxiliary instrumentation object */
+			if (es->progressive)
+			{
+				local_instr = es->pe_local_instr;
+				*local_instr = *outerPlanState(mtstate)->instrument;
+				/* Force end loop even if node is in progress */
+				InstrEndLoopForce(local_instr);
+			}
+			else
+			{
+				local_instr = outerPlanState(mtstate)->instrument;
+				InstrEndLoop(local_instr);
+			}
 
 			/* count the number of source rows */
-			total = outerPlanState(mtstate)->instrument->ntuples;
+			total = local_instr->ntuples;
 			insert_path = mtstate->mt_merge_inserted;
 			update_path = mtstate->mt_merge_updated;
 			delete_path = mtstate->mt_merge_deleted;
diff --git a/src/backend/commands/explain_format.c b/src/backend/commands/explain_format.c
index 752691d56db..c0d6973d1e5 100644
--- a/src/backend/commands/explain_format.c
+++ b/src/backend/commands/explain_format.c
@@ -16,6 +16,7 @@
 #include "commands/explain.h"
 #include "commands/explain_format.h"
 #include "commands/explain_state.h"
+#include "utils/guc_tables.h"
 #include "utils/json.h"
 #include "utils/xml.h"
 
@@ -25,6 +26,17 @@
 #define X_CLOSE_IMMEDIATE 2
 #define X_NOWHITESPACE 4
 
+/*
+ * GUC support
+ */
+const struct config_enum_entry explain_format_options[] = {
+	{"text", EXPLAIN_FORMAT_TEXT, false},
+	{"xml", EXPLAIN_FORMAT_XML, false},
+	{"json", EXPLAIN_FORMAT_JSON, false},
+	{"yaml", EXPLAIN_FORMAT_YAML, false},
+	{NULL, 0, false}
+};
+
 static void ExplainJSONLineEnding(ExplainState *es);
 static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es);
 static void ExplainYAMLLineStarting(ExplainState *es);
diff --git a/src/backend/commands/explain_progressive.c b/src/backend/commands/explain_progressive.c
new file mode 100644
index 00000000000..eddd57ccf50
--- /dev/null
+++ b/src/backend/commands/explain_progressive.c
@@ -0,0 +1,493 @@
+/*-------------------------------------------------------------------------
+ *
+ * explain_progressive.c
+ *	  Code for the progressive explain feature
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/explain_progressive.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "catalog/pg_authid.h"
+#include "commands/explain.h"
+#include "commands/explain_format.h"
+#include "commands/explain_progressive.h"
+#include "commands/explain_state.h"
+#include "foreign/fdwapi.h"
+#include "funcapi.h"
+#include "utils/acl.h"
+#include "utils/backend_status.h"
+#include "utils/builtins.h"
+#include "utils/guc_tables.h"
+#include "utils/timeout.h"
+
+
+#define PROGRESSIVE_EXPLAIN_FREE_SIZE 4096
+
+/* Shared hash to store progressive explains */
+static HTAB *progressiveExplainHash = NULL;
+
+/* Pointer to the tracked query */
+static QueryDesc *activeQueryDesc = NULL;
+
+/* Transaction nest level of the tracked query */
+static int	activeQueryXactNestLevel = -1;
+
+/* Flag set by timeouts to control when to update progressive explains */
+bool		ProgressiveExplainPending = false;
+
+static void ProgressiveExplainPrint(QueryDesc *queryDesc);
+static void ProgressiveExplainCleanup(void);
+
+
+
+/*
+ * ProgressiveExplainSetup -
+ *	  Track query descriptor and adjust instrumentation.
+ *
+ * If progressive explain is enabled and configured to collect
+ * instrumentation details, we adjust QueryDesc accordingly even if
+ * the query was not initiated with EXPLAIN ANALYZE. This will
+ * directly affect query execution and add computation overhead.
+ */
+void
+ProgressiveExplainSetup(QueryDesc *queryDesc)
+{
+	/* Setup only if this is the outer most query */
+	if (activeQueryDesc == NULL)
+	{
+		activeQueryDesc = queryDesc;
+		activeQueryXactNestLevel = GetCurrentTransactionNestLevel();
+
+		/*
+		 * Enable instrumentation if the plan will be updated more than once.
+		 */
+		if (progressive_explain_interval > 0)
+		{
+			if (progressive_explain_timing)
+				queryDesc->instrument_options |= INSTRUMENT_TIMER;
+			else
+				queryDesc->instrument_options |= INSTRUMENT_ROWS;
+			if (progressive_explain_buffers)
+				queryDesc->instrument_options |= INSTRUMENT_BUFFERS;
+			if (progressive_explain_wal)
+				queryDesc->instrument_options |= INSTRUMENT_WAL;
+		}
+	}
+}
+
+/*
+ * ProgressiveExplainStart -
+ *	  Responsible for initialization of all structures related to progressive
+ *	  explains.
+ *
+ * We define a ExplainState that will be reused in every iteration of
+ * plan updates.
+ *
+ * Progressive explain plans are updated in shared memory via DSAs. Each
+ * backend updating plans has its own DSA, shared with other backends via
+ * the global progressive explain hash through dsa_handle and
+ * dsa_pointer pointers.
+ *
+ * A periodic timeout is configured to update the plan in fixed intervals if
+ * progressive explain is configured with instrumentation enabled. Otherwise
+ * the plain plan is updated once.
+ */
+void
+ProgressiveExplainStart(QueryDesc *queryDesc)
+{
+	ExplainState *es;
+	ProgressiveExplainHashEntry *entry;
+	bool		found;
+
+	/* Initialize ExplainState to be used for all plan updates */
+	es = NewExplainState();
+	queryDesc->pestate = es;
+
+	/* Local instrumentation object to be reused for every node */
+	es->pe_local_instr = palloc0(sizeof(Instrumentation));
+
+	/*
+	 * Mark ExplainState as progressive so that ExplainNode() function uses a
+	 * special logic when printing the plan.
+	 */
+	es->progressive = true;
+
+	es->analyze = (queryDesc->instrument_options &&
+				   (progressive_explain_interval > 0));
+	es->buffers = (es->analyze && progressive_explain_buffers);
+	es->wal = (es->analyze && progressive_explain_wal);
+	es->timing = (es->analyze && progressive_explain_timing);
+	es->summary = (es->analyze);
+	es->format = progressive_explain_format;
+	es->verbose = progressive_explain_verbose;
+	es->settings = progressive_explain_settings;
+	es->costs = progressive_explain_costs;
+
+	/* Define the DSA and share through the hash */
+	es->pe_a = dsa_create(LWTRANCHE_PROGRESSIVE_EXPLAIN_DSA);
+
+	/* Exclusive access is needed to update the hash */
+	LWLockAcquire(ProgressiveExplainHashLock, LW_EXCLUSIVE);
+
+	/* Find or create an entry with desired hash code */
+	entry = (ProgressiveExplainHashEntry *) hash_search(progressiveExplainHash,
+														&MyProcPid,
+														HASH_ENTER,
+														&found);
+
+	entry->pe_h = dsa_get_handle(es->pe_a);
+	entry->pe_p = (dsa_pointer) NULL;
+
+	LWLockRelease(ProgressiveExplainHashLock);
+
+	/* Enable timeout only if instrumentation is enabled */
+	if (es->analyze)
+		enable_timeout_every(PROGRESSIVE_EXPLAIN_TIMEOUT,
+							 TimestampTzPlusMilliseconds(GetCurrentTimestamp(),
+														 progressive_explain_interval),
+							 progressive_explain_interval);
+
+	/* Print progressive plan for the first time */
+	ProgressiveExplainPrint(queryDesc);
+}
+
+/*
+ * ProgressiveExplainUpdate
+ * Updates progressive explain for instrumented runs.
+ */
+void
+ProgressiveExplainUpdate(PlanState *node)
+{
+	/* Track the current PlanState */
+	node->state->query_desc->pestate->pe_curr_node = node;
+	ProgressiveExplainPrint(node->state->query_desc);
+	node->state->query_desc->pestate->pe_curr_node = NULL;
+
+	/* Reset timeout flag */
+	ProgressiveExplainPending = false;
+}
+
+/*
+ * ProgressiveExplainPrint -
+ *	  Updates progressive explain in memory.
+ *
+ * This function resets the reusable ExplainState, updates the
+ * plan and updates the DSA with new data.
+ *
+ * DSA memory allocation is also done here. Amount of shared
+ * memory allocated depends on size of currently updated plan.
+ * There may be reallocations in subsequent calls if new plans
+ * don't fit in the existing area.
+ */
+void
+ProgressiveExplainPrint(QueryDesc *queryDesc)
+{
+	bool		alloc_needed = false;
+	QueryDesc  *currentQueryDesc = queryDesc;
+	ProgressiveExplainHashEntry *entry;
+	ProgressiveExplainHashData *pe_data;
+	ExplainState *es = queryDesc->pestate;
+
+	/* Reset the string to be reused */
+	resetStringInfo(es->str);
+
+	/* Print the plan */
+	ExplainBeginOutput(es);
+	ExplainPrintPlan(es, currentQueryDesc);
+	ExplainEndOutput(es);
+
+	/* Exclusive access is needed to update the hash */
+	LWLockAcquire(ProgressiveExplainHashLock, LW_EXCLUSIVE);
+	entry = (ProgressiveExplainHashEntry *) hash_search(progressiveExplainHash,
+														&MyProcPid,
+														HASH_FIND,
+														NULL);
+
+	/* Entry must already exist in shared memory at this point */
+	if (!entry)
+		elog(ERROR, "no entry in progressive explain hash for pid %d",
+			 MyProcPid);
+
+	/* Plan was never printed */
+	if (!entry->pe_p)
+		alloc_needed = true;
+	else
+	{
+		pe_data = dsa_get_address(es->pe_a,
+								  entry->pe_p);
+
+		/*
+		 * Plan does not fit in existing shared memory area. Reallocation is
+		 * needed.
+		 */
+		if (strlen(es->str->data) > entry->plan_alloc_size)
+		{
+			dsa_free(es->pe_a, entry->pe_p);
+			alloc_needed = true;
+		}
+	}
+
+	if (alloc_needed)
+	{
+		/*
+		 * The allocated size combines the length of the currently printed
+		 * query plan with an additional delta defined by
+		 * PROGRESSIVE_EXPLAIN_FREE_SIZE. This strategy prevents having to
+		 * reallocate the segment very often, which would be needed in case
+		 * the length of the next printed exceeds the previously allocated
+		 * size.
+		 */
+		entry->plan_alloc_size = add_size(strlen(es->str->data),
+										  PROGRESSIVE_EXPLAIN_FREE_SIZE);
+		entry->pe_p = dsa_allocate(es->pe_a,
+								   add_size(sizeof(ProgressiveExplainHashData),
+											entry->plan_alloc_size));
+		pe_data = dsa_get_address(es->pe_a, entry->pe_p);
+		pe_data->pid = MyProcPid;
+	}
+
+	/* Update shared memory with new data */
+	strcpy(pe_data->plan, es->str->data);
+	pe_data->last_update = GetCurrentTimestamp();
+
+	LWLockRelease(ProgressiveExplainHashLock);
+}
+
+/*
+ * ProgressiveExplainFinish -
+ *	  Finalizes query execution with progressive explain enabled.
+ */
+void
+ProgressiveExplainFinish(QueryDesc *queryDesc)
+{
+	/*
+	 * Progressive explain is only done for the outer most query descriptor.
+	 */
+	if (queryDesc == activeQueryDesc)
+	{
+		ProgressiveExplainCleanup();
+		dsa_detach(queryDesc->pestate->pe_a);
+	}
+}
+
+/*
+ * ProgressiveExplainIsActive -
+ *	  Checks if argument queryDesc is the one being tracked.
+ */
+bool
+ProgressiveExplainIsActive(QueryDesc *queryDesc)
+{
+	return queryDesc == activeQueryDesc;
+}
+
+/*
+ * End-of-transaction cleanup for progressive explains.
+ */
+void
+AtEOXact_ProgressiveExplain(bool isCommit)
+{
+	/* Only perform cleanup if query descriptor is being tracked */
+	if (activeQueryDesc != NULL)
+	{
+		if (isCommit)
+			elog(WARNING, "leaked progressive explain query descriptor");
+		ProgressiveExplainCleanup();
+	}
+}
+
+/*
+ * End-of-subtransaction cleanup for progressive explains.
+ */
+void
+AtEOSubXact_ProgressiveExplain(bool isCommit, int nestDepth)
+{
+	/*
+	 * Only perform cleanup if progressive explain is enabled
+	 * (activeQueryXactNestLevel != -1) and the transaction nested level of
+	 * the aborted subtransaction is greater or equal compared to the level of
+	 * the tracked query. This is to avoid doing cleanup in subtransaction
+	 * aborts triggered by exception blocks in functions and procedures.
+	 */
+	if (activeQueryXactNestLevel >= nestDepth)
+	{
+		if (isCommit)
+			elog(WARNING, "leaked progressive explain query descriptor");
+		ProgressiveExplainCleanup();
+	}
+}
+
+/*
+ * ProgressiveExplainCleanup -
+ *	  Cleanup routine when progressive explain is enabled.
+ *
+ * We need to deal with structures not automatically released by the memory
+ * context removal. Current tasks are:
+ * - remove local backend from progressive explain hash
+ * - detach from DSA used to store shared data
+ */
+void
+ProgressiveExplainCleanup(void)
+{
+	/* Stop timeout */
+	disable_timeout(PROGRESSIVE_EXPLAIN_TIMEOUT, false);
+
+	/* Reset timeout flag */
+	ProgressiveExplainPending = false;
+
+	/* Reset querydesc tracker and nested level */
+	activeQueryDesc = NULL;
+	activeQueryXactNestLevel = -1;
+
+	/* Remove backend from the shared hash */
+	LWLockAcquire(ProgressiveExplainHashLock, LW_EXCLUSIVE);
+	hash_search(progressiveExplainHash, &MyProcPid, HASH_REMOVE, NULL);
+	LWLockRelease(ProgressiveExplainHashLock);
+}
+
+/*
+ * ExecProcNodeInstrExplain -
+ *	  ExecProcNode wrapper that performs instrumentation calls and updates
+ *	  progressive explains.  By keeping this a separate function, we add
+ *	  overhead only when progressive explain is enabled.
+ */
+TupleTableSlot *
+ExecProcNodeInstrExplain(PlanState *node)
+{
+	TupleTableSlot *result;
+
+	InstrStartNode(node->instrument);
+
+	/*
+	 * Update progressive after timeout is reached.
+	 */
+	if (ProgressiveExplainPending)
+		ProgressiveExplainUpdate(node);
+
+	result = node->ExecProcNodeReal(node);
+
+	InstrStopNode(node->instrument, TupIsNull(result) ? 0.0 : 1.0);
+
+	return result;
+}
+
+/*
+ * ProgressiveExplainHashShmemSize
+ * Compute shared memory space needed for explain hash.
+ */
+Size
+ProgressiveExplainHashShmemSize(void)
+{
+	Size		size = 0;
+	long		max_table_size;
+
+	max_table_size = add_size(MaxBackends,
+							  max_parallel_workers);
+	size = add_size(size,
+					hash_estimate_size(max_table_size,
+									   sizeof(ProgressiveExplainHashEntry)));
+
+	return size;
+}
+
+/*
+ * InitProgressiveExplainHash -
+ *	  Initialize hash used to store data of progressive explains.
+ */
+void
+InitProgressiveExplainHash(void)
+{
+	HASHCTL		info;
+
+	info.keysize = sizeof(int);
+	info.entrysize = sizeof(ProgressiveExplainHashEntry);
+
+	progressiveExplainHash = ShmemInitHash("progressive explain hash",
+										   50, 50,
+										   &info,
+										   HASH_ELEM | HASH_BLOBS);
+}
+
+/*
+ * pg_stat_progress_explain -
+ *	  Return the progress of progressive explains.
+ */
+Datum
+pg_stat_progress_explain(PG_FUNCTION_ARGS)
+{
+#define EXPLAIN_ACTIVITY_COLS	4
+	int			num_backends = pgstat_fetch_stat_numbackends();
+	int			curr_backend;
+	HASH_SEQ_STATUS hash_seq;
+	ProgressiveExplainHashEntry *entry;
+	dsa_area   *a;
+	ProgressiveExplainHashData *ped;
+
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	LWLockAcquire(ProgressiveExplainHashLock, LW_SHARED);
+
+	hash_seq_init(&hash_seq, progressiveExplainHash);
+	while ((entry = hash_seq_search(&hash_seq)) != NULL)
+	{
+		Datum		values[EXPLAIN_ACTIVITY_COLS] = {0};
+		bool		nulls[EXPLAIN_ACTIVITY_COLS] = {0};
+
+		/*
+		 * We don't look at a DSA that doesn't contain data yet, or at our own
+		 * row.
+		 */
+		if (!DsaPointerIsValid(entry->pe_p) ||
+			MyProcPid == entry->pid)
+			continue;
+
+		a = dsa_attach(entry->pe_h);
+		ped = dsa_get_address(a, entry->pe_p);
+
+		/* 1-based index */
+		for (curr_backend = 1; curr_backend <= num_backends; curr_backend++)
+		{
+			LocalPgBackendStatus *local_beentry;
+			PgBackendStatus *beentry;
+
+			/* Get the next one in the list */
+			local_beentry = pgstat_get_local_beentry_by_index(curr_backend);
+			beentry = &local_beentry->backendStatus;
+
+			if (beentry->st_procpid == ped->pid)
+			{
+				/* Values available to all callers */
+				if (beentry->st_databaseid != InvalidOid)
+					values[0] = ObjectIdGetDatum(beentry->st_databaseid);
+				else
+					nulls[0] = true;
+
+				values[1] = ped->pid;
+				values[2] = TimestampTzGetDatum(ped->last_update);
+
+				if (has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS) ||
+					has_privs_of_role(GetUserId(), beentry->st_procpid))
+					values[3] = CStringGetTextDatum(ped->plan);
+				else
+					values[3] = CStringGetTextDatum("<insufficient privilege>");
+				break;
+			}
+		}
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+
+		dsa_detach(a);
+
+	}
+	LWLockRelease(ProgressiveExplainHashLock);
+
+	return (Datum) 0;
+}
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index dd4cde41d32..2bb0ac7d286 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -24,6 +24,7 @@ backend_sources += files(
   'explain.c',
   'explain_dr.c',
   'explain_format.c',
+  'explain_progressive.c',
   'explain_state.c',
   'extension.c',
   'foreigncmds.c',
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 2da848970be..89e9fb1bb04 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -43,6 +43,7 @@
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "catalog/partition.h"
+#include "commands/explain_progressive.h"
 #include "commands/matview.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
@@ -160,6 +161,12 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	/* caller must ensure the query's snapshot is active */
 	Assert(GetActiveSnapshot() == queryDesc->snapshot);
 
+	/*
+	 * Setup progressive explain if enabled.
+	 */
+	if (progressive_explain)
+		ProgressiveExplainSetup(queryDesc);
+
 	/*
 	 * If the transaction is read-only, we need to check if any writes are
 	 * planned to non-temporary tables.  EXPLAIN is considered read-only.
@@ -185,6 +192,11 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	estate = CreateExecutorState();
 	queryDesc->estate = estate;
 
+	/*
+	 * Adding back reference to QueryDesc
+	 */
+	estate->query_desc = queryDesc;
+
 	oldcontext = MemoryContextSwitchTo(estate->es_query_cxt);
 
 	/*
@@ -270,6 +282,12 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
 	 */
 	InitPlan(queryDesc, eflags);
 
+	/*
+	 * Start progressive explain if enabled.
+	 */
+	if (progressive_explain)
+		ProgressiveExplainStart(queryDesc);
+
 	MemoryContextSwitchTo(oldcontext);
 
 	return ExecPlanStillValid(queryDesc->estate);
@@ -519,6 +537,9 @@ standard_ExecutorFinish(QueryDesc *queryDesc)
 
 	MemoryContextSwitchTo(oldcontext);
 
+	/* Finish progressive explain if enabled */
+	ProgressiveExplainFinish(queryDesc);
+
 	estate->es_finished = true;
 }
 
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.c
index f5f9cfbeead..7ca0544e45e 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -72,6 +72,7 @@
  */
 #include "postgres.h"
 
+#include "commands/explain_progressive.h"
 #include "executor/executor.h"
 #include "executor/nodeAgg.h"
 #include "executor/nodeAppend.h"
@@ -118,6 +119,7 @@
 #include "executor/nodeWorktablescan.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
+#include "utils/guc.h"
 
 static TupleTableSlot *ExecProcNodeFirst(PlanState *node);
 static TupleTableSlot *ExecProcNodeInstr(PlanState *node);
@@ -462,7 +464,19 @@ ExecProcNodeFirst(PlanState *node)
 	 * have ExecProcNode() directly call the relevant function from now on.
 	 */
 	if (node->instrument)
-		node->ExecProcNode = ExecProcNodeInstr;
+	{
+		/*
+		 * Use instrumented wrapper for progressive explains only if the
+		 * feature is enabled, is configured to update the plan more than once
+		 * and the node belongs to the currently tracked query descriptor.
+		 */
+		if (progressive_explain &&
+			progressive_explain_interval > 0 &&
+			ProgressiveExplainIsActive(node->state->query_desc))
+			node->ExecProcNode = ExecProcNodeInstrExplain;
+		else
+			node->ExecProcNode = ExecProcNodeInstr;
+	}
 	else
 		node->ExecProcNode = node->ExecProcNodeReal;
 
diff --git a/src/backend/executor/instrument.c b/src/backend/executor/instrument.c
index 56e635f4700..6a160ee254f 100644
--- a/src/backend/executor/instrument.c
+++ b/src/backend/executor/instrument.c
@@ -25,6 +25,8 @@ static WalUsage save_pgWalUsage;
 static void BufferUsageAdd(BufferUsage *dst, const BufferUsage *add);
 static void WalUsageAdd(WalUsage *dst, WalUsage *add);
 
+static void InstrEndLoopInternal(Instrumentation *instr, bool force);
+
 
 /* Allocate new instrumentation structure(s) */
 Instrumentation *
@@ -137,7 +139,7 @@ InstrUpdateTupleCount(Instrumentation *instr, double nTuples)
 
 /* Finish a run cycle for a plan node */
 void
-InstrEndLoop(Instrumentation *instr)
+InstrEndLoopInternal(Instrumentation *instr, bool force)
 {
 	double		totaltime;
 
@@ -145,7 +147,7 @@ InstrEndLoop(Instrumentation *instr)
 	if (!instr->running)
 		return;
 
-	if (!INSTR_TIME_IS_ZERO(instr->starttime))
+	if (!INSTR_TIME_IS_ZERO(instr->starttime) && !force)
 		elog(ERROR, "InstrEndLoop called on running node");
 
 	/* Accumulate per-cycle statistics into totals */
@@ -164,6 +166,20 @@ InstrEndLoop(Instrumentation *instr)
 	instr->tuplecount = 0;
 }
 
+/* Safely finish a run cycle for a plan node */
+void
+InstrEndLoop(Instrumentation *instr)
+{
+	InstrEndLoopInternal(instr, false);
+}
+
+/* Forcibly finish a run cycle for a plan node */
+void
+InstrEndLoopForce(Instrumentation *instr)
+{
+	InstrEndLoopInternal(instr, true);
+}
+
 /* aggregate instrumentation information */
 void
 InstrAggNode(Instrumentation *dst, Instrumentation *add)
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..389f5b55831 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "access/xlogrecovery.h"
 #include "commands/async.h"
+#include "commands/explain_progressive.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/autovacuum.h"
@@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, InjectionPointShmemSize());
 	size = add_size(size, SlotSyncShmemSize());
 	size = add_size(size, AioShmemSize());
+	size = add_size(size, ProgressiveExplainHashShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -302,6 +304,11 @@ CreateOrAttachShmemStructs(void)
 	 */
 	PredicateLockShmemInit();
 
+	/*
+	 * Set up instrumented explain hash table
+	 */
+	InitProgressiveExplainHash();
+
 	/*
 	 * Set up process table
 	 */
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 3df29658f18..5b913e2eff0 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -178,6 +178,7 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_XACT_SLRU] = "XactSLRU",
 	[LWTRANCHE_PARALLEL_VACUUM_DSA] = "ParallelVacuumDSA",
 	[LWTRANCHE_AIO_URING_COMPLETION] = "AioUringCompletion",
+	[LWTRANCHE_PROGRESSIVE_EXPLAIN_DSA] = "ProgressiveExplainDSA",
 };
 
 StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 8164d0fbb4f..081966ca267 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -102,6 +102,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt,
 	/* not yet executed */
 	qd->already_executed = false;
 
+	/* null until set by progressive explains */
+	qd->pestate = NULL;
+
 	return qd;
 }
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4f44648aca8..c00f985b5b8 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -351,6 +351,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+ProgressiveExplainHash	"Waiting to access backend progressive explain shared hash table."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 7958ea11b73..e070509b403 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -33,6 +33,7 @@
 #include "catalog/pg_database.h"
 #include "catalog/pg_db_role_setting.h"
 #include "catalog/pg_tablespace.h"
+#include "commands/explain_progressive.h"
 #include "libpq/auth.h"
 #include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
@@ -82,6 +83,7 @@ static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
+static void ProgressiveExplainTimeoutHandler(void);
 static bool ThereIsAtLeastOneRole(void);
 static void process_startup_options(Port *port, bool am_superuser);
 static void process_settings(Oid databaseid, Oid roleid);
@@ -771,6 +773,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
 						IdleStatsUpdateTimeoutHandler);
+		RegisterTimeout(PROGRESSIVE_EXPLAIN_TIMEOUT,
+						ProgressiveExplainTimeoutHandler);
 	}
 
 	/*
@@ -1432,6 +1436,12 @@ ClientCheckTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+ProgressiveExplainTimeoutHandler(void)
+{
+	ProgressiveExplainPending = true;
+}
+
 /*
  * Returns true if at least one role is defined in this database cluster.
  */
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 76c7c6bb4b1..d260cbbe18c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -41,6 +41,8 @@
 #include "commands/async.h"
 #include "commands/extension.h"
 #include "commands/event_trigger.h"
+#include "commands/explain_progressive.h"
+#include "commands/explain_state.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
 #include "commands/user.h"
@@ -533,6 +535,15 @@ int			log_parameter_max_length_on_error = 0;
 int			log_temp_files = -1;
 double		log_statement_sample_rate = 1.0;
 double		log_xact_sample_rate = 0;
+bool		progressive_explain = false;
+bool		progressive_explain_verbose = false;
+bool		progressive_explain_settings = false;
+bool		progressive_explain_timing = true;
+bool		progressive_explain_buffers = false;
+bool		progressive_explain_wal = false;
+bool		progressive_explain_costs = true;
+int			progressive_explain_interval = 0;
+int			progressive_explain_format = EXPLAIN_FORMAT_TEXT;
 char	   *backtrace_functions;
 
 int			temp_file_limit = -1;
@@ -2131,6 +2142,83 @@ struct config_bool ConfigureNamesBool[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"progressive_explain", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Enables progressive explains."),
+			gettext_noop("Explain output is visible via pg_stat_progress_explain."),
+			GUC_EXPLAIN
+		},
+		&progressive_explain,
+		false,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"progressive_explain_verbose", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Controls whether verbose details are added to progressive explains."),
+			gettext_noop("Equivalent to the VERBOSE option of EXPLAIN."),
+			GUC_EXPLAIN
+		},
+		&progressive_explain_verbose,
+		false,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"progressive_explain_settings", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Controls whether information on modified configuration is added to progressive explains."),
+			gettext_noop("Equivalent to the SETTINGS option of EXPLAIN."),
+			GUC_EXPLAIN
+		},
+		&progressive_explain_settings,
+		false,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"progressive_explain_timing", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Controls whether information on per node timing is added to progressive explains."),
+			gettext_noop("Equivalent to the TIMING option of EXPLAIN."),
+			GUC_EXPLAIN
+		},
+		&progressive_explain_timing,
+		true,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"progressive_explain_buffers", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Controls whether information on buffer usage is added to progressive explains."),
+			gettext_noop("Equivalent to the BUFFERS option of EXPLAIN."),
+			GUC_EXPLAIN
+		},
+		&progressive_explain_buffers,
+		false,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"progressive_explain_wal", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Controls whether information on WAL record generation is added to progressive explains."),
+			gettext_noop("Equivalent to the WAL option of EXPLAIN."),
+			GUC_EXPLAIN
+		},
+		&progressive_explain_wal,
+		false,
+		NULL, NULL, NULL
+	},
+
+	{
+		{"progressive_explain_costs", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Controls whether information on the estimated startup and total cost of each plan node is added to progressive explains."),
+			gettext_noop("Equivalent to the COSTS option of EXPLAIN."),
+			GUC_EXPLAIN
+		},
+		&progressive_explain_costs,
+		true,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL
@@ -3848,6 +3936,18 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"progressive_explain_interval", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Sets the interval between instrumented progressive "
+						 "explains."),
+			NULL,
+			GUC_UNIT_MS
+		},
+		&progressive_explain_interval,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL
@@ -5396,6 +5496,16 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, assign_io_method, NULL
 	},
 
+	{
+		{"progressive_explain_format", PGC_USERSET, STATS_MONITORING,
+			gettext_noop("Selects the EXPLAIN output format to be used with progressive explains."),
+			gettext_noop("Equivalent to the FORMAT option of EXPLAIN.")
+		},
+		&progressive_explain_format,
+		EXPLAIN_FORMAT_TEXT, explain_format_options,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 7c12434efa2..f58d9745105 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -670,6 +670,19 @@
 #log_executor_stats = off
 
 
+# - Progressive Explain -
+
+#progressive_explain = off
+#progressive_explain_interval = 0
+#progressive_explain_format = text
+#progressive_explain_settings = off
+#progressive_explain_verbose = off
+#progressive_explain_buffers = off
+#progressive_explain_wal = off
+#progressive_explain_timing = on
+#progressive_explain_costs = on
+
+
 #------------------------------------------------------------------------------
 # VACUUMING
 #------------------------------------------------------------------------------
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8b68b16d79d..69092d9ccc8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12493,4 +12493,14 @@
   proargtypes => 'int4',
   prosrc => 'gist_stratnum_common' },
 
+{ oid => '8770',
+  descr => 'statistics: information about progress of backends running statements',
+  proname => 'pg_stat_progress_explain', prorows => '100', proisstrict => 'f',
+  proretset => 't', provolatile => 's', proparallel => 'r',
+  prorettype => 'record', proargtypes => '',
+  proallargtypes => '{oid,int4,timestamptz,text}',
+  proargmodes => '{o,o,o,o}',
+  proargnames => '{datid,pid,last_update,query_plan}',
+  prosrc => 'pg_stat_progress_explain' },
+
 ]
diff --git a/src/include/commands/explain_progressive.h b/src/include/commands/explain_progressive.h
new file mode 100644
index 00000000000..0926680c15e
--- /dev/null
+++ b/src/include/commands/explain_progressive.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * explain_progressive.h
+ *	  prototypes for explain_progressive.c
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * src/include/commands/explain_progressive.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXPLAIN_PROGRESSIVE_H
+#define EXPLAIN_PROGRESSIVE_H
+
+#include "datatype/timestamp.h"
+#include "executor/executor.h"
+
+typedef struct ProgressiveExplainHashEntry
+{
+	int			pid;			/* hash key of entry - MUST BE FIRST */
+	int			plan_alloc_size;
+	dsa_handle	pe_h;
+	dsa_pointer pe_p;
+} ProgressiveExplainHashEntry;
+
+typedef struct ProgressiveExplainHashData
+{
+	int			pid;
+	TimestampTz last_update;
+	char		plan[FLEXIBLE_ARRAY_MEMBER];
+} ProgressiveExplainHashData;
+
+extern bool ProgressiveExplainIsActive(QueryDesc *queryDesc);
+extern void ProgressiveExplainSetup(QueryDesc *queryDesc);
+extern void ProgressiveExplainStart(QueryDesc *queryDesc);
+extern void ProgressiveExplainTrigger(void);
+extern void ProgressiveExplainUpdate(PlanState *node);
+extern void ProgressiveExplainFinish(QueryDesc *queryDesc);
+extern Size ProgressiveExplainHashShmemSize(void);
+extern void InitProgressiveExplainHash(void);
+extern TupleTableSlot *ExecProcNodeInstrExplain(PlanState *node);
+
+/* transaction cleanup code */
+extern void AtEOXact_ProgressiveExplain(bool isCommit);
+extern void AtEOSubXact_ProgressiveExplain(bool isCommit, int nestDepth);
+
+extern bool ProgressiveExplainPending;
+
+#endif							/* EXPLAIN_PROGRESSIVE_H */
diff --git a/src/include/commands/explain_state.h b/src/include/commands/explain_state.h
index 32728f5d1a1..64370a5d6ea 100644
--- a/src/include/commands/explain_state.h
+++ b/src/include/commands/explain_state.h
@@ -16,6 +16,7 @@
 #include "nodes/parsenodes.h"
 #include "nodes/plannodes.h"
 #include "parser/parse_node.h"
+#include "utils/dsa.h"
 
 typedef enum ExplainSerializeOption
 {
@@ -74,6 +75,14 @@ typedef struct ExplainState
 	/* extensions */
 	void	  **extension_state;
 	int			extension_state_allocated;
+	/* set if tracking a progressive explain */
+	bool		progressive;
+	/* current plan node in progressive explains */
+	struct PlanState *pe_curr_node;
+	/* reusable instr object used in progressive explains */
+	struct Instrumentation *pe_local_instr;
+	/* dsa area used to store progressive explain data */
+	dsa_area   *pe_a;
 } ExplainState;
 
 typedef void (*ExplainOptionHandler) (ExplainState *, DefElem *, ParseState *);
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index ba53305ad42..27692aee542 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -48,6 +48,7 @@ typedef struct QueryDesc
 	TupleDesc	tupDesc;		/* descriptor for result tuples */
 	EState	   *estate;			/* executor's query-wide state */
 	PlanState  *planstate;		/* tree of per-plan-node state */
+	struct ExplainState *pestate;	/* progressive explain state if enabled */
 
 	/* This field is set by ExecutePlan */
 	bool		already_executed;	/* true if previously executed */
diff --git a/src/include/executor/instrument.h b/src/include/executor/instrument.h
index 03653ab6c6c..21de2a5632d 100644
--- a/src/include/executor/instrument.h
+++ b/src/include/executor/instrument.h
@@ -109,6 +109,7 @@ extern void InstrStartNode(Instrumentation *instr);
 extern void InstrStopNode(Instrumentation *instr, double nTuples);
 extern void InstrUpdateTupleCount(Instrumentation *instr, double nTuples);
 extern void InstrEndLoop(Instrumentation *instr);
+extern void InstrEndLoopForce(Instrumentation *instr);
 extern void InstrAggNode(Instrumentation *dst, Instrumentation *add);
 extern void InstrStartParallelQuery(void);
 extern void InstrEndParallelQuery(BufferUsage *bufusage, WalUsage *walusage);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5b6cadb5a6c..b7d2d0458de 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -57,6 +57,7 @@ struct ExprState;
 struct ExprContext;
 struct RangeTblEntry;			/* avoid including parsenodes.h here */
 struct ExprEvalStep;			/* avoid including execExpr.h everywhere */
+struct QueryDesc;				/* avoid including execdesc.h here */
 struct CopyMultiInsertBuffer;
 struct LogicalTapeSet;
 
@@ -769,6 +770,9 @@ typedef struct EState
 	 */
 	List	   *es_insert_pending_result_relations;
 	List	   *es_insert_pending_modifytables;
+
+	/* Reference to query descriptor */
+	struct QueryDesc *query_desc;
 } EState;
 
 
@@ -1165,6 +1169,8 @@ typedef struct PlanState
 	ExecProcNodeMtd ExecProcNode;	/* function to return next tuple */
 	ExecProcNodeMtd ExecProcNodeReal;	/* actual function, if above is a
 										 * wrapper */
+	ExecProcNodeMtd ExecProcNodeOriginal;	/* pointer to original function
+											 * another wrapper was added */
 
 	Instrumentation *instrument;	/* Optional runtime stats for this node */
 	WorkerInstrumentation *worker_instrument;	/* per-worker instrumentation */
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 4df1d25c045..a8cff27646c 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -219,6 +219,7 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_XACT_SLRU,
 	LWTRANCHE_PARALLEL_VACUUM_DSA,
 	LWTRANCHE_AIO_URING_COMPLETION,
+	LWTRANCHE_PROGRESSIVE_EXPLAIN_DSA,
 	LWTRANCHE_FIRST_USER_DEFINED,
 }			BuiltinTrancheIds;
 
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 932024b1b0b..7d88e7e9b58 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -84,3 +84,4 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, ProgressiveExplainHash)
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index f619100467d..6bb9d36b003 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -278,6 +278,15 @@ extern PGDLLIMPORT int log_min_duration_statement;
 extern PGDLLIMPORT int log_temp_files;
 extern PGDLLIMPORT double log_statement_sample_rate;
 extern PGDLLIMPORT double log_xact_sample_rate;
+extern PGDLLIMPORT bool progressive_explain;
+extern PGDLLIMPORT int progressive_explain_interval;
+extern PGDLLIMPORT int progressive_explain_format;
+extern PGDLLIMPORT bool progressive_explain_verbose;
+extern PGDLLIMPORT bool progressive_explain_settings;
+extern PGDLLIMPORT bool progressive_explain_timing;
+extern PGDLLIMPORT bool progressive_explain_buffers;
+extern PGDLLIMPORT bool progressive_explain_wal;
+extern PGDLLIMPORT bool progressive_explain_costs;
 extern PGDLLIMPORT char *backtrace_functions;
 
 extern PGDLLIMPORT int temp_file_limit;
@@ -322,6 +331,7 @@ extern PGDLLIMPORT const struct config_enum_entry io_method_options[];
 extern PGDLLIMPORT const struct config_enum_entry recovery_target_action_options[];
 extern PGDLLIMPORT const struct config_enum_entry wal_level_options[];
 extern PGDLLIMPORT const struct config_enum_entry wal_sync_method_options[];
+extern PGDLLIMPORT const struct config_enum_entry explain_format_options[];
 
 /*
  * Functions exported by guc.c
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 7b19beafdc9..f2751c5b4df 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -36,6 +36,7 @@ typedef enum TimeoutId
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
 	STARTUP_PROGRESS_TIMEOUT,
+	PROGRESSIVE_EXPLAIN_TIMEOUT,
 	/* First user-definable timeout reason */
 	USER_TIMEOUT,
 	/* Maximum number of timeout reasons */
diff --git a/src/test/modules/test_misc/t/008_progressive_explain.pl b/src/test/modules/test_misc/t/008_progressive_explain.pl
new file mode 100644
index 00000000000..895031524ec
--- /dev/null
+++ b/src/test/modules/test_misc/t/008_progressive_explain.pl
@@ -0,0 +1,128 @@
+# Copyright (c) 2023-2025, PostgreSQL Global Development Group
+#
+# Test progressive explain
+#
+# We need to make sure pg_stat_progress_explain does not show rows for the local
+# session, even if progressive explain is enabled. For other sessions pg_stat_progress_explain
+# should contain data for a PID only if progressive_explain is enabled and a query
+# is running. Data needs to be removed when query finishes (or gets cancelled).
+
+use strict;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize node
+my $node = PostgreSQL::Test::Cluster->new('progressive_explain');
+
+$node->init;
+# Configure progressive explain to be logged immediately
+$node->append_conf('postgresql.conf', 'progressive_explain_interval = 0');
+$node->start;
+
+# Test for local session
+sub test_local_session
+{
+	my $setting = $_[0];
+	# Make sure local session does not appear in pg_stat_progress_explain
+	my $count = $node->safe_psql(
+		'postgres', qq[
+	SET progressive_explain='$setting';
+	SELECT count(*) from pg_stat_progress_explain WHERE pid = pg_backend_pid()
+	]);
+
+	ok($count == "0",
+		"Session cannot see its own explain with progressive_explain set to ${setting}");
+}
+
+# Tests for peer session
+sub test_peer_session
+{
+	my $setting = $_[0];
+	my $ret;
+
+	# Start a background session and get its PID
+	my $background_psql = $node->background_psql(
+		'postgres',
+		on_error_stop => 0);
+
+	my $pid = $background_psql->query_safe(
+		qq[
+		SELECT pg_backend_pid();
+	]);
+
+	# Configure progressive explain in background session and run a simple query
+	# letting it finish
+	$background_psql->query_safe(
+		qq[
+		SET progressive_explain='$setting';
+		SELECT 1;
+	]);
+
+	# Check that pg_stat_progress_explain contains no row for the PID that finished
+	# its query gracefully
+	$ret = $node->safe_psql(
+		'postgres', qq[
+	SELECT count(*) FROM pg_stat_progress_explain where pid = $pid
+	]);
+
+	ok($ret == "0",
+		"pg_stat_progress_explain empty for completed query with progressive_explain set to ${setting}");
+
+	# Start query in background session and leave it running
+	$background_psql->query_until(
+		qr/start/, q(
+	\echo start
+	SELECT pg_sleep(600);
+	));
+
+	$ret = $node->safe_psql(
+		'postgres', qq[
+	SELECT count(*) FROM pg_stat_progress_explain where pid = $pid
+	]);
+
+	# If progressive_explain is disabled pg_stat_progress_explain should not contain
+	# row for PID
+	if ($setting eq 'off') {
+		ok($ret == "0",
+			"pg_stat_progress_explain empty for running query with progressive_explain set to ${setting}");
+	}
+	# 1 row for pid is expected for running query
+	else {
+		ok($ret == "1",
+			"pg_stat_progress_explain with 1 row for running query with progressive_explain set to ${setting}");
+	}
+
+	# Terminate running query and make sure it is gone
+	$node->safe_psql(
+		'postgres', qq[
+	SELECT pg_cancel_backend($pid);
+	]);
+
+	$node->poll_query_until(
+		'postgres', qq[
+		SELECT count(*) = 0 FROM pg_stat_activity
+		WHERE pid = $pid and state = 'active'
+	]);
+
+	# Check again pg_stat_progress_explain and confirm that the existing row is
+	# now gone
+	$ret = $node->safe_psql(
+		'postgres', qq[
+	SELECT count(*) FROM pg_stat_progress_explain where pid = $pid
+	]);
+
+	ok($ret == "0",
+		"pg_stat_progress_explain empty for canceled query with progressive_explain set to ${setting}");
+}
+
+# Run tests for the local session
+test_local_session('off');
+test_local_session('on');
+
+# Run tests for peer session
+test_peer_session('off');
+test_peer_session('on');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 47478969135..62b70cf4618 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2041,6 +2041,13 @@ pg_stat_progress_create_index| SELECT s.pid,
     s.param15 AS partitions_done
    FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_explain| SELECT s.datid,
+    d.datname,
+    s.pid,
+    s.last_update,
+    s.query_plan
+   FROM (pg_stat_progress_explain() s(datid, pid, last_update, query_plan)
+     LEFT JOIN pg_database d ON ((s.datid = d.oid)));
 pg_stat_progress_vacuum| SELECT s.pid,
     s.datid,
     d.datname,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b66cecd8799..412e47eda9a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2302,6 +2302,8 @@ ProcessUtilityContext
 ProcessUtility_hook_type
 ProcessingMode
 ProgressCommandType
+ProgressiveExplainHashData
+ProgressiveExplainHashEntry
 ProjectSet
 ProjectSetPath
 ProjectSetState
-- 
2.43.0

