From be4c08ea278ac9c506a87a5a0a6e6b19f3f2d610 Mon Sep 17 00:00:00 2001
From: Anthonin Bonnefoy <anthonin.bonnefoy@gmail.com>
Date: Mon, 15 Jul 2024 07:23:39 +0200
Subject: Add ALL_CANDIDATES explain option

When ALL_CANDIDATES is provided, the query planner is forced to keep all
paths instead of discarding the more expensive option. From those paths,
a list of PlannedStmt is generated which is then outputed by explain.

Due to how planning mutates the PlannerGlobal, we can't directly iterate
the path list generated by the subquery_planner and create a planned
statement for them. Instead, we need to plan from scratch for every path
in the pathlist to generate the list of PlannedStmt.
---
 doc/src/sgml/ref/explain.sgml         |  38 +++
 src/backend/commands/explain.c        |  57 +++-
 src/backend/optimizer/plan/planner.c  | 402 ++++++++++++++++----------
 src/backend/optimizer/util/pathnode.c |  18 ++
 src/backend/tcop/postgres.c           |  44 +++
 src/bin/psql/tab-complete.c           |   4 +-
 src/include/commands/explain.h        |   1 +
 src/include/nodes/pathnodes.h         |   3 +
 src/include/optimizer/optimizer.h     |   2 +
 src/include/tcop/tcopprot.h           |   3 +
 src/test/regress/expected/explain.out | 115 ++++++++
 src/test/regress/sql/explain.sql      |  12 +
 12 files changed, 536 insertions(+), 163 deletions(-)

diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index db9d3a8549a..d12bc80efb4 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -46,6 +46,7 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep
     TIMING [ <replaceable class="parameter">boolean</replaceable> ]
     SUMMARY [ <replaceable class="parameter">boolean</replaceable> ]
     MEMORY [ <replaceable class="parameter">boolean</replaceable> ]
+    ALL_CANDIDATES [ <replaceable class="parameter">boolean</replaceable> ]
     FORMAT { TEXT | XML | JSON | YAML }
 </synopsis>
  </refsynopsisdiv>
@@ -293,6 +294,22 @@ ROLLBACK;
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>ALL_CANDIDATES</literal></term>
+    <listitem>
+     <para>
+      Print all plan candidates. By default, the planner discards more expensive
+      plans. <literal>ALL_CANDIDATES</literal> forces the planner to keep all
+      plans which will be printed by <command>EXPLAIN</command>. The plans are
+      sorted from the most expensive at the top to the cheapest at the bottom.
+      The last plan and is the plan that is displayed by a normal
+      <command>EXPLAIN</command>.
+      This parameter may only be used when <literal>ANALYZE</literal> is not
+      enabled. It defaults to <literal>FALSE</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>FORMAT</literal></term>
     <listitem>
@@ -465,6 +482,27 @@ EXPLAIN (COSTS FALSE) SELECT * FROM foo WHERE i = 4;
  Index Scan using fi on foo
    Index Cond: (i = 4)
 (2 rows)
+</programlisting>
+  </para>
+  <para>
+   Here is the same plan with all plan candidates:
+
+<programlisting>
+EXPLAIN (ALL_CANDIDATES) SELECT * FROM foo where id = 4;
+                                   QUERY PLAN
+---------------------------------------------------------------------------------
+ Plan 1
+   ->  Seq Scan on foo  (cost=0.00..170.00 rows=1 width=4)
+         Filter: (id = 4)
+ Plan 2
+   ->  Bitmap Heap Scan on foo  (cost=4.29..8.31 rows=1 width=4)
+         Recheck Cond: (id = 4)
+         ->  Bitmap Index Scan on foo_id_idx  (cost=0.00..4.29 rows=1 width=0)
+               Index Cond: (id = 4)
+ Plan 3
+   ->  Index Only Scan using foo_id_idx on foo  (cost=0.29..4.30 rows=1 width=4)
+         Index Cond: (id = 4)
+(11 rows)
 </programlisting>
   </para>
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5771aabf40a..b850a8f7cbd 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -212,6 +212,8 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 			es->settings = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "generic_plan") == 0)
 			es->generic = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "all_candidates") == 0)
+			es->all_candidates = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "timing") == 0)
 		{
 			timing_set = true;
@@ -303,6 +305,11 @@ ExplainQuery(ParseState *pstate, ExplainStmt *stmt,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together")));
 
+	if (es->all_candidates && es->analyze)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("EXPLAIN options ANALYZE and ALL_CANDIDATES cannot be used together")));
+
 	/* if the summary was not set explicitly, set default value */
 	es->summary = (summary_set) ? es->summary : es->analyze;
 
@@ -458,7 +465,10 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 						 const char *queryString, ParamListInfo params,
 						 QueryEnvironment *queryEnv)
 {
-	PlannedStmt *plan;
+	ListCell   *plan_list;
+	List	   *plans = NULL;
+	PlannedStmt *plan = NULL;
+	int			plan_number = 0;
 	instr_time	planstart,
 				planduration;
 	BufferUsage bufusage_start,
@@ -488,7 +498,10 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	INSTR_TIME_SET_CURRENT(planstart);
 
 	/* plan the query */
-	plan = pg_plan_query(query, queryString, cursorOptions, params);
+	if (es->all_candidates)
+		plans = pg_plan_query_all_candidates(query, queryString, cursorOptions, params);
+	else
+		plan = pg_plan_query(query, queryString, cursorOptions, params);
 
 	INSTR_TIME_SET_CURRENT(planduration);
 	INSTR_TIME_SUBTRACT(planduration, planstart);
@@ -507,9 +520,43 @@ standard_ExplainOneQuery(Query *query, int cursorOptions,
 	}
 
 	/* run it (if needed) and produce output */
-	ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
-				   &planduration, (es->buffers ? &bufusage : NULL),
-				   es->memory ? &mem_counters : NULL);
+	if (es->all_candidates)
+	{
+		/* We have a list of plan candidates, print them */
+		foreach(plan_list, plans)
+		{
+			char	   *plan_name;
+
+			plan = lfirst(plan_list);
+
+			plan_number++;
+			plan_name = psprintf("Plan %d", plan_number);
+
+			ExplainOpenGroup(plan_name, NULL, true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+			{
+				ExplainIndentText(es);
+				appendStringInfoString(es->str, plan_name);
+				appendStringInfoString(es->str, "\n");
+				es->indent++;
+			}
+
+			ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+						   &planduration, (es->buffers ? &bufusage : NULL),
+						   es->memory ? &mem_counters : NULL);
+
+			ExplainCloseGroup(plan_name, NULL, true, es);
+			if (es->format == EXPLAIN_FORMAT_TEXT)
+				es->indent--;
+		}
+	}
+	else
+	{
+		/* We have the top plan from the planner, print it */
+		ExplainOnePlan(plan, into, es, queryString, params, queryEnv,
+					   &planduration, (es->buffers ? &bufusage : NULL),
+					   es->memory ? &mem_counters : NULL);
+	}
 }
 
 /*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 8e6dd23df94..9a57d4f07bc 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -283,142 +283,13 @@ planner(Query *parse, const char *query_string, int cursorOptions,
 	return result;
 }
 
-PlannedStmt *
-standard_planner(Query *parse, const char *query_string, int cursorOptions,
-				 ParamListInfo boundParams)
+static PlannedStmt *
+create_planned_stmt(PlannerInfo *root, Query *parse, Plan *plan, int cursorOptions)
 {
 	PlannedStmt *result;
-	PlannerGlobal *glob;
-	double		tuple_fraction;
-	PlannerInfo *root;
-	RelOptInfo *final_rel;
-	Path	   *best_path;
-	Plan	   *top_plan;
 	ListCell   *lp,
 			   *lr;
-
-	/*
-	 * Set up global state for this planner invocation.  This data is needed
-	 * across all levels of sub-Query that might exist in the given command,
-	 * so we keep it in a separate struct that's linked to by each per-Query
-	 * PlannerInfo.
-	 */
-	glob = makeNode(PlannerGlobal);
-
-	glob->boundParams = boundParams;
-	glob->subplans = NIL;
-	glob->subpaths = NIL;
-	glob->subroots = NIL;
-	glob->rewindPlanIDs = NULL;
-	glob->finalrtable = NIL;
-	glob->finalrteperminfos = NIL;
-	glob->finalrowmarks = NIL;
-	glob->resultRelations = NIL;
-	glob->appendRelations = NIL;
-	glob->relationOids = NIL;
-	glob->invalItems = NIL;
-	glob->paramExecTypes = NIL;
-	glob->lastPHId = 0;
-	glob->lastRowMarkId = 0;
-	glob->lastPlanNodeId = 0;
-	glob->transientPlan = false;
-	glob->dependsOnRole = false;
-
-	/*
-	 * Assess whether it's feasible to use parallel mode for this query. We
-	 * can't do this in a standalone backend, or if the command will try to
-	 * modify any data, or if this is a cursor operation, or if GUCs are set
-	 * to values that don't permit parallelism, or if parallel-unsafe
-	 * functions are present in the query tree.
-	 *
-	 * (Note that we do allow CREATE TABLE AS, SELECT INTO, and CREATE
-	 * MATERIALIZED VIEW to use parallel plans, but this is safe only because
-	 * the command is writing into a completely new table which workers won't
-	 * be able to see.  If the workers could see the table, the fact that
-	 * group locking would cause them to ignore the leader's heavyweight GIN
-	 * page locks would make this unsafe.  We'll have to fix that somehow if
-	 * we want to allow parallel inserts in general; updates and deletes have
-	 * additional problems especially around combo CIDs.)
-	 *
-	 * For now, we don't try to use parallel mode if we're running inside a
-	 * parallel worker.  We might eventually be able to relax this
-	 * restriction, but for now it seems best not to have parallel workers
-	 * trying to create their own parallel workers.
-	 */
-	if ((cursorOptions & CURSOR_OPT_PARALLEL_OK) != 0 &&
-		IsUnderPostmaster &&
-		parse->commandType == CMD_SELECT &&
-		!parse->hasModifyingCTE &&
-		max_parallel_workers_per_gather > 0 &&
-		!IsParallelWorker())
-	{
-		/* all the cheap tests pass, so scan the query tree */
-		glob->maxParallelHazard = max_parallel_hazard(parse);
-		glob->parallelModeOK = (glob->maxParallelHazard != PROPARALLEL_UNSAFE);
-	}
-	else
-	{
-		/* skip the query tree scan, just assume it's unsafe */
-		glob->maxParallelHazard = PROPARALLEL_UNSAFE;
-		glob->parallelModeOK = false;
-	}
-
-	/*
-	 * glob->parallelModeNeeded is normally set to false here and changed to
-	 * true during plan creation if a Gather or Gather Merge plan is actually
-	 * created (cf. create_gather_plan, create_gather_merge_plan).
-	 *
-	 * However, if debug_parallel_query = on or debug_parallel_query =
-	 * regress, then we impose parallel mode whenever it's safe to do so, even
-	 * if the final plan doesn't use parallelism.  It's not safe to do so if
-	 * the query contains anything parallel-unsafe; parallelModeOK will be
-	 * false in that case.  Note that parallelModeOK can't change after this
-	 * point. Otherwise, everything in the query is either parallel-safe or
-	 * parallel-restricted, and in either case it should be OK to impose
-	 * parallel-mode restrictions.  If that ends up breaking something, then
-	 * either some function the user included in the query is incorrectly
-	 * labeled as parallel-safe or parallel-restricted when in reality it's
-	 * parallel-unsafe, or else the query planner itself has a bug.
-	 */
-	glob->parallelModeNeeded = glob->parallelModeOK &&
-		(debug_parallel_query != DEBUG_PARALLEL_OFF);
-
-	/* Determine what fraction of the plan is likely to be scanned */
-	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
-	{
-		/*
-		 * We have no real idea how many tuples the user will ultimately FETCH
-		 * from a cursor, but it is often the case that he doesn't want 'em
-		 * all, or would prefer a fast-start plan anyway so that he can
-		 * process some of the tuples sooner.  Use a GUC parameter to decide
-		 * what fraction to optimize for.
-		 */
-		tuple_fraction = cursor_tuple_fraction;
-
-		/*
-		 * We document cursor_tuple_fraction as simply being a fraction, which
-		 * means the edge cases 0 and 1 have to be treated specially here.  We
-		 * convert 1 to 0 ("all the tuples") and 0 to a very small fraction.
-		 */
-		if (tuple_fraction >= 1.0)
-			tuple_fraction = 0.0;
-		else if (tuple_fraction <= 0.0)
-			tuple_fraction = 1e-10;
-	}
-	else
-	{
-		/* Default assumption is we need all the tuples */
-		tuple_fraction = 0.0;
-	}
-
-	/* primary planning entry point (may recurse for subqueries) */
-	root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
-
-	/* Select best Path and turn it into a Plan */
-	final_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL);
-	best_path = get_cheapest_fractional_path(final_rel, tuple_fraction);
-
-	top_plan = create_plan(root, best_path);
+	PlannerGlobal *glob = root->glob;
 
 	/*
 	 * If creating a plan for a scrollable cursor, make sure it can run
@@ -426,8 +297,8 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	 */
 	if (cursorOptions & CURSOR_OPT_SCROLL)
 	{
-		if (!ExecSupportsBackwardScan(top_plan))
-			top_plan = materialize_finished_plan(top_plan);
+		if (!ExecSupportsBackwardScan(plan))
+			plan = materialize_finished_plan(plan);
 	}
 
 	/*
@@ -443,25 +314,25 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	 * EXPLAIN to hide, so skip it when debug_parallel_query = regress.
 	 */
 	if (debug_parallel_query != DEBUG_PARALLEL_OFF &&
-		top_plan->parallel_safe &&
-		(top_plan->initPlan == NIL ||
+		plan->parallel_safe &&
+		(plan->initPlan == NIL ||
 		 debug_parallel_query != DEBUG_PARALLEL_REGRESS))
 	{
 		Gather	   *gather = makeNode(Gather);
 		Cost		initplan_cost;
 		bool		unsafe_initplans;
 
-		gather->plan.targetlist = top_plan->targetlist;
+		gather->plan.targetlist = plan->targetlist;
 		gather->plan.qual = NIL;
-		gather->plan.lefttree = top_plan;
+		gather->plan.lefttree = plan;
 		gather->plan.righttree = NULL;
 		gather->num_workers = 1;
 		gather->single_copy = true;
 		gather->invisible = (debug_parallel_query == DEBUG_PARALLEL_REGRESS);
 
 		/* Transfer any initPlans to the new top node */
-		gather->plan.initPlan = top_plan->initPlan;
-		top_plan->initPlan = NIL;
+		gather->plan.initPlan = plan->initPlan;
+		plan->initPlan = NIL;
 
 		/*
 		 * Since this Gather has no parallel-aware descendants to signal to,
@@ -473,28 +344,28 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 		 * Ideally we'd use cost_gather here, but setting up dummy path data
 		 * to satisfy it doesn't seem much cleaner than knowing what it does.
 		 */
-		gather->plan.startup_cost = top_plan->startup_cost +
+		gather->plan.startup_cost = plan->startup_cost +
 			parallel_setup_cost;
-		gather->plan.total_cost = top_plan->total_cost +
-			parallel_setup_cost + parallel_tuple_cost * top_plan->plan_rows;
-		gather->plan.plan_rows = top_plan->plan_rows;
-		gather->plan.plan_width = top_plan->plan_width;
+		gather->plan.total_cost = plan->total_cost +
+			parallel_setup_cost + parallel_tuple_cost * plan->plan_rows;
+		gather->plan.plan_rows = plan->plan_rows;
+		gather->plan.plan_width = plan->plan_width;
 		gather->plan.parallel_aware = false;
 		gather->plan.parallel_safe = false;
 
 		/*
-		 * Delete the initplans' cost from top_plan.  We needn't add it to the
+		 * Delete the initplans' cost from plan.  We needn't add it to the
 		 * Gather node, since the above coding already included it there.
 		 */
 		SS_compute_initplan_cost(gather->plan.initPlan,
 								 &initplan_cost, &unsafe_initplans);
-		top_plan->startup_cost -= initplan_cost;
-		top_plan->total_cost -= initplan_cost;
+		plan->startup_cost -= initplan_cost;
+		plan->total_cost -= initplan_cost;
 
 		/* use parallel mode for parallel plans. */
-		root->glob->parallelModeNeeded = true;
+		glob->parallelModeNeeded = true;
 
-		top_plan = &gather->plan;
+		plan = &gather->plan;
 	}
 
 	/*
@@ -513,7 +384,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 			SS_finalize_plan(subroot, subplan);
 		}
-		SS_finalize_plan(root, top_plan);
+		SS_finalize_plan(root, plan);
 	}
 
 	/* final cleanup of the plan */
@@ -522,7 +393,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	Assert(glob->finalrowmarks == NIL);
 	Assert(glob->resultRelations == NIL);
 	Assert(glob->appendRelations == NIL);
-	top_plan = set_plan_references(root, top_plan);
+	plan = set_plan_references(root, plan);
 	/* ... and the subplans (both regular subplans and initplans) */
 	Assert(list_length(glob->subplans) == list_length(glob->subroots));
 	forboth(lp, glob->subplans, lr, glob->subroots)
@@ -544,7 +415,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->transientPlan = glob->transientPlan;
 	result->dependsOnRole = glob->dependsOnRole;
 	result->parallelModeNeeded = glob->parallelModeNeeded;
-	result->planTree = top_plan;
+	result->planTree = plan;
 	result->rtable = glob->finalrtable;
 	result->permInfos = glob->finalrteperminfos;
 	result->resultRelations = glob->resultRelations;
@@ -562,7 +433,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 
 	result->jitFlags = PGJIT_NONE;
 	if (jit_enabled && jit_above_cost >= 0 &&
-		top_plan->total_cost > jit_above_cost)
+		plan->total_cost > jit_above_cost)
 	{
 		result->jitFlags |= PGJIT_PERFORM;
 
@@ -570,10 +441,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 		 * Decide how much effort should be put into generating better code.
 		 */
 		if (jit_optimize_above_cost >= 0 &&
-			top_plan->total_cost > jit_optimize_above_cost)
+			plan->total_cost > jit_optimize_above_cost)
 			result->jitFlags |= PGJIT_OPT3;
 		if (jit_inline_above_cost >= 0 &&
-			top_plan->total_cost > jit_inline_above_cost)
+			plan->total_cost > jit_inline_above_cost)
 			result->jitFlags |= PGJIT_INLINE;
 
 		/*
@@ -591,6 +462,225 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	return result;
 }
 
+static PlannerGlobal *
+setup_planner_global(Query *parse, int cursorOptions, ParamListInfo boundParams, bool keepAllCandidates)
+{
+	PlannerGlobal *glob;
+
+	/*
+	 * Set up global state for this planner invocation.  This data is needed
+	 * across all levels of sub-Query that might exist in the given command,
+	 * so we keep it in a separate struct that's linked to by each per-Query
+	 * PlannerInfo.
+	 */
+	glob = makeNode(PlannerGlobal);
+
+	glob->boundParams = boundParams;
+	glob->subplans = NIL;
+	glob->subpaths = NIL;
+	glob->subroots = NIL;
+	glob->rewindPlanIDs = NULL;
+	glob->finalrtable = NIL;
+	glob->finalrteperminfos = NIL;
+	glob->finalrowmarks = NIL;
+	glob->resultRelations = NIL;
+	glob->appendRelations = NIL;
+	glob->relationOids = NIL;
+	glob->invalItems = NIL;
+	glob->paramExecTypes = NIL;
+	glob->lastPHId = 0;
+	glob->lastRowMarkId = 0;
+	glob->lastPlanNodeId = 0;
+	glob->transientPlan = false;
+	glob->dependsOnRole = false;
+	glob->keepAllCandidates = keepAllCandidates;
+
+	/*
+	 * Assess whether it's feasible to use parallel mode for this query. We
+	 * can't do this in a standalone backend, or if the command will try to
+	 * modify any data, or if this is a cursor operation, or if GUCs are set
+	 * to values that don't permit parallelism, or if parallel-unsafe
+	 * functions are present in the query tree.
+	 *
+	 * (Note that we do allow CREATE TABLE AS, SELECT INTO, and CREATE
+	 * MATERIALIZED VIEW to use parallel plans, but this is safe only because
+	 * the command is writing into a completely new table which workers won't
+	 * be able to see.  If the workers could see the table, the fact that
+	 * group locking would cause them to ignore the leader's heavyweight GIN
+	 * page locks would make this unsafe.  We'll have to fix that somehow if
+	 * we want to allow parallel inserts in general; updates and deletes have
+	 * additional problems especially around combo CIDs.)
+	 *
+	 * For now, we don't try to use parallel mode if we're running inside a
+	 * parallel worker.  We might eventually be able to relax this
+	 * restriction, but for now it seems best not to have parallel workers
+	 * trying to create their own parallel workers.
+	 */
+	if ((cursorOptions & CURSOR_OPT_PARALLEL_OK) != 0 &&
+		IsUnderPostmaster &&
+		parse->commandType == CMD_SELECT &&
+		!parse->hasModifyingCTE &&
+		max_parallel_workers_per_gather > 0 &&
+		!IsParallelWorker())
+	{
+		/* all the cheap tests pass, so scan the query tree */
+		glob->maxParallelHazard = max_parallel_hazard(parse);
+		glob->parallelModeOK = (glob->maxParallelHazard != PROPARALLEL_UNSAFE);
+	}
+	else
+	{
+		/* skip the query tree scan, just assume it's unsafe */
+		glob->maxParallelHazard = PROPARALLEL_UNSAFE;
+		glob->parallelModeOK = false;
+	}
+
+	/*
+	 * glob->parallelModeNeeded is normally set to false here and changed to
+	 * true during plan creation if a Gather or Gather Merge plan is actually
+	 * created (cf. create_gather_plan, create_gather_merge_plan).
+	 *
+	 * However, if debug_parallel_query = on or debug_parallel_query =
+	 * regress, then we impose parallel mode whenever it's safe to do so, even
+	 * if the final plan doesn't use parallelism.  It's not safe to do so if
+	 * the query contains anything parallel-unsafe; parallelModeOK will be
+	 * false in that case.  Note that parallelModeOK can't change after this
+	 * point. Otherwise, everything in the query is either parallel-safe or
+	 * parallel-restricted, and in either case it should be OK to impose
+	 * parallel-mode restrictions.  If that ends up breaking something, then
+	 * either some function the user included in the query is incorrectly
+	 * labeled as parallel-safe or parallel-restricted when in reality it's
+	 * parallel-unsafe, or else the query planner itself has a bug.
+	 */
+	glob->parallelModeNeeded = glob->parallelModeOK &&
+		(debug_parallel_query != DEBUG_PARALLEL_OFF);
+	return glob;
+}
+
+static double
+compute_tuple_fraction(int cursorOptions)
+{
+	double		tuple_fraction;
+
+	/* Determine what fraction of the plan is likely to be scanned */
+	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
+	{
+		/*
+		 * We have no real idea how many tuples the user will ultimately FETCH
+		 * from a cursor, but it is often the case that he doesn't want 'em
+		 * all, or would prefer a fast-start plan anyway so that he can
+		 * process some of the tuples sooner.  Use a GUC parameter to decide
+		 * what fraction to optimize for.
+		 */
+		tuple_fraction = cursor_tuple_fraction;
+
+		/*
+		 * We document cursor_tuple_fraction as simply being a fraction, which
+		 * means the edge cases 0 and 1 have to be treated specially here.  We
+		 * convert 1 to 0 ("all the tuples") and 0 to a very small fraction.
+		 */
+		if (tuple_fraction >= 1.0)
+			tuple_fraction = 0.0;
+		else if (tuple_fraction <= 0.0)
+			tuple_fraction = 1e-10;
+	}
+	else
+	{
+		/* Default assumption is we need all the tuples */
+		tuple_fraction = 0.0;
+	}
+	return tuple_fraction;
+}
+
+PlannedStmt *
+standard_planner(Query *parse, const char *query_string, int cursorOptions,
+				 ParamListInfo boundParams)
+{
+	PlannedStmt *result;
+	PlannerGlobal *glob;
+	double		tuple_fraction;
+	PlannerInfo *root;
+	RelOptInfo *final_rel;
+	Path	   *best_path;
+	Plan	   *top_plan;
+
+	glob = setup_planner_global(parse, cursorOptions, boundParams, false);
+	tuple_fraction = compute_tuple_fraction(cursorOptions);
+
+	/* primary planning entry point (may recurse for subqueries) */
+	root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
+
+	/* Select best Path and turn it into a Plan */
+	final_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL);
+
+
+	best_path = get_cheapest_fractional_path(final_rel, tuple_fraction);
+
+	top_plan = create_plan(root, best_path);
+	result = create_planned_stmt(root, parse, top_plan, cursorOptions);
+
+	return result;
+}
+
+/*
+ * planner_all_candidates
+ * 		Build the list of all candidate PlannedStmt
+ *
+ * With ALL_CANDIDATES explain option, a list of PlannedStmt is built for
+ * all possible path from the upper_rel.
+ */
+List *
+planner_all_candidates(Query *parse, int cursorOptions, ParamListInfo boundParams)
+{
+	List	   *result = NULL;
+	double		tuple_fraction;
+	Query	   *base_parse;
+	RelOptInfo *upper_rel;
+	int			current_path_index = 0;
+
+	/*
+	 * Parse may be modified by planner, save a copy of the initial parse
+	 * state
+	 */
+	base_parse = copyObject(parse);
+	tuple_fraction = compute_tuple_fraction(cursorOptions);
+
+	do
+	{
+		Plan	   *plan;
+		PlannedStmt *planned_stmt;
+		Path	   *path;
+		PlannerGlobal *glob;
+		PlannerInfo *root;
+
+		/*
+		 * During planning, parse, glob and path may be modified. We need to
+		 * create on a copy of those for all planned_stmt we create. However,
+		 * we can't use copyObject on PlannerInfo. The current solution is to
+		 * redo the whole planning for every plan we create.
+		 */
+		parse = copyObject(base_parse);
+		glob = setup_planner_global(parse, cursorOptions, boundParams, true);
+		/* Run the planner */
+		root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL);
+
+		/* Get the path */
+		upper_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL);
+		if (current_path_index >= upper_rel->pathlist->length)
+		{
+			/* Planner generated a different number of path, bail out */
+			break;
+		}
+		path = (Path *) list_nth(upper_rel->pathlist, current_path_index++);
+
+		/* Create the plan from the path */
+		plan = create_plan(root, path);
+		planned_stmt = create_planned_stmt(root, parse, plan, cursorOptions);
+		/* Add it to the list */
+		result = lcons(planned_stmt, result);
+	} while (current_path_index < upper_rel->pathlist->length);
+
+	return result;
+}
 
 /*--------------------
  * subquery_planner
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 91d394ce62e..263a9eca0fb 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -430,6 +430,24 @@ add_path(PlannerInfo *root, RelOptInfo *parent_rel, Path *new_path)
 	 */
 	CHECK_FOR_INTERRUPTS();
 
+	/*
+	 * When keepAllCandidates is enabled, we always add the path in the
+	 * pathlist
+	 */
+	if (root->glob->keepAllCandidates)
+	{
+		foreach(p1, parent_rel->pathlist)
+		{
+			Path	   *old_path = (Path *) lfirst(p1);
+
+			if (new_path->total_cost >= old_path->total_cost)
+				insert_at = foreach_current_index(p1) + 1;
+		}
+		parent_rel->pathlist =
+			list_insert_nth(parent_rel->pathlist, insert_at, new_path);
+		return;
+	}
+
 	/* Pretend parameterized paths have no pathkeys, per comment above */
 	new_path_pathkeys = new_path->param_info ? NIL : new_path->pathkeys;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index ea517f4b9bb..cbff66f7796 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -877,6 +877,50 @@ pg_rewrite_query(Query *query)
 	return querytree_list;
 }
 
+/*
+ * Generate a list of all plan candidates for a single already-rewritten query.
+ * This is a thin wrapper around planner() and takes the same parameters.
+ */
+List *
+pg_plan_query_all_candidates(Query *querytree, const char *query_string, int cursorOptions,
+							 ParamListInfo boundParams)
+{
+	List	   *plans;
+
+	/* Utility commands have no plans. */
+	if (querytree->commandType == CMD_UTILITY)
+		return NULL;
+
+	/* Planner must have a snapshot in case it calls user-defined functions. */
+	Assert(ActiveSnapshotSet());
+
+	TRACE_POSTGRESQL_QUERY_PLAN_START();
+
+	if (log_planner_stats)
+		ResetUsage();
+
+	/* call the optimizer */
+	plans = planner_all_candidates(querytree, cursorOptions, boundParams);
+
+	/*
+	 * Print plan list if debugging.
+	 */
+	if (Debug_print_plan)
+	{
+		ListCell   *lc;
+
+		foreach(lc, plans)
+		{
+			Plan	   *plan = lfirst(lc);
+
+			elog_node_display(LOG, "plan", plan, Debug_pretty_print);
+		}
+	}
+
+	TRACE_POSTGRESQL_QUERY_PLAN_DONE();
+
+	return plans;
+}
 
 /*
  * Generate a plan for a single already-rewritten query.
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 891face1b65..c172ce7eb94 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3871,8 +3871,8 @@ psql_completion(const char *text, int start, int end)
 		if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
 			COMPLETE_WITH("ANALYZE", "VERBOSE", "COSTS", "SETTINGS", "GENERIC_PLAN",
 						  "BUFFERS", "SERIALIZE", "WAL", "TIMING", "SUMMARY",
-						  "MEMORY", "FORMAT");
-		else if (TailMatches("ANALYZE|VERBOSE|COSTS|SETTINGS|GENERIC_PLAN|BUFFERS|WAL|TIMING|SUMMARY|MEMORY"))
+						  "MEMORY", "ALL_CANDIDATES", "FORMAT");
+		else if (TailMatches("ANALYZE|VERBOSE|COSTS|SETTINGS|GENERIC_PLAN|BUFFERS|WAL|TIMING|SUMMARY|MEMORY|ALL_CANDIDATES"))
 			COMPLETE_WITH("ON", "OFF");
 		else if (TailMatches("SERIALIZE"))
 			COMPLETE_WITH("TEXT", "NONE", "BINARY");
diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h
index 9b8b351d9a2..5303f76239c 100644
--- a/src/include/commands/explain.h
+++ b/src/include/commands/explain.h
@@ -55,6 +55,7 @@ typedef struct ExplainState
 	bool		memory;			/* print planner's memory usage information */
 	bool		settings;		/* print modified settings */
 	bool		generic;		/* generate a generic plan */
+	bool		all_candidates; /* print all plan candidates */
 	ExplainSerializeOption serialize;	/* serialize the query's output? */
 	ExplainFormat format;		/* output format */
 	/* state for output formatting --- not reset for each new plan tree */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 14ccfc1ac1c..721dc2fe993 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -158,6 +158,9 @@ typedef struct PlannerGlobal
 	/* parallel mode actually required? */
 	bool		parallelModeNeeded;
 
+	/* do no discard more expensive plans from pathlist */
+	bool		keepAllCandidates;
+
 	/* worst PROPARALLEL hazard level */
 	char		maxParallelHazard;
 
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 7b63c5cf718..5d928267dac 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -115,6 +115,8 @@ extern PGDLLIMPORT bool parallel_leader_participation;
 extern struct PlannedStmt *planner(Query *parse, const char *query_string,
 								   int cursorOptions,
 								   struct ParamListInfoData *boundParams);
+extern List *planner_all_candidates(Query *parse, int cursorOptions,
+									struct ParamListInfoData *boundParams);
 
 extern Expr *expression_planner(Expr *expr);
 extern Expr *expression_planner_with_deps(Expr *expr,
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 147a294950e..54e33cf0e1d 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -62,6 +62,9 @@ extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
 extern PlannedStmt *pg_plan_query(Query *querytree, const char *query_string,
 								  int cursorOptions,
 								  ParamListInfo boundParams);
+extern List *pg_plan_query_all_candidates(Query *querytree, const char *query_string,
+										  int cursorOptions,
+										  ParamListInfo boundParams);
 extern List *pg_plan_queries(List *querytrees, const char *query_string,
 							 int cursorOptions,
 							 ParamListInfo boundParams);
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 6585c6a69ef..418a7b12621 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -416,6 +416,121 @@ select explain_filter('explain (memory) execute int8_query');
    Memory: used=NkB  allocated=NkB
 (2 rows)
 
+-- ALL_CANDIDATES option
+begin;
+-- encourage planner to compute parallel plan
+set local min_parallel_table_scan_size=0;
+select explain_filter('explain (all_candidates) select * from int8_tbl i8');
+                                explain_filter                                
+------------------------------------------------------------------------------
+ Plan N
+   ->  Gather  (cost=N.N..N.N rows=N width=N)
+         Workers Planned: N
+         ->  Parallel Seq Scan on int8_tbl i8  (cost=N.N..N.N rows=N width=N)
+ Plan N
+   ->  Seq Scan on int8_tbl i8  (cost=N.N..N.N rows=N width=N)
+(6 rows)
+
+select explain_filter('explain (all_candidates, summary, format yaml) select * from int8_tbl i8');
+             explain_filter             
+----------------------------------------
+ - - Plan:                             +
+       Node Type: "Gather"             +
+       Parallel Aware: false           +
+       Async Capable: false            +
+       Startup Cost: N.N               +
+       Total Cost: N.N                 +
+       Plan Rows: N                    +
+       Plan Width: N                   +
+       Workers Planned: N              +
+       Single Copy: false              +
+       Plans:                          +
+         - Node Type: "Seq Scan"       +
+           Parent Relationship: "Outer"+
+           Parallel Aware: true        +
+           Async Capable: false        +
+           Relation Name: "int8_tbl"   +
+           Alias: "i8"                 +
+           Startup Cost: N.N           +
+           Total Cost: N.N             +
+           Plan Rows: N                +
+           Plan Width: N               +
+     Planning Time: N.N                +
+ - - Plan:                             +
+       Node Type: "Seq Scan"           +
+       Parallel Aware: false           +
+       Async Capable: false            +
+       Relation Name: "int8_tbl"       +
+       Alias: "i8"                     +
+       Startup Cost: N.N               +
+       Total Cost: N.N                 +
+       Plan Rows: N                    +
+       Plan Width: N                   +
+     Planning Time: N.N
+(1 row)
+
+select explain_filter('explain (all_candidates, format json) select * from int8_tbl i8');
+               explain_filter                
+---------------------------------------------
+ [                                          +
+   {                                        +
+     {                                      +
+       "Plan": {                            +
+         "Node Type": "Gather",             +
+         "Parallel Aware": false,           +
+         "Async Capable": false,            +
+         "Startup Cost": N.N,               +
+         "Total Cost": N.N,                 +
+         "Plan Rows": N,                    +
+         "Plan Width": N,                   +
+         "Workers Planned": N,              +
+         "Single Copy": false,              +
+         "Plans": [                         +
+           {                                +
+             "Node Type": "Seq Scan",       +
+             "Parent Relationship": "Outer",+
+             "Parallel Aware": true,        +
+             "Async Capable": false,        +
+             "Relation Name": "int8_tbl",   +
+             "Alias": "i8",                 +
+             "Startup Cost": N.N,           +
+             "Total Cost": N.N,             +
+             "Plan Rows": N,                +
+             "Plan Width": N                +
+           }                                +
+         ]                                  +
+       }                                    +
+     }                                      +
+   },                                       +
+   {                                        +
+     {                                      +
+       "Plan": {                            +
+         "Node Type": "Seq Scan",           +
+         "Parallel Aware": false,           +
+         "Async Capable": false,            +
+         "Relation Name": "int8_tbl",       +
+         "Alias": "i8",                     +
+         "Startup Cost": N.N,               +
+         "Total Cost": N.N,                 +
+         "Plan Rows": N,                    +
+         "Plan Width": N                    +
+       }                                    +
+     }                                      +
+   }                                        +
+ ]
+(1 row)
+
+select explain_filter('explain (all_candidates) execute int8_query');
+                     explain_filter                      
+---------------------------------------------------------
+ Seq Scan on int8_tbl i8  (cost=N.N..N.N rows=N width=N)
+(1 row)
+
+rollback;
+-- should fail
+select explain_filter('explain (all_candidates, analyze) select * from int8_tbl i8');
+ERROR:  EXPLAIN options ANALYZE and ALL_CANDIDATES cannot be used together
+CONTEXT:  PL/pgSQL function explain_filter(text) line 5 at FOR over EXECUTE statement
 -- Test EXPLAIN (GENERIC_PLAN) with partition pruning
 -- partitions should be pruned at plan time, based on constants,
 -- but there should be no pruning based on parameter placeholders
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index c7055f850c5..d6b1439824b 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -102,6 +102,18 @@ select explain_filter('explain (memory, analyze, format json) select * from int8
 prepare int8_query as select * from int8_tbl i8;
 select explain_filter('explain (memory) execute int8_query');
 
+-- ALL_CANDIDATES option
+begin;
+-- encourage planner to compute parallel plan
+set local min_parallel_table_scan_size=0;
+select explain_filter('explain (all_candidates) select * from int8_tbl i8');
+select explain_filter('explain (all_candidates, summary, format yaml) select * from int8_tbl i8');
+select explain_filter('explain (all_candidates, format json) select * from int8_tbl i8');
+select explain_filter('explain (all_candidates) execute int8_query');
+rollback;
+-- should fail
+select explain_filter('explain (all_candidates, analyze) select * from int8_tbl i8');
+
 -- Test EXPLAIN (GENERIC_PLAN) with partition pruning
 -- partitions should be pruned at plan time, based on constants,
 -- but there should be no pruning based on parameter placeholders
-- 
2.39.3 (Apple Git-146)

