From 4451c69ee8bbdfd1f5973e822b35b17266fa0995 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 3 Jun 2025 11:37:23 +0200 Subject: [PATCH v11] Consider an explicit sort of the MergeAppend subpaths. Broaden the optimiser's search scope slightly: when retrieving optimal subpaths that match pathkeys for the planning MergeAppend, also consider the case of an overall optimal path that includes an explicit Sort node at the top. It may provide a more effective plan in both full and fractional scan cases: 1. The Sort node may be pushed down to subpaths under a parallel or async Append. 2. The case when a minor set of subpaths doesn't have a proper index, and it is profitable to sort them instead of switching to plain Append. Having implemented that strategy, it became clear that the cost of multiple small sortings merged by a single MergeAppend node exceeds that of a single Sort operation over a plain Append. The code and benchmarks demonstrate that such an assumption is incorrect because the Sort operator has optimisations that work faster than a MergeAppend. To arrange the cost model, change the merge cost multiplier, considering that heap rebuilding needs two comparison operations. --- .../postgres_fdw/expected/postgres_fdw.out | 6 +- src/backend/optimizer/path/allpaths.c | 74 ++++--- src/backend/optimizer/path/pathkeys.c | 183 ++++++++++++++++++ src/include/optimizer/paths.h | 10 + src/test/regress/expected/inherit.out | 19 +- src/test/regress/expected/partition_join.out | 149 ++++++++------ src/test/regress/expected/partition_prune.out | 16 +- src/test/regress/expected/union.out | 6 +- src/test/regress/sql/inherit.sql | 4 +- 9 files changed, 351 insertions(+), 116 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 78b8367d289..beaa9df7024 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -10474,13 +10474,15 @@ SELECT t1.a, t2.b FROM fprt1 t1 INNER JOIN fprt2 t2 ON (t1.a = t2.b) WHERE t1.a -> Nested Loop Join Filter: (t1.a = t2.b) -> Append - -> Foreign Scan on ftprt1_p1 t1_1 + -> Sort + Sort Key: t1_1.a + -> Foreign Scan on ftprt1_p1 t1_1 -> Foreign Scan on ftprt1_p2 t1_2 -> Materialize -> Append -> Foreign Scan on ftprt2_p1 t2_1 -> Foreign Scan on ftprt2_p2 t2_2 -(10 rows) +(12 rows) SELECT t1.a, t2.b FROM fprt1 t1 INNER JOIN fprt2 t2 ON (t1.a = t2.b) WHERE t1.a % 25 = 0 ORDER BY 1,2 FOR UPDATE OF t1; a | b diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 6cc6966b060..8a16d9200b7 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -1792,6 +1792,9 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, List *total_subpaths = NIL; List *fractional_subpaths = NIL; bool startup_neq_total = false; + bool total_has_ordered = false; + bool startup_has_ordered = false; + bool fractional_has_ordered = false; bool match_partition_order; bool match_partition_order_desc; int end_index; @@ -1855,29 +1858,24 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, /* Locate the right paths, if they are available. */ cheapest_startup = - get_cheapest_path_for_pathkeys(childrel->pathlist, - pathkeys, - NULL, - STARTUP_COST, - false); + get_cheapest_path_for_pathkeys_ext(root, childrel, pathkeys, + NULL, STARTUP_COST, false); cheapest_total = - get_cheapest_path_for_pathkeys(childrel->pathlist, - pathkeys, - NULL, - TOTAL_COST, - false); + get_cheapest_path_for_pathkeys_ext(root, childrel, pathkeys, + NULL, TOTAL_COST, false); + + if (pathkeys_contained_in(pathkeys, cheapest_startup->pathkeys)) + startup_has_ordered = true; + + if (pathkeys_contained_in(pathkeys, cheapest_total->pathkeys)) + total_has_ordered = true; /* - * If we can't find any paths with the right order just use the - * cheapest-total path; we'll have to sort it later. + * In accordance to current planning logic there are no + * parameterised paths under a merge append. */ - if (cheapest_startup == NULL || cheapest_total == NULL) - { - cheapest_startup = cheapest_total = - childrel->cheapest_total_path; - /* Assert we do have an unparameterized path for this child */ - Assert(cheapest_total->param_info == NULL); - } + Assert(cheapest_startup != NULL && cheapest_total != NULL); + Assert(cheapest_total->param_info == NULL); /* * When building a fractional path, determine a cheapest @@ -1904,21 +1902,20 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, path_fraction /= childrel->rows; cheapest_fractional = - get_cheapest_fractional_path_for_pathkeys(childrel->pathlist, - pathkeys, - NULL, - path_fraction); + get_cheapest_fractional_path_for_pathkeys_ext(root, + childrel, + pathkeys, + NULL, + path_fraction); + + if (pathkeys_contained_in(pathkeys, cheapest_fractional->pathkeys)) + fractional_has_ordered = true; /* - * If we found no path with matching pathkeys, use the - * cheapest total path instead. - * - * XXX We might consider partially sorted paths too (with an - * incremental sort on top). But we'd have to build all the - * incremental paths, do the costing etc. + * In accordance to current planning logic there are no + * parameterised fractional paths under a merge append. */ - if (!cheapest_fractional) - cheapest_fractional = cheapest_total; + Assert(cheapest_fractional != NULL); } /* @@ -2009,19 +2006,20 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, else { /* We need MergeAppend */ - add_path(rel, (Path *) create_merge_append_path(root, - rel, - startup_subpaths, - pathkeys, - NULL)); - if (startup_neq_total) + if (startup_has_ordered) + add_path(rel, (Path *) create_merge_append_path(root, + rel, + startup_subpaths, + pathkeys, + NULL)); + if (startup_neq_total && total_has_ordered) add_path(rel, (Path *) create_merge_append_path(root, rel, total_subpaths, pathkeys, NULL)); - if (fractional_subpaths) + if (fractional_subpaths && fractional_has_ordered) add_path(rel, (Path *) create_merge_append_path(root, rel, fractional_subpaths, diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 8b04d40d36d..0eb618f304c 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -19,11 +19,13 @@ #include "access/stratnum.h" #include "catalog/pg_opfamily.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "optimizer/cost.h" #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "optimizer/planner.h" #include "partitioning/partbounds.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" @@ -648,6 +650,98 @@ get_cheapest_path_for_pathkeys(List *paths, List *pathkeys, return matched_path; } +/* + * get_cheapest_path_for_pathkeys_ext + * Calls get_cheapest_path_for_pathkeys to obtain cheapest path that + * satisfies defined criterias and сonsiders one more option: choose + * overall-optimal path (according the criterion) and explicitly sort its + * output to satisfy the pathkeys. + * + * Caller is responsible to insert corresponding sort path at the top of + * returned path if it will be chosen to be used. + * + * Return NULL if no such path. + */ +Path * +get_cheapest_path_for_pathkeys_ext(PlannerInfo *root, RelOptInfo *rel, + List *pathkeys, Relids required_outer, + CostSelector cost_criterion, + bool require_parallel_safe) +{ + Path sort_path; + Path *base_path = rel->cheapest_total_path; + Path *path; + + /* In generate_orderedappend_paths() all childrels do have some paths */ + Assert(base_path); + + path = get_cheapest_path_for_pathkeys(rel->pathlist, pathkeys, + required_outer, cost_criterion, + require_parallel_safe); + + /* + * Stop here if the cheapest total path doesn't satisfy necessary + * conditions + */ + if ((require_parallel_safe && !base_path->parallel_safe) || + !bms_is_subset(PATH_REQ_OUTER(base_path), required_outer)) + return path; + + if (path == NULL) + + /* + * Current pathlist doesn't fit the pathkeys. No need to check extra + * sort path ways. + */ + return base_path; + + /* Consider the cheapest total path with extra sort */ + if (path != base_path) + { + int presorted_keys; + + if (!pathkeys_count_contained_in(pathkeys, base_path->pathkeys, + &presorted_keys)) + { + /* + * We'll need to insert a Sort node, so include costs for that. + * We choose to use incremental sort if it is enabled and there + * are presorted keys; otherwise we use full sort. + * + * We can use the parent's LIMIT if any, since we certainly won't + * pull more than that many tuples from any child. + */ + if (enable_incremental_sort && presorted_keys > 0) + { + cost_incremental_sort(&sort_path, root, pathkeys, + presorted_keys, + base_path->disabled_nodes, + base_path->startup_cost, + base_path->total_cost, base_path->rows, + base_path->pathtarget->width, 0.0, + work_mem, -1.0); + } + else + { + cost_sort(&sort_path, root, pathkeys, base_path->disabled_nodes, + base_path->total_cost, base_path->rows, + base_path->pathtarget->width, 0.0, work_mem, -1.0); + } + } + else + { + sort_path.rows = base_path->rows; + sort_path.disabled_nodes = base_path->disabled_nodes; + sort_path.startup_cost = base_path->startup_cost; + sort_path.total_cost = base_path->total_cost; + } + + if (compare_path_costs(&sort_path, path, cost_criterion) < 0) + return base_path; + } + return path; +} + /* * get_cheapest_fractional_path_for_pathkeys * Find the cheapest path (for retrieving a specified fraction of all @@ -690,6 +784,95 @@ get_cheapest_fractional_path_for_pathkeys(List *paths, return matched_path; } +/* + * get_cheapest_fractional_path_for_pathkeys_ext + * obtain cheapest fractional path that satisfies defined criterias excluding + * pathkeys and explicitly sort its output to satisfy the pathkeys. + * + * Caller is responsible to insert corresponding sort path at the top of + * returned path if it will be chosen to be used. + * + * Return NULL if no such path. + */ +Path * +get_cheapest_fractional_path_for_pathkeys_ext(PlannerInfo *root, + RelOptInfo *rel, + List *pathkeys, + Relids required_outer, + double fraction) +{ + Path sort_path; + Path *base_path = rel->cheapest_total_path; + Path *path; + + /* In generate_orderedappend_paths() all childrels do have some paths */ + Assert(base_path); + + path = get_cheapest_fractional_path_for_pathkeys(rel->pathlist, pathkeys, + required_outer, fraction); + + /* + * Stop here if the cheapest total path doesn't satisfy necessary + * conditions + */ + if (!bms_is_subset(PATH_REQ_OUTER(base_path), required_outer)) + return path; + + if (path == NULL) + + /* + * Current pathlist doesn't fit the pathkeys. No need to check extra + * sort path ways. + */ + return base_path; + + /* Consider the cheapest total path with extra sort */ + if (path != base_path) + { + int presorted_keys; + + if (!pathkeys_count_contained_in(pathkeys, base_path->pathkeys, + &presorted_keys)) + { + /* + * We'll need to insert a Sort node, so include costs for that. + * We choose to use incremental sort if it is enabled and there + * are presorted keys; otherwise we use full sort. + * + * We can use the parent's LIMIT if any, since we certainly won't + * pull more than that many tuples from any child. + */ + if (enable_incremental_sort && presorted_keys > 0) + { + cost_incremental_sort(&sort_path, root, pathkeys, + presorted_keys, + base_path->disabled_nodes, + base_path->startup_cost, + base_path->total_cost, base_path->rows, + base_path->pathtarget->width, 0.0, + work_mem, -1.0); + } + else + { + cost_sort(&sort_path, root, pathkeys, base_path->disabled_nodes, + base_path->total_cost, base_path->rows, + base_path->pathtarget->width, 0.0, work_mem, -1.0); + } + } + else + { + sort_path.rows = base_path->rows; + sort_path.disabled_nodes = base_path->disabled_nodes; + sort_path.startup_cost = base_path->startup_cost; + sort_path.total_cost = base_path->total_cost; + } + + if (compare_fractional_path_costs(&sort_path, path, fraction) <= 0) + return base_path; + } + + return path; +} /* * get_cheapest_parallel_safe_total_inner diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index cbade77b717..fd7f6f115b3 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -219,10 +219,20 @@ extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys, Relids required_outer, CostSelector cost_criterion, bool require_parallel_safe); +extern Path *get_cheapest_path_for_pathkeys_ext(PlannerInfo *root, + RelOptInfo *rel, List *pathkeys, + Relids required_outer, + CostSelector cost_criterion, + bool require_parallel_safe); extern Path *get_cheapest_fractional_path_for_pathkeys(List *paths, List *pathkeys, Relids required_outer, double fraction); +extern Path *get_cheapest_fractional_path_for_pathkeys_ext(PlannerInfo *root, + RelOptInfo *rel, + List *pathkeys, + Relids required_outer, + double fraction); extern Path *get_cheapest_parallel_safe_total_inner(List *paths); extern List *build_index_pathkeys(PlannerInfo *root, IndexOptInfo *index, ScanDirection scandir); diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index 5b5055babdc..a5be7789fd7 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -1665,11 +1665,13 @@ insert into matest2 (name) values ('Test 3'); insert into matest2 (name) values ('Test 4'); insert into matest3 (name) values ('Test 5'); insert into matest3 (name) values ('Test 6'); -set enable_indexscan = off; -- force use of seqscan/sort, so no merge +set enable_indexscan = off; -- force use of seqscan/sort +set enable_sort = off; -- since merge append may employ sort in children we need to disable sort explain (verbose, costs off) select * from matest0 order by 1-id; QUERY PLAN ------------------------------------------------------------ Sort + Disabled: true Output: matest0.id, matest0.name, ((1 - matest0.id)) Sort Key: ((1 - matest0.id)) -> Result @@ -1683,7 +1685,7 @@ explain (verbose, costs off) select * from matest0 order by 1-id; Output: matest0_3.id, matest0_3.name -> Seq Scan on public.matest3 matest0_4 Output: matest0_4.id, matest0_4.name -(14 rows) +(15 rows) select * from matest0 order by 1-id; id | name @@ -1719,6 +1721,7 @@ select min(1-id) from matest0; (1 row) reset enable_indexscan; +reset enable_sort; set enable_seqscan = off; -- plan with fewest seqscans should be merge set enable_parallel_append = off; -- Don't let parallel-append interfere explain (verbose, costs off) select * from matest0 order by 1-id; @@ -1844,16 +1847,20 @@ order by t1.b limit 10; Merge Cond: (t1.b = t2.b) -> Merge Append Sort Key: t1.b - -> Index Scan using matest0i on matest0 t1_1 + -> Sort + Sort Key: t1_1.b + -> Seq Scan on matest0 t1_1 -> Index Scan using matest1i on matest1 t1_2 -> Materialize -> Merge Append Sort Key: t2.b - -> Index Scan using matest0i on matest0 t2_1 - Filter: (c = d) + -> Sort + Sort Key: t2_1.b + -> Seq Scan on matest0 t2_1 + Filter: (c = d) -> Index Scan using matest1i on matest1 t2_2 Filter: (c = d) -(14 rows) +(18 rows) reset enable_nestloop; drop table matest0 cascade; diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out index 24e06845f92..3de11957f18 100644 --- a/src/test/regress/expected/partition_join.out +++ b/src/test/regress/expected/partition_join.out @@ -65,31 +65,34 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = -- inner join with partially-redundant join clauses EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = t2.b ORDER BY t1.a, t2.b; - QUERY PLAN ---------------------------------------------------------------- - Sort + QUERY PLAN +--------------------------------------------------------- + Merge Append Sort Key: t1.a - -> Append - -> Merge Join - Merge Cond: (t1_1.a = t2_1.a) - -> Index Scan using iprt1_p1_a on prt1_p1 t1_1 - -> Sort - Sort Key: t2_1.b - -> Seq Scan on prt2_p1 t2_1 - Filter: (a = b) + -> Merge Join + Merge Cond: (t1_1.a = t2_1.a) + -> Index Scan using iprt1_p1_a on prt1_p1 t1_1 + -> Sort + Sort Key: t2_1.b + -> Seq Scan on prt2_p1 t2_1 + Filter: (a = b) + -> Sort + Sort Key: t1_2.a -> Hash Join Hash Cond: (t1_2.a = t2_2.a) -> Seq Scan on prt1_p2 t1_2 -> Hash -> Seq Scan on prt2_p2 t2_2 Filter: (a = b) + -> Sort + Sort Key: t1_3.a -> Hash Join Hash Cond: (t1_3.a = t2_3.a) -> Seq Scan on prt1_p3 t1_3 -> Hash -> Seq Scan on prt2_p3 t2_3 Filter: (a = b) -(22 rows) +(25 rows) SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = t2.b ORDER BY t1.a, t2.b; a | c | b | c @@ -371,9 +374,10 @@ EXPLAIN (COSTS OFF) SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t2.b FROM prt2 t2 WHERE t2.a = 0) AND t1.b = 0 ORDER BY t1.a; QUERY PLAN -------------------------------------------------- - Sort + Merge Append Sort Key: t1.a - -> Append + -> Sort + Sort Key: t1_1.a -> Hash Semi Join Hash Cond: (t1_1.a = t2_1.b) -> Seq Scan on prt1_p1 t1_1 @@ -381,6 +385,8 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t2.b FROM prt2 t2 WHERE t2.a = 0) -> Hash -> Seq Scan on prt2_p1 t2_1 Filter: (a = 0) + -> Sort + Sort Key: t1_2.a -> Hash Semi Join Hash Cond: (t1_2.a = t2_2.b) -> Seq Scan on prt1_p2 t1_2 @@ -388,14 +394,16 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t2.b FROM prt2 t2 WHERE t2.a = 0) -> Hash -> Seq Scan on prt2_p2 t2_2 Filter: (a = 0) - -> Nested Loop Semi Join - Join Filter: (t1_3.a = t2_3.b) - -> Seq Scan on prt1_p3 t1_3 - Filter: (b = 0) - -> Materialize + -> Nested Loop + Join Filter: (t1_3.a = t2_3.b) + -> Unique + -> Sort + Sort Key: t2_3.b -> Seq Scan on prt2_p3 t2_3 Filter: (a = 0) -(24 rows) + -> Seq Scan on prt1_p3 t1_3 + Filter: (b = 0) +(29 rows) SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t2.b FROM prt2 t2 WHERE t2.a = 0) AND t1.b = 0 ORDER BY t1.a; a | b | c @@ -1387,28 +1395,32 @@ SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2 -- This should generate a partitionwise join, but currently fails to EXPLAIN (COSTS OFF) SELECT t1.a, t2.b FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT * FROM prt2 WHERE b > 250) t2 ON t1.a = t2.b WHERE t1.b = 0 ORDER BY t1.a, t2.b; - QUERY PLAN ------------------------------------------------------------ - Incremental Sort + QUERY PLAN +----------------------------------------------------------------- + Sort Sort Key: prt1.a, prt2.b - Presorted Key: prt1.a - -> Merge Left Join - Merge Cond: (prt1.a = prt2.b) - -> Sort - Sort Key: prt1.a - -> Append - -> Seq Scan on prt1_p1 prt1_1 - Filter: ((a < 450) AND (b = 0)) - -> Seq Scan on prt1_p2 prt1_2 - Filter: ((a < 450) AND (b = 0)) - -> Sort - Sort Key: prt2.b - -> Append + -> Merge Right Join + Merge Cond: (prt2.b = prt1.a) + -> Append + -> Sort + Sort Key: prt2_1.b -> Seq Scan on prt2_p2 prt2_1 Filter: (b > 250) + -> Sort + Sort Key: prt2_2.b -> Seq Scan on prt2_p3 prt2_2 Filter: (b > 250) -(19 rows) + -> Materialize + -> Append + -> Sort + Sort Key: prt1_1.a + -> Seq Scan on prt1_p1 prt1_1 + Filter: ((a < 450) AND (b = 0)) + -> Sort + Sort Key: prt1_2.a + -> Seq Scan on prt1_p2 prt1_2 + Filter: ((a < 450) AND (b = 0)) +(23 rows) SELECT t1.a, t2.b FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT * FROM prt2 WHERE b > 250) t2 ON t1.a = t2.b WHERE t1.b = 0 ORDER BY t1.a, t2.b; a | b @@ -1428,25 +1440,33 @@ SELECT t1.a, t2.b FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT * -- partitionwise join does not apply EXPLAIN (COSTS OFF) SELECT t1.a, t2.b FROM prt1 t1, prt2 t2 WHERE t1::text = t2::text AND t1.a = t2.b ORDER BY t1.a; - QUERY PLAN ------------------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------ Merge Join - Merge Cond: ((t1.a = t2.b) AND (((((t1.*)::prt1))::text) = ((((t2.*)::prt2))::text))) - -> Sort - Sort Key: t1.a, ((((t1.*)::prt1))::text) - -> Result - -> Append - -> Seq Scan on prt1_p1 t1_1 - -> Seq Scan on prt1_p2 t1_2 - -> Seq Scan on prt1_p3 t1_3 - -> Sort - Sort Key: t2.b, ((((t2.*)::prt2))::text) - -> Result - -> Append + Merge Cond: (t1.a = t2.b) + Join Filter: ((((t2.*)::prt2))::text = (((t1.*)::prt1))::text) + -> Append + -> Sort + Sort Key: t1_1.a + -> Seq Scan on prt1_p1 t1_1 + -> Sort + Sort Key: t1_2.a + -> Seq Scan on prt1_p2 t1_2 + -> Sort + Sort Key: t1_3.a + -> Seq Scan on prt1_p3 t1_3 + -> Materialize + -> Append + -> Sort + Sort Key: t2_1.b -> Seq Scan on prt2_p1 t2_1 + -> Sort + Sort Key: t2_2.b -> Seq Scan on prt2_p2 t2_2 + -> Sort + Sort Key: t2_3.b -> Seq Scan on prt2_p3 t2_3 -(16 rows) +(24 rows) SELECT t1.a, t2.b FROM prt1 t1, prt2 t2 WHERE t1::text = t2::text AND t1.a = t2.b ORDER BY t1.a; a | b @@ -4512,9 +4532,10 @@ EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) LEFT JOIN plt1_adv t3 ON (t1.a = t3.a AND t1.c = t3.c) WHERE t1.b < 10 ORDER BY t1.a; QUERY PLAN -------------------------------------------------------------------------------- - Sort + Merge Append Sort Key: t1.a - -> Append + -> Sort + Sort Key: t1_1.a -> Hash Right Join Hash Cond: ((t3_1.a = t1_1.a) AND (t3_1.c = t1_1.c)) -> Seq Scan on plt1_adv_p1 t3_1 @@ -4525,6 +4546,8 @@ SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM plt1_adv t1 LEFT JOIN plt2_adv t2 -> Hash -> Seq Scan on plt1_adv_p1 t1_1 Filter: (b < 10) + -> Sort + Sort Key: t1_2.a -> Hash Right Join Hash Cond: ((t3_2.a = t1_2.a) AND (t3_2.c = t1_2.c)) -> Seq Scan on plt1_adv_p2 t3_2 @@ -4535,6 +4558,8 @@ SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM plt1_adv t1 LEFT JOIN plt2_adv t2 -> Hash -> Seq Scan on plt1_adv_p2 t1_2 Filter: (b < 10) + -> Sort + Sort Key: t1_3.a -> Hash Right Join Hash Cond: ((t3_3.a = t1_3.a) AND (t3_3.c = t1_3.c)) -> Seq Scan on plt1_adv_p3 t3_3 @@ -4545,15 +4570,19 @@ SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM plt1_adv t1 LEFT JOIN plt2_adv t2 -> Hash -> Seq Scan on plt1_adv_p3 t1_3 Filter: (b < 10) - -> Nested Loop Left Join - Join Filter: ((t1_4.a = t3_4.a) AND (t1_4.c = t3_4.c)) - -> Nested Loop Left Join - Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c)) + -> Nested Loop Left Join + Join Filter: ((t1_4.a = t3_4.a) AND (t1_4.c = t3_4.c)) + -> Merge Left Join + Merge Cond: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c)) + -> Sort + Sort Key: t1_4.a, t1_4.c -> Seq Scan on plt1_adv_extra t1_4 Filter: (b < 10) + -> Sort + Sort Key: t2_4.a, t2_4.c -> Seq Scan on plt2_adv_extra t2_4 - -> Seq Scan on plt1_adv_extra t3_4 -(41 rows) + -> Seq Scan on plt1_adv_extra t3_4 +(50 rows) SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c) LEFT JOIN plt1_adv t3 ON (t1.a = t3.a AND t1.c = t3.c) WHERE t1.b < 10 ORDER BY t1.a; a | c | a | c | a | c diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index d1966cd7d82..43e2962009e 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -4768,9 +4768,10 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o Window: w1 AS (PARTITION BY part_abc.a ORDER BY part_abc.a) -> Append Subplans Removed: 1 - -> Index Scan using part_abc_2_a_idx on part_abc_2 part_abc_1 - Index Cond: (a >= (stable_one() + 1)) - Filter: (d <= stable_one()) + -> Sort + Sort Key: part_abc_1.a + -> Seq Scan on part_abc_2 part_abc_1 + Filter: ((d <= stable_one()) AND (a >= (stable_one() + 1))) -> Merge Append Sort Key: part_abc_3.a Subplans Removed: 1 @@ -4785,9 +4786,10 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o Window: w1 AS (PARTITION BY part_abc_5.a ORDER BY part_abc_5.a) -> Append Subplans Removed: 1 - -> Index Scan using part_abc_2_a_idx on part_abc_2 part_abc_6 - Index Cond: (a >= (stable_one() + 1)) - Filter: (d >= stable_one()) + -> Sort + Sort Key: part_abc_6.a + -> Seq Scan on part_abc_2 part_abc_6 + Filter: ((d >= stable_one()) AND (a >= (stable_one() + 1))) -> Merge Append Sort Key: a Subplans Removed: 1 @@ -4797,7 +4799,7 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o -> Index Scan using part_abc_3_3_a_idx on part_abc_3_3 part_abc_9 Index Cond: (a >= (stable_one() + 1)) Filter: (d >= stable_one()) -(35 rows) +(37 rows) drop view part_abc_view; drop table part_abc; diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index 96962817ed4..0ccadea910c 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -1207,12 +1207,14 @@ select event_id ---------------------------------------------------------- Merge Append Sort Key: events.event_id - -> Index Scan using events_pkey on events + -> Sort + Sort Key: events.event_id + -> Seq Scan on events -> Sort Sort Key: events_1.event_id -> Seq Scan on events_child events_1 -> Index Scan using other_events_pkey on other_events -(7 rows) +(9 rows) drop table events_child, events, other_events; reset enable_indexonlyscan; diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index 699e8ac09c8..c58beebbd1e 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -641,12 +641,14 @@ insert into matest2 (name) values ('Test 4'); insert into matest3 (name) values ('Test 5'); insert into matest3 (name) values ('Test 6'); -set enable_indexscan = off; -- force use of seqscan/sort, so no merge +set enable_indexscan = off; -- force use of seqscan/sort +set enable_sort = off; -- since merge append may employ sort in children we need to disable sort explain (verbose, costs off) select * from matest0 order by 1-id; select * from matest0 order by 1-id; explain (verbose, costs off) select min(1-id) from matest0; select min(1-id) from matest0; reset enable_indexscan; +reset enable_sort; set enable_seqscan = off; -- plan with fewest seqscans should be merge set enable_parallel_append = off; -- Don't let parallel-append interfere -- 2.51.0