aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/backend/optimizer/path/allpaths.c21
-rw-r--r--src/backend/optimizer/path/joinrels.c33
-rw-r--r--src/backend/optimizer/plan/createplan.c11
-rw-r--r--src/backend/optimizer/plan/planner.c148
-rw-r--r--src/include/nodes/relation.h25
-rw-r--r--src/test/regress/expected/tsrf.out33
-rw-r--r--src/test/regress/sql/tsrf.sql12
7 files changed, 180 insertions, 103 deletions
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f8f4e0b68f3..2091f715d32 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -1607,7 +1607,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;
@@ -2004,9 +2004,11 @@ 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).
+ * AppendPath with no members (see also IS_DUMMY_APPEND/IS_DUMMY_REL macros).
*
- * This is exported because inheritance_planner() has need for it.
+ * (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
set_dummy_rel_pathlist(RelOptInfo *rel)
@@ -2019,14 +2021,15 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
rel->pathlist = NIL;
rel->partial_pathlist = NIL;
+ /* Set up the dummy path */
add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, NULL,
0, false, NIL, -1));
/*
- * We set the cheapest path immediately, to ensure that IS_DUMMY_REL()
- * will recognize the relation as dummy if anyone asks. This is redundant
- * when we're called from set_rel_size(), but not when called from
- * elsewhere, and doing it twice is harmless anyway.
+ * We set the cheapest-path fields immediately, just in case they were
+ * pointing at some discarded path. This is redundant when we're called
+ * from set_rel_size(), but not when called from elsewhere, and doing it
+ * twice is harmless anyway.
*/
set_cheapest(rel);
}
@@ -3552,12 +3555,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 6ddf8461257..50fc0c500d0 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -33,7 +33,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);
@@ -1192,10 +1191,38 @@ 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;
+
+ /*
+ * A rel that is known dummy will have just one path that is a childless
+ * Append. (Even if somehow it has more paths, a childless Append will
+ * have cost zero and hence should be at the front of the pathlist.)
+ */
+ if (rel->pathlist == NIL)
+ return false;
+ path = (Path *) linitial(rel->pathlist);
+
+ /*
+ * Initially, a dummy path will just be a childless Append. But in later
+ * planning stages we might stick a ProjectSetPath and/or ProjectionPath
+ * on top, since Append can't project. Rather than make assumptions about
+ * which combinations can occur, just descend through whatever we find.
+ */
+ for (;;)
+ {
+ if (IsA(path, ProjectionPath))
+ path = ((ProjectionPath *) path)->subpath;
+ else if (IsA(path, ProjectSetPath))
+ path = ((ProjectSetPath *) path)->subpath;
+ else
+ break;
+ }
+ if (IS_DUMMY_APPEND(path))
+ return true;
+ return false;
}
/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c225e61be96..ae51c0e0b0c 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1038,7 +1038,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)
{
@@ -6506,12 +6506,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 bbf355dbf2c..680085096ae 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1495,7 +1495,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;
/*
@@ -3987,12 +3987,10 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
* If this is the topmost grouping relation or if the parent relation is
* doing some form of partitionwise aggregation, then we may be able to do
* it at this level also. However, if the input relation is not
- * partitioned, partitionwise aggregate is impossible, and if it is dummy,
- * partitionwise aggregate is pointless.
+ * partitioned, partitionwise aggregate is impossible.
*/
if (extra->patype != PARTITIONWISE_AGGREGATE_NONE &&
- input_rel->part_scheme && input_rel->part_rels &&
- !IS_DUMMY_REL(input_rel))
+ IS_PARTITIONED_REL(input_rel))
{
/*
* If this is the topmost relation or if the parent relation is doing
@@ -6817,27 +6815,48 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
bool scanjoin_target_parallel_safe,
bool tlist_same_exprs)
{
- ListCell *lc;
+ bool rel_is_partitioned = IS_PARTITIONED_REL(rel);
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 want to drop its existing paths and
+ * generate new ones. This function would still be correct if we kept the
+ * existing paths: we'd modify them to generate the correct target above
+ * the partitioning Append, and then they'd compete on cost with paths
+ * generating the target below the Append. However, in our current cost
+ * model the latter way is always the same or cheaper cost, so modifying
+ * the existing paths would just be useless work. Moreover, when the cost
+ * is the same, varying roundoff errors might sometimes allow an existing
+ * path to be picked, resulting in undesirable cross-platform plan
+ * variations. So we drop old paths and thereby force the work to be done
+ * below the Append, except in the case of a non-parallel-safe target.
+ *
+ * Some care is needed, 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.
- *
- * Note that this may invalidate rel->cheapest_total_path, so we must
- * not rely on it after this point without first calling set_cheapest.
+ * 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.
*/
generate_gather_paths(root, rel, false);
@@ -6846,61 +6865,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)
@@ -6908,17 +6893,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);
@@ -6928,39 +6914,54 @@ 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 might 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).
*/
- if (rel->part_scheme && rel->part_rels)
+ 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 in which the
+ * scan/join target is computed below the Append rather than above it.
+ * Since Append is not projection-capable, that might save a separate
+ * Result node, and it also is important for partitionwise aggregate.
+ */
+ 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,
@@ -6992,8 +6993,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/relation.h b/src/include/nodes/relation.h
index adb42650479..e61c1a2a295 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -711,15 +711,12 @@ typedef struct RelOptInfo
*
* It's not enough to test whether rel->part_scheme is set, because it might
* be that the basic partitioning properties of the input relations matched
- * but the partition bounds did not.
- *
- * We treat dummy relations as unpartitioned. We could alternatively
- * treat them as partitioned, but it's not clear whether that's a useful thing
- * to do.
+ * but the partition bounds did not. Also, if we are able to prove a rel
+ * dummy (empty), we should henceforth treat it as unpartitioned.
*/
#define IS_PARTITIONED_REL(rel) \
((rel)->part_scheme && (rel)->boundinfo && (rel)->nparts > 0 && \
- (rel)->part_rels && !(IS_DUMMY_REL(rel)))
+ (rel)->part_rels && !IS_DUMMY_REL(rel))
/*
* Convenience macro to make sure that a partitioned relation has all the
@@ -1312,6 +1309,9 @@ typedef struct CustomPath
* elements. These cases are optimized during create_append_plan.
* In particular, an AppendPath with no subpaths is a "dummy" path that
* is created to represent the case that a relation is provably empty.
+ * (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 relation as dummy.)
*/
typedef struct AppendPath
{
@@ -1324,13 +1324,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/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out
index 25be6b9ab13..d47b5f6ec57 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 0a1e8e56660..7c22529a0db 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;