diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 0debac7..f1eed32 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -105,6 +105,7 @@ static Path *get_cheapest_parameterized_child_path(PlannerInfo *root, Relids required_outer); static void accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths); +static void set_dummy_rel_pathlist(RelOptInfo *rel); static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); static void set_function_pathlist(PlannerInfo *root, RelOptInfo *rel, @@ -1557,7 +1558,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, * Consider an append of unordered, unparameterized partial paths. Make * it parallel-aware if possible. */ - if (partial_subpaths_valid) + if (partial_subpaths_valid && partial_subpaths != NIL) { AppendPath *appendpath; ListCell *lc; @@ -1954,11 +1955,16 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths) * Build a dummy path for a relation that's been excluded by constraints * * Rather than inventing a special "dummy" path type, we represent this as an - * AppendPath with no members (see also IS_DUMMY_PATH/IS_DUMMY_REL macros). - * - * This is exported because inheritance_planner() has need for it. + * AppendPath with no members (see also IS_DUMMY_APPEND/IS_DUMMY_REL macros). + * This is a convenient representation because it means that when we build + * an appendrel and find that all its children have been excluded, no extra + * action is needed to recognize the rel as dummy. + * + * (See also mark_dummy_rel, which does basically the same thing, but is + * typically used to change a rel into dummy state after we already made + * paths for it.) */ -void +static void set_dummy_rel_pathlist(RelOptInfo *rel) { /* Set dummy size estimates --- we leave attr_widths[] as zeroes */ @@ -1969,6 +1975,12 @@ set_dummy_rel_pathlist(RelOptInfo *rel) rel->pathlist = NIL; rel->partial_pathlist = NIL; + /* Forget about child partitions, too, since they must all be dummy */ + rel->nparts = 0; + rel->boundinfo = NULL; + rel->part_rels = NULL; + + /* Set up the dummy path */ add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NULL, 0, false, NIL, -1)); @@ -3532,12 +3544,12 @@ generate_partitionwise_join_paths(PlannerInfo *root, RelOptInfo *rel) /* Add partitionwise join paths for partitioned child-joins. */ generate_partitionwise_join_paths(root, child_rel); + set_cheapest(child_rel); + /* Dummy children will not be scanned, so ignore those. */ if (IS_DUMMY_REL(child_rel)) continue; - set_cheapest(child_rel); - #ifdef OPTIMIZER_DEBUG debug_print_rel(root, child_rel); #endif diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c index dfbbfda..36536bf 100644 --- a/src/backend/optimizer/path/joinrels.c +++ b/src/backend/optimizer/path/joinrels.c @@ -34,7 +34,6 @@ static void make_rels_by_clauseless_joins(PlannerInfo *root, ListCell *other_rels); static bool has_join_restriction(PlannerInfo *root, RelOptInfo *rel); static bool has_legal_joinclause(PlannerInfo *root, RelOptInfo *rel); -static bool is_dummy_rel(RelOptInfo *rel); static bool restriction_is_constant_false(List *restrictlist, RelOptInfo *joinrel, bool only_pushed_down); @@ -1196,10 +1195,31 @@ have_dangerous_phv(PlannerInfo *root, /* * is_dummy_rel --- has relation been proven empty? */ -static bool +bool is_dummy_rel(RelOptInfo *rel) { - return IS_DUMMY_REL(rel); + Path *path = rel->cheapest_total_path; + + /* + * Initially, a rel that is known dummy will have a childless Append path. + * But in later planning stages we might stick a ProjectSetPath on top, + * and it seems possible that a ProjectionPath could end up atop that + * (since ProjectSetPath does not accept further layers of projection). + * Rather than make assumptions about exactly what combinations can occur, + * just descend through whatever we find. + */ + while (path) + { + if (IsA(path, ProjectionPath)) + path = ((ProjectionPath *) path)->subpath; + else if (IsA(path, ProjectSetPath)) + path = ((ProjectSetPath *) path)->subpath; + else + break; + } + if (path && IS_DUMMY_APPEND(path)) + return true; + return false; } /* @@ -1236,6 +1256,11 @@ mark_dummy_rel(RelOptInfo *rel) rel->pathlist = NIL; rel->partial_pathlist = NIL; + /* Forget about child partitions, too, since they must all be dummy */ + rel->nparts = 0; + rel->boundinfo = NULL; + rel->part_rels = NULL; + /* Set up the dummy path */ add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NULL, 0, false, NIL, -1)); diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 236f506..9fbe5b2 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -1072,7 +1072,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path) * * Note that an AppendPath with no members is also generated in certain * cases where there was no appending construct at all, but we know the - * relation is empty (see set_dummy_rel_pathlist). + * relation is empty (see set_dummy_rel_pathlist and mark_dummy_rel). */ if (best_path->subpaths == NIL) { @@ -6585,12 +6585,11 @@ is_projection_capable_path(Path *path) case T_Append: /* - * Append can't project, but if it's being used to represent a - * dummy path, claim that it can project. This prevents us from - * converting a rel from dummy to non-dummy status by applying a - * projection to its dummy path. + * Append can't project, but if an AppendPath is being used to + * represent a dummy path, what will actually be generated is a + * Result which can project. */ - return IS_DUMMY_PATH(path); + return IS_DUMMY_APPEND(path); case T_ProjectSet: /* diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index bc81535..df9c398 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -1517,7 +1517,7 @@ inheritance_planner(PlannerInfo *root) * If this child rel was excluded by constraint exclusion, exclude it * from the result plan. */ - if (IS_DUMMY_PATH(subpath)) + if (IS_DUMMY_REL(sub_final_rel)) continue; /* @@ -2508,38 +2508,6 @@ remap_to_groupclause_idx(List *groupClause, } - -/* - * Detect whether a plan node is a "dummy" plan created when a relation - * is deemed not to need scanning due to constraint exclusion. - * - * Currently, such dummy plans are Result nodes with constant FALSE - * filter quals (see set_dummy_rel_pathlist and create_append_plan). - * - * XXX this probably ought to be somewhere else, but not clear where. - */ -bool -is_dummy_plan(Plan *plan) -{ - if (IsA(plan, Result)) - { - List *rcqual = (List *) ((Result *) plan)->resconstantqual; - - if (list_length(rcqual) == 1) - { - Const *constqual = (Const *) linitial(rcqual); - - if (constqual && IsA(constqual, Const)) - { - if (!constqual->constisnull && - !DatumGetBool(constqual->constvalue)) - return true; - } - } - } - return false; -} - /* * preprocess_rowmarks - set up PlanRowMarks if needed */ @@ -6901,27 +6869,44 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, bool scanjoin_target_parallel_safe, bool tlist_same_exprs) { - ListCell *lc; + bool rel_is_partitioned = (rel->part_scheme && rel->part_rels); PathTarget *scanjoin_target; - bool is_dummy_rel = IS_DUMMY_REL(rel); + ListCell *lc; + /* This recurses, so be paranoid. */ check_stack_depth(); /* + * If the rel is partitioned, we're going to generate all-new paths here, + * and there's no point in doing work on the existing paths. So we want + * to drop those paths. Care is needed, though, because we have to allow + * generate_gather_paths to see the old partial paths in the next stanza. + * Hence, zap the main pathlist here, then allow generate_gather_paths to + * add path(s) to the main list, and finally zap the partial pathlist. + */ + if (rel_is_partitioned) + rel->pathlist = NIL; + + /* * If the scan/join target is not parallel-safe, partial paths cannot * generate it. */ if (!scanjoin_target_parallel_safe) { /* - * Since we can't generate the final scan/join target, this is our - * last opportunity to use any partial paths that exist. We don't do - * this if the case where the target is parallel-safe, since we will - * be able to generate superior paths by doing it after the final - * scan/join target has been applied. + * Since we can't generate the final scan/join target in parallel + * workers, this is our last opportunity to use any partial paths that + * exist; so build Gather path(s) that use them and emit whatever the + * current reltarget is. We don't do this in the case where the + * target is parallel-safe, since we will be able to generate superior + * paths by doing it after the final scan/join target has been + * applied. * * Note that this may invalidate rel->cheapest_total_path, so we must - * not rely on it after this point without first calling set_cheapest. + * not rely on that path being valid until we call set_cheapest. In + * particular, this means that IS_DUMMY_REL(rel) might not work during + * this routine. We don't currently need that, but if in future we + * do, save its value at the start of the function. */ generate_gather_paths(root, rel, false); @@ -6930,61 +6915,27 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, rel->consider_parallel = false; } - /* - * Update the reltarget. This may not be strictly necessary in all cases, - * but it is at least necessary when create_append_path() gets called - * below directly or indirectly, since that function uses the reltarget as - * the pathtarget for the resulting path. It seems like a good idea to do - * it unconditionally. - */ - rel->reltarget = llast_node(PathTarget, scanjoin_targets); - - /* Special case: handle dummy relations separately. */ - if (is_dummy_rel) - { - /* - * Since this is a dummy rel, it's got a single Append path with no - * child paths. Replace it with a new path having the final scan/join - * target. (Note that since Append is not projection-capable, it - * would be bad to handle this using the general purpose code below; - * we'd end up putting a ProjectionPath on top of the existing Append - * node, which would cause this relation to stop appearing to be a - * dummy rel.) - */ - rel->pathlist = list_make1(create_append_path(root, rel, NIL, NIL, - NULL, 0, false, NIL, - -1)); + /* Finish dropping old paths for a partitioned rel, per comment above */ + if (rel_is_partitioned) rel->partial_pathlist = NIL; - set_cheapest(rel); - Assert(IS_DUMMY_REL(rel)); - - /* - * Forget about any child relations. There's no point in adjusting - * them and no point in using them for later planning stages (in - * particular, partitionwise aggregate). - */ - rel->nparts = 0; - rel->part_rels = NULL; - rel->boundinfo = NULL; - - return; - } /* Extract SRF-free scan/join target. */ scanjoin_target = linitial_node(PathTarget, scanjoin_targets); /* - * Adjust each input path. If the tlist exprs are the same, we can just - * inject the sortgroupref information into the existing pathtarget. - * Otherwise, replace each path with a projection path that generates the - * SRF-free scan/join target. This can't change the ordering of paths - * within rel->pathlist, so we just modify the list in place. + * Apply the SRF-free scan/join target to each existing path. + * + * If the tlist exprs are the same, we can just inject the sortgroupref + * information into the existing pathtargets. Otherwise, replace each + * path with a projection path that generates the SRF-free scan/join + * target. This can't change the ordering of paths within rel->pathlist, + * so we just modify the list in place. */ foreach(lc, rel->pathlist) { Path *subpath = (Path *) lfirst(lc); - Path *newpath; + /* Shouldn't have any parameterized paths anymore */ Assert(subpath->param_info == NULL); if (tlist_same_exprs) @@ -6992,17 +6943,18 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, scanjoin_target->sortgrouprefs; else { + Path *newpath; + newpath = (Path *) create_projection_path(root, rel, subpath, scanjoin_target); lfirst(lc) = newpath; } } - /* Same for partial paths. */ + /* Likewise adjust the targets for any partial paths. */ foreach(lc, rel->partial_pathlist) { Path *subpath = (Path *) lfirst(lc); - Path *newpath; /* Shouldn't have any parameterized paths anymore */ Assert(subpath->param_info == NULL); @@ -7012,39 +6964,53 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, scanjoin_target->sortgrouprefs; else { - newpath = (Path *) create_projection_path(root, - rel, - subpath, + Path *newpath; + + newpath = (Path *) create_projection_path(root, rel, subpath, scanjoin_target); lfirst(lc) = newpath; } } - /* Now fix things up if scan/join target contains SRFs */ + /* + * Now, if final scan/join target contains SRFs, insert ProjectSetPath(s) + * atop each existing path. (Note that this function doesn't look at the + * cheapest-path fields, which is a good thing because they're bogus right + * now.) + */ if (root->parse->hasTargetSRFs) adjust_paths_for_srfs(root, rel, scanjoin_targets, scanjoin_targets_contain_srfs); /* - * If the relation is partitioned, recursively apply the same changes to - * all partitions and generate new Append paths. Since Append is not - * projection-capable, that might save a separate Result node, and it also - * is important for partitionwise aggregate. + * Update the rel's target to be the final (with SRFs) scan/join target. + * This now matches the actual output of all the paths, and we'll get + * confused in createplan.c if they don't agree. We must do this now so + * that any append paths made in the next part will use the correct + * pathtarget (cf. create_append_path). + */ + rel->reltarget = llast_node(PathTarget, scanjoin_targets); + + /* + * If the relation is partitioned, recursively apply the scan/join target + * to all partitions and generate brand-new Append paths. Since Append is + * not projection-capable, that might save a separate Result node, and it + * also is important for partitionwise aggregate. */ - if (rel->part_scheme && rel->part_rels) + if (rel_is_partitioned) { - int partition_idx; List *live_children = NIL; + int partition_idx; /* Adjust each partition. */ for (partition_idx = 0; partition_idx < rel->nparts; partition_idx++) { RelOptInfo *child_rel = rel->part_rels[partition_idx]; - ListCell *lc; AppendRelInfo **appinfos; int nappinfos; List *child_scanjoin_targets = NIL; + ListCell *lc; /* Translate scan/join targets for this child. */ appinfos = find_appinfos_by_relids(root, child_rel->relids, @@ -7076,8 +7042,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, } /* Build new paths for this relation by appending child paths. */ - if (live_children != NIL) - add_paths_to_append_rel(root, rel, live_children); + add_paths_to_append_rel(root, rel, live_children); } /* diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index a008ae0..7a8c3f7 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1361,13 +1361,16 @@ typedef struct AppendPath int first_partial_path; } AppendPath; -#define IS_DUMMY_PATH(p) \ +#define IS_DUMMY_APPEND(p) \ (IsA((p), AppendPath) && ((AppendPath *) (p))->subpaths == NIL) -/* A relation that's been proven empty will have one path that is dummy */ -#define IS_DUMMY_REL(r) \ - ((r)->cheapest_total_path != NULL && \ - IS_DUMMY_PATH((r)->cheapest_total_path)) +/* + * A relation that's been proven empty will have one path that is dummy + * (but might have projection paths on top). For historical reasons, + * this is provided as a macro that wraps is_dummy_rel(). + */ +#define IS_DUMMY_REL(r) is_dummy_rel(r) +extern bool is_dummy_rel(RelOptInfo *rel); /* * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 040335a..36d12bc 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -49,7 +49,6 @@ extern PGDLLIMPORT join_search_hook_type join_search_hook; extern RelOptInfo *make_one_rel(PlannerInfo *root, List *joinlist); -extern void set_dummy_rel_pathlist(RelOptInfo *rel); extern RelOptInfo *standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels); diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h index cb41e40..830557e 100644 --- a/src/include/optimizer/planner.h +++ b/src/include/optimizer/planner.h @@ -44,8 +44,6 @@ extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, bool hasRecursion, double tuple_fraction); -extern bool is_dummy_plan(Plan *plan); - extern RowMarkType select_rowmark_type(RangeTblEntry *rte, LockClauseStrength strength); diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out index 25be6b9..d47b5f6 100644 --- a/src/test/regress/expected/tsrf.out +++ b/src/test/regress/expected/tsrf.out @@ -83,6 +83,39 @@ SELECT generate_series(1, generate_series(1, 3)), generate_series(2, 4); CREATE TABLE few(id int, dataa text, datab text); INSERT INTO few VALUES(1, 'a', 'foo'),(2, 'a', 'bar'),(3, 'b', 'bar'); +-- SRF with a provably-dummy relation +explain (verbose, costs off) +SELECT unnest(ARRAY[1, 2]) FROM few WHERE false; + QUERY PLAN +-------------------------------------- + ProjectSet + Output: unnest('{1,2}'::integer[]) + -> Result + One-Time Filter: false +(4 rows) + +SELECT unnest(ARRAY[1, 2]) FROM few WHERE false; + unnest +-------- +(0 rows) + +-- SRF shouldn't prevent upper query from recognizing lower as dummy +explain (verbose, costs off) +SELECT * FROM few f1, + (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss; + QUERY PLAN +------------------------------------------------ + Result + Output: f1.id, f1.dataa, f1.datab, ss.unnest + One-Time Filter: false +(3 rows) + +SELECT * FROM few f1, + (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss; + id | dataa | datab | unnest +----+-------+-------+-------- +(0 rows) + -- SRF output order of sorting is maintained, if SRF is not referenced SELECT few.id, generate_series(1,3) g FROM few ORDER BY id DESC; id | g diff --git a/src/test/regress/sql/tsrf.sql b/src/test/regress/sql/tsrf.sql index 0a1e8e5..7c22529 100644 --- a/src/test/regress/sql/tsrf.sql +++ b/src/test/regress/sql/tsrf.sql @@ -28,6 +28,18 @@ SELECT generate_series(1, generate_series(1, 3)), generate_series(2, 4); CREATE TABLE few(id int, dataa text, datab text); INSERT INTO few VALUES(1, 'a', 'foo'),(2, 'a', 'bar'),(3, 'b', 'bar'); +-- SRF with a provably-dummy relation +explain (verbose, costs off) +SELECT unnest(ARRAY[1, 2]) FROM few WHERE false; +SELECT unnest(ARRAY[1, 2]) FROM few WHERE false; + +-- SRF shouldn't prevent upper query from recognizing lower as dummy +explain (verbose, costs off) +SELECT * FROM few f1, + (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss; +SELECT * FROM few f1, + (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss; + -- SRF output order of sorting is maintained, if SRF is not referenced SELECT few.id, generate_series(1,3) g FROM few ORDER BY id DESC;