From be4c08ea278ac9c506a87a5a0a6e6b19f3f2d610 Mon Sep 17 00:00:00 2001 From: Anthonin Bonnefoy 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 [ ( option [, ...] ) ] boolean ] SUMMARY [ boolean ] MEMORY [ boolean ] + ALL_CANDIDATES [ boolean ] FORMAT { TEXT | XML | JSON | YAML } @@ -293,6 +294,22 @@ ROLLBACK; + + ALL_CANDIDATES + + + Print all plan candidates. By default, the planner discards more expensive + plans. ALL_CANDIDATES forces the planner to keep all + plans which will be printed by EXPLAIN. 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 + EXPLAIN. + This parameter may only be used when ANALYZE is not + enabled. It defaults to FALSE. + + + + FORMAT @@ -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) + + + + Here is the same plan with all plan candidates: + + +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) 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)