From b978d01f4051ead2f8b2e6bf3debb193ff398da4 Mon Sep 17 00:00:00 2001 From: zhenglong li Date: Sat, 15 Aug 2026 13:55:00 +0800 Subject: [PATCH v4 2/2] Consider window functions when reordering GROUP BY items preprocess_groupclause() orders the GROUP BY items to match the query's ORDER BY clause, so that a single sort can serve both the grouping step and the final ordering. However, when the query contains window functions, the sort performed directly above the grouping step is the one required by the first window clause, not the one for ORDER BY, which is only performed above the WindowAgg nodes. Unless the window's sort requirements happened to coincide with a prefix of the ORDER BY clause, an additional sort was needed to provide the first WindowAgg with correctly ordered input. Teach get_useful_group_keys_orderings() to also offer the GROUP BY keys reordered to match the sort order of the first active window, ie its PARTITION BY keys followed by its ORDER BY keys, as one more candidate ordering. The remaining GROUP BY keys keep the order preprocess_groupclause() gave them, which matches the query's ORDER BY as far as possible; since WindowAgg nodes preserve their input ordering, this can additionally save the final sort. For example, the query SELECT a, b, count(*) OVER (PARTITION BY b) FROM t GROUP BY a, b; previously sorted twice: once by (a, b) for the grouping step and again by (b) for the WindowAgg. Grouping in the order (b, a) allows a single sort to serve both. With an ORDER BY that does not lead with the window's sort keys, such as adding "ORDER BY a" to the query above, the sort count similarly drops from three to two. We match the first window only because it alone can share a sort with the grouping step: select_active_windows() has already fixed the windows' evaluation order, placing the window with the strongest sort requirements first, directly above the grouping step. Satisfying its requirements may satisfy subsequent windows too, since windows whose requirements form a prefix of another's are evaluated later. The window's ordering is only offered as a candidate, rather than applied to root->processed_groupClause up front, because the GROUP BY pathkeys also serve as query_pathkeys, which guide the planning of the scan/join steps below the grouping step. If they took on the window's ordering, those steps would be steered towards it even where the grouping step can't provide it as cheaply as a sort above the grouping step can. For instance, with an index on (a, b) and SELECT a, count(*), sum(count(*)) OVER (ORDER BY a DESC) FROM t JOIN s USING (a, b) GROUP BY a; grouping by a DESC up front lost the plan that merge-joins off the index in ascending order in parallel and groups the result, falling back to a plan almost twice as expensive, although the window needs a sort either way. Keeping the original ordering as well, and letting the cost decide, retains that plan. Like the other alternative orderings of the GROUP BY keys, this one is considered only if enable_group_by_reordering is on. gather_grouping_paths() likewise sorts the partial paths by the window's ordering of the GROUP BY keys too, in addition to the original one, so that the sort can be done by the parallel workers below the Gather Merge rather than by the leader. The window's sort direction and NULLS ordering need not match those of the GROUP BY items, which the parser takes from a matching ORDER BY item or leaves at the default. So with SELECT a, b, c, count(*) OVER (PARTITION BY b ORDER BY c DESC) FROM t GROUP BY a, b, c; the GROUP BY item c sorts ascending. Since grouping only requires equal values to be adjacent, the reordered GROUP BY keys take on the window's c DESC, using the preceding commit's matching of pathkeys that ignores sort direction, so that a single sort by (b, c DESC, a) serves both steps. That matching requires the same equivalence class and operator family, so a window's ORDER BY ... USING with an operator from another operator family is not adopted, even if both families share the equality operator: under a nondeterministic collation, text_pattern_ops' ~<~ compares bytes, so sorting by it might not bring equal values together. Note that the sort cost model is insensitive to column cardinalities, so it costs a sort by (b, a) the same as one by (a, b), and the window's ordering wins whenever it saves a sort above the grouping step. Moving a low-cardinality window key to the front of a large grouping sort defeats the leading-key specializations in tuplesort and can cost more than the eliminated sort saved when the number of groups is small relative to the input. This is mitigated in practice because a small number of groups is also when hash aggregation tends to win, whereas sorted aggregation tends to be chosen when groups are numerous, which is exactly when the eliminated sort is large. Suggested-by: Henson Choi Reviewed-by: Henson Choi --- src/backend/optimizer/path/pathkeys.c | 90 +++- src/backend/optimizer/plan/planner.c | 129 ++--- src/include/optimizer/paths.h | 1 + .../regress/expected/collate.icu.utf8.out | 30 ++ src/test/regress/expected/window.out | 441 ++++++++++++++++++ src/test/regress/sql/collate.icu.utf8.sql | 12 + src/test/regress/sql/window.sql | 206 ++++++++ 7 files changed, 853 insertions(+), 56 deletions(-) diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 27ff8f404ef..715d034694c 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -626,6 +626,68 @@ pathkey_has_sort_operator(PathKey *pathkey) return true; } +/* + * get_window_group_keys_ordering + * Reorder the GROUP BY keys to match the ordering of the first window. + * + * If there are window functions, the sort performed directly above the + * grouping step is the one required by the first window, rather than the one + * for ORDER BY, which is performed above the WindowAgg nodes. So it can pay + * to group in the window's ordering, letting the sort for the grouping step + * also serve the window. We reorder the GROUP BY keys to match as much of + * root->window_pathkeys as possible, taking on its sort direction and NULLS + * ordering, since grouping doesn't care about those. The remaining group + * keys keep the order preprocess_groupclause() gave them, which matches the + * ORDER BY as far as possible; as WindowAgg nodes preserve their input + * ordering, that may save the final sort as well. + * + * This ordering is merely offered as one more candidate, rather than being + * applied to root->group_pathkeys, which also guide the planning of the + * scan/join steps below the grouping step (as query_pathkeys). The window's + * ordering would be the wrong one to suggest there whenever the grouping step + * can't provide it as cheaply as an extra sort above the grouping step can, + * for instance when an index provides the original ordering. So we leave it + * to the cost to decide. + * + * Like the other alternative orderings of the GROUP BY keys, this one is + * considered only if enable_group_by_reordering is on. + * + * Returns NULL if there is no such ordering that differs from + * root->group_pathkeys. + */ +GroupByOrdering * +get_window_group_keys_ordering(PlannerInfo *root) +{ + GroupByOrdering *info; + List *pathkeys = root->group_pathkeys; + List *clauses = root->processed_groupClause; + + if (!enable_group_by_reordering || + root->window_pathkeys == NIL || root->group_pathkeys == NIL || + root->parse->groupingSets || + pathkeys_contained_in(root->window_pathkeys, root->group_pathkeys)) + return NULL; + + if (group_keys_reorder_by_pathkeys(root->window_pathkeys, + &pathkeys, &clauses, + root->num_groupby_pathkeys) == 0) + return NULL; + + /* + * The window's ordering may extend beyond the GROUP BY keys in their + * original order, e.g. with an aggregate as a further window ORDER BY + * key, in which case the reordering changes nothing. + */ + if (compare_pathkeys(pathkeys, root->group_pathkeys) == PATHKEYS_EQUAL) + return NULL; + + info = makeNode(GroupByOrdering); + info->pathkeys = pathkeys; + info->clauses = clauses; + + return info; +} + /* * get_useful_group_keys_orderings * Determine which orderings of GROUP BY keys are potentially interesting. @@ -640,7 +702,10 @@ pathkey_has_sort_operator(PathKey *pathkey) * ORDER BY clause (as much as possible), * - GROUP BY keys reordered to match 'path' ordering (as much as possible), * taking on the sort direction and NULLS ordering of 'path' where they - * differ from those of the GROUP BY keys. + * differ from those of the GROUP BY keys, + * - GROUP BY keys reordered to match the ordering required by the first + * window (as much as possible), taking on its sort direction and NULLS + * ordering likewise. */ List * get_useful_group_keys_orderings(PlannerInfo *root, Path *path) @@ -697,6 +762,29 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path) } } + /* + * Also try the ordering of the group keys that matches the first window, + * unless the path's ordering already produced it above. A partial match + * is useful here even without incremental sort, since the grouping step's + * input is sorted on all of the group keys anyway. + */ + info = get_window_group_keys_ordering(root); + if (info != NULL) + { + ListCell *lc; + + foreach(lc, infos) + { + GroupByOrdering *other = lfirst_node(GroupByOrdering, lc); + + if (compare_pathkeys(info->pathkeys, other->pathkeys) == + PATHKEYS_EQUAL) + break; + } + if (lc == NULL) + infos = lappend(infos, info); + } + #ifdef USE_ASSERT_CHECKING { GroupByOrdering *pinfo = linitial_node(GroupByOrdering, infos); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index edaa42d713a..bb064c32750 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -8061,75 +8061,94 @@ gather_grouping_paths(PlannerInfo *root, RelOptInfo *rel) { ListCell *lc; Path *cheapest_partial_path; - List *groupby_pathkeys; + List *orderings; + GroupByOrdering *window_ordering; /* - * This occurs after any partial aggregation has taken place, so trim off - * any pathkeys added for ORDER BY / DISTINCT aggregates. + * Sort the partial paths by the GROUP BY keys in their original order, + * and also in the order matching the first window, if any, so that the + * finalization step can produce that ordering without a sort of its own. + * Sorting below the Gather Merge has the workers share the work. + * + * XXX Shouldn't this also consider the other group-key orderings? */ - if (list_length(root->group_pathkeys) > root->num_groupby_pathkeys) - groupby_pathkeys = list_copy_head(root->group_pathkeys, - root->num_groupby_pathkeys); - else - groupby_pathkeys = root->group_pathkeys; + orderings = list_make1(root->group_pathkeys); + window_ordering = get_window_group_keys_ordering(root); + if (window_ordering != NULL) + orderings = lappend(orderings, window_ordering->pathkeys); /* Try Gather for unordered paths and Gather Merge for ordered ones. */ generate_useful_gather_paths(root, rel, true); cheapest_partial_path = linitial(rel->partial_pathlist); - /* XXX Shouldn't this also consider the group-key-reordering? */ - foreach(lc, rel->partial_pathlist) + foreach_ptr(List, pathkeys, orderings) { - Path *path = (Path *) lfirst(lc); - bool is_sorted; - int presorted_keys; - double total_groups; - - is_sorted = pathkeys_count_contained_in(groupby_pathkeys, - path->pathkeys, - &presorted_keys); - - if (is_sorted) - continue; - - /* - * Try at least sorting the cheapest path and also try incrementally - * sorting any path which is partially sorted already (no need to deal - * with paths which have presorted keys when incremental sort is - * disabled unless it's the cheapest input path). - */ - if (path != cheapest_partial_path && - (presorted_keys == 0 || !enable_incremental_sort)) - continue; + List *groupby_pathkeys; /* - * We've no need to consider both a sort and incremental sort. We'll - * just do a sort if there are no presorted keys and an incremental - * sort when there are presorted keys. + * This occurs after any partial aggregation has taken place, so trim + * off any pathkeys added for ORDER BY / DISTINCT aggregates. */ - if (presorted_keys == 0 || !enable_incremental_sort) - path = (Path *) create_sort_path(root, rel, path, - groupby_pathkeys, - -1.0); + if (list_length(pathkeys) > root->num_groupby_pathkeys) + groupby_pathkeys = list_copy_head(pathkeys, + root->num_groupby_pathkeys); else - path = (Path *) create_incremental_sort_path(root, - rel, - path, - groupby_pathkeys, - presorted_keys, - -1.0); - total_groups = compute_gather_rows(path); - path = (Path *) - create_gather_merge_path(root, - rel, - path, - rel->reltarget, - groupby_pathkeys, - NULL, - &total_groups); - - add_path(rel, path); + groupby_pathkeys = pathkeys; + + foreach(lc, rel->partial_pathlist) + { + Path *path = (Path *) lfirst(lc); + bool is_sorted; + int presorted_keys; + double total_groups; + + is_sorted = pathkeys_count_contained_in(groupby_pathkeys, + path->pathkeys, + &presorted_keys); + + if (is_sorted) + continue; + + /* + * Try at least sorting the cheapest path and also try + * incrementally sorting any path which is partially sorted + * already (no need to deal with paths which have presorted keys + * when incremental sort is disabled unless it's the cheapest + * input path). + */ + if (path != cheapest_partial_path && + (presorted_keys == 0 || !enable_incremental_sort)) + continue; + + /* + * We've no need to consider both a sort and incremental sort. + * We'll just do a sort if there are no presorted keys and an + * incremental sort when there are presorted keys. + */ + if (presorted_keys == 0 || !enable_incremental_sort) + path = (Path *) create_sort_path(root, rel, path, + groupby_pathkeys, + -1.0); + else + path = (Path *) create_incremental_sort_path(root, + rel, + path, + groupby_pathkeys, + presorted_keys, + -1.0); + total_groups = compute_gather_rows(path); + path = (Path *) + create_gather_merge_path(root, + rel, + path, + rel->reltarget, + groupby_pathkeys, + NULL, + &total_groups); + + add_path(rel, path); + } } } diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index a47e4ee54df..9a38b7fb927 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -228,6 +228,7 @@ extern bool pathkeys_contained_in(List *keys1, List *keys2); extern bool pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common); extern List *adjust_group_pathkeys_to_input(List *group_pathkeys, List *pathkeys); +extern GroupByOrdering *get_window_group_keys_ordering(PlannerInfo *root); extern List *get_useful_group_keys_orderings(PlannerInfo *root, Path *path); extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys, Relids required_outer, diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index 69b2f5109ad..33da1d8bef9 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -2046,6 +2046,36 @@ SELECT x FROM test1ci INTERSECT SELECT x FROM test2ci ORDER BY x; ghi (2 rows) +-- GROUP BY must not adopt a window's sort operator from another opfamily: +-- text_pattern_ops' ~<~ sorts 'A' and 'a' apart although they are equal +SET enable_hashagg TO off; +EXPLAIN (COSTS OFF) +SELECT x, count(*), count(*) OVER (ORDER BY x USING ~<~) +FROM (VALUES ('a' COLLATE case_insensitive), ('B'), ('A'), ('b'), ('a')) v(x) +GROUP BY x; + QUERY PLAN +--------------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY "*VALUES*".column1) + -> Sort + Sort Key: "*VALUES*".column1 COLLATE case_insensitive USING ~<~ + -> GroupAggregate + Group Key: "*VALUES*".column1 + -> Sort + Sort Key: "*VALUES*".column1 COLLATE case_insensitive + -> Values Scan on "*VALUES*" +(9 rows) + +SELECT x, count(*), count(*) OVER (ORDER BY x USING ~<~) +FROM (VALUES ('a' COLLATE case_insensitive), ('B'), ('A'), ('b'), ('a')) v(x) +GROUP BY x; + x | count | count +---+-------+------- + B | 2 | 1 + a | 3 | 2 +(2 rows) + +RESET enable_hashagg; SELECT x FROM test2ci INTERSECT SELECT x FROM test1ci ORDER BY x; x ----- diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index c0bde1c5eec..ea0858dd790 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -4839,6 +4839,447 @@ WHERE first_emp = 1 OR last_emp = 1; sales | 4 | 4800 | 08-08-2007 | 3 | 1 (6 rows) +-- Test reordering of the GROUP BY keys to match the first window's sort +-- order +SET enable_hashagg TO off; +-- Ensure the GROUP BY keys are reordered to match the sort order of the +-- window's PARTITION BY clause, allowing the sort for the grouping step to +-- also provide the sorted input required by the WindowAgg. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, depname; + QUERY PLAN +----------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY depname) + -> GroupAggregate + Group Key: depname, empno + -> Sort + Sort Key: depname, empno + -> Seq Scan on empsalary +(7 rows) + +-- As above, but include the window's ORDER BY keys too. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c +FROM empsalary +GROUP BY empno, depname; + QUERY PLAN +------------------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY depname ORDER BY empno) + -> Group + Group Key: depname, empno + -> Sort + Sort Key: depname, empno + -> Seq Scan on empsalary +(7 rows) + +-- Ensure the GROUP BY keys are matched to the window's sort order in +-- preference to the query's ORDER BY, since the latter sort is performed +-- above the WindowAgg. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, depname +ORDER BY empno; + QUERY PLAN +----------------------------------------------- + Sort + Sort Key: empno + -> WindowAgg + Window: w1 AS (PARTITION BY depname) + -> GroupAggregate + Group Key: depname, empno + -> Sort + Sort Key: depname, empno + -> Seq Scan on empsalary +(9 rows) + +-- Ensure the GROUP BY keys remaining after matching the window's sort order +-- are ordered to match the query's ORDER BY, allowing the final sort to be +-- avoided when the WindowAgg preserves the grouping step's output ordering. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, enroll_date, depname +ORDER BY depname, enroll_date; + QUERY PLAN +----------------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY depname) + -> GroupAggregate + Group Key: depname, enroll_date, empno + -> Sort + Sort Key: depname, enroll_date, empno + -> Seq Scan on empsalary +(7 rows) + +-- As above, but with an ORDER BY that does not lead with the window's sort +-- keys. Ensure the remaining GROUP BY keys still follow the query's ORDER +-- BY even though the final sort cannot be avoided. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, enroll_date, depname +ORDER BY enroll_date, empno; + QUERY PLAN +----------------------------------------------------------- + Sort + Sort Key: enroll_date, empno + -> WindowAgg + Window: w1 AS (PARTITION BY depname) + -> GroupAggregate + Group Key: depname, enroll_date, empno + -> Sort + Sort Key: depname, enroll_date, empno + -> Seq Scan on empsalary +(9 rows) + +-- Ensure the GROUP BY keys are still matched to the query's ORDER BY when +-- the window imposes no sort order of its own. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER () salarysum +FROM empsalary +GROUP BY depname, empno +ORDER BY empno, depname; + QUERY PLAN +----------------------------------------- + WindowAgg + Window: w1 AS () + -> GroupAggregate + Group Key: empno, depname + -> Sort + Sort Key: empno, depname + -> Seq Scan on empsalary +(7 rows) + +-- Ensure the GROUP BY keys adopt the sort direction of the window's ORDER BY +-- keys, so that a single sort still serves both steps. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c +FROM empsalary +GROUP BY empno, depname; + QUERY PLAN +------------------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY depname ORDER BY empno) + -> Group + Group Key: depname, empno + -> Sort + Sort Key: depname, empno DESC + -> Seq Scan on empsalary +(7 rows) + +-- As above, but with NULLS FIRST in the window's ORDER BY. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno NULLS FIRST) c +FROM empsalary +GROUP BY empno, depname; + QUERY PLAN +------------------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY depname ORDER BY empno) + -> Group + Group Key: depname, empno + -> Sort + Sort Key: depname, empno NULLS FIRST + -> Seq Scan on empsalary +(7 rows) + +-- Ensure the window's sort direction is preferred even when the query's +-- ORDER BY has given the GROUP BY key a different one. The final sort can +-- then be done incrementally. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c +FROM empsalary +GROUP BY empno, depname +ORDER BY depname, empno; + QUERY PLAN +------------------------------------------------------------- + Incremental Sort + Sort Key: depname, empno + Presorted Key: depname + -> WindowAgg + Window: w1 AS (PARTITION BY depname ORDER BY empno) + -> Group + Group Key: depname, empno + -> Sort + Sort Key: depname, empno DESC + -> Seq Scan on empsalary +(10 rows) + +-- Likewise for a PARTITION BY key whose direction differs from that given +-- to the GROUP BY key by the query's ORDER BY. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c +FROM empsalary +GROUP BY empno, depname +ORDER BY depname DESC; + QUERY PLAN +------------------------------------------------------------- + Sort + Sort Key: depname DESC + -> WindowAgg + Window: w1 AS (PARTITION BY depname ORDER BY empno) + -> Group + Group Key: depname, empno + -> Sort + Sort Key: depname, empno + -> Seq Scan on empsalary +(9 rows) + +-- Ensure the GROUP BY keys follow the sort order of the first window to be +-- evaluated when windows sort the same key in opposite directions, so that +-- the grouping step's sort serves that window and only the other window +-- needs a sort of its own. Check the results too, wrapping the query so that +-- the outer ORDER BY doesn't give the GROUP BY keys a sort order. +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c1, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c2 + FROM empsalary + GROUP BY empno, depname) s +ORDER BY depname, empno; + QUERY PLAN +--------------------------------------------------------------------------------------- + WindowAgg + Window: w2 AS (PARTITION BY empsalary.depname ORDER BY empsalary.empno) + -> Incremental Sort + Sort Key: empsalary.depname, empsalary.empno + Presorted Key: empsalary.depname + -> WindowAgg + Window: w1 AS (PARTITION BY empsalary.depname ORDER BY empsalary.empno) + -> Group + Group Key: empsalary.depname, empsalary.empno + -> Sort + Sort Key: empsalary.depname, empsalary.empno DESC + -> Seq Scan on empsalary +(12 rows) + +SELECT * FROM + (SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c1, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c2 + FROM empsalary + GROUP BY empno, depname) s +ORDER BY depname, empno; + empno | depname | c1 | c2 +-------+-----------+----+---- + 7 | develop | 1 | 5 + 8 | develop | 2 | 4 + 9 | develop | 3 | 3 + 10 | develop | 4 | 2 + 11 | develop | 5 | 1 + 2 | personnel | 1 | 2 + 5 | personnel | 2 | 1 + 1 | sales | 1 | 3 + 3 | sales | 2 | 2 + 4 | sales | 3 | 1 +(10 rows) + +-- Ensure the GROUP BY key does not adopt the window's sort ordering when +-- the query's ORDER BY has given it a different equality operator. +EXPLAIN (COSTS OFF) +SELECT r, count(*) OVER (ORDER BY r DESC) c +FROM (SELECT row(depname, empno) AS r FROM empsalary) s +GROUP BY r +ORDER BY r USING *<; + QUERY PLAN +---------------------------------------------------------------------------------------- + Sort + Sort Key: (ROW(empsalary.depname, empsalary.empno)) USING *< + -> WindowAgg + Window: w1 AS (ORDER BY (ROW(empsalary.depname, empsalary.empno))) + -> Sort + Sort Key: (ROW(empsalary.depname, empsalary.empno)) DESC + -> Group + Group Key: (ROW(empsalary.depname, empsalary.empno)) + -> Sort + Sort Key: (ROW(empsalary.depname, empsalary.empno)) USING *< + -> Seq Scan on empsalary +(11 rows) + +-- Ensure the GROUP BY keys do not adopt a window's ORDER BY that uses a +-- different operator family, even with the same equality operator. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY empno ORDER BY depname USING ~<~) c +FROM empsalary +GROUP BY depname, empno; + QUERY PLAN +------------------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY empno ORDER BY depname) + -> Incremental Sort + Sort Key: empno, depname USING ~<~ + Presorted Key: empno + -> Group + Group Key: empno, depname + -> Sort + Sort Key: empno, depname + -> Seq Scan on empsalary +(10 rows) + +CREATE TABLE groupwin (x int, y int, z int); +INSERT INTO groupwin + SELECT i % 4, i % 5, CASE WHEN i % 7 = 0 THEN NULL ELSE i % 3 END + FROM generate_series(1, 1000) i; +CREATE INDEX groupwin_x_y_idx ON groupwin (x, y); +ANALYZE groupwin; +-- Ensure the GROUP BY keys adopting the window's sort direction doesn't +-- prevent grouping directly off a presorted input, then sorting just the +-- groups for the window. +EXPLAIN (COSTS OFF) +SELECT x, y, count(*), count(*) OVER (PARTITION BY x ORDER BY y DESC) c +FROM groupwin +GROUP BY x, y; + QUERY PLAN +---------------------------------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY x ORDER BY y) + -> Incremental Sort + Sort Key: x, y DESC + Presorted Key: x + -> GroupAggregate + Group Key: x, y + -> Index Only Scan using groupwin_x_y_idx on groupwin +(8 rows) + +-- Check the results with a descending window ORDER BY over NULLs. The query +-- is wrapped so that the outer ORDER BY doesn't give the GROUP BY keys a sort +-- order, leaving them to take the window's. +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT x, z, count(*) c, + sum(count(*)) OVER (PARTITION BY x ORDER BY z DESC NULLS LAST) s + FROM groupwin + GROUP BY x, z) ss +ORDER BY x, z DESC NULLS LAST; + QUERY PLAN +---------------------------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY groupwin.x ORDER BY groupwin.z) + -> GroupAggregate + Group Key: groupwin.x, groupwin.z + -> Sort + Sort Key: groupwin.x, groupwin.z DESC NULLS LAST + -> Seq Scan on groupwin +(7 rows) + +SELECT * FROM + (SELECT x, z, count(*) c, + sum(count(*)) OVER (PARTITION BY x ORDER BY z DESC NULLS LAST) s + FROM groupwin + GROUP BY x, z) ss +ORDER BY x, z DESC NULLS LAST; + x | z | c | s +---+---+----+----- + 0 | 2 | 71 | 71 + 0 | 1 | 72 | 143 + 0 | 0 | 72 | 215 + 0 | | 35 | 250 + 1 | 2 | 72 | 72 + 1 | 1 | 72 | 144 + 1 | 0 | 71 | 215 + 1 | | 35 | 250 + 2 | 2 | 72 | 72 + 2 | 1 | 71 | 143 + 2 | 0 | 71 | 214 + 2 | | 36 | 250 + 3 | 2 | 71 | 71 + 3 | 1 | 71 | 142 + 3 | 0 | 72 | 214 + 3 | | 36 | 250 +(16 rows) + +-- Ensure the GROUP BY keys are not reordered to match the window when +-- enable_group_by_reordering is off. +SET enable_group_by_reordering = off; +EXPLAIN (COSTS OFF) +SELECT y, z, count(*) OVER (PARTITION BY z) c +FROM groupwin +GROUP BY y, z; + QUERY PLAN +---------------------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY z) + -> Sort + Sort Key: z + -> Group + Group Key: y, z + -> Sort + Sort Key: y, z + -> Seq Scan on groupwin +(9 rows) + +RESET enable_group_by_reordering; +-- Ensure the window's sort order is only offered as an additional ordering +-- of the GROUP BY keys, rather than replacing the original one, which also +-- guides the planning of the join below the grouping step. Here the +-- original ascending order lets the merge join use the index, and the sort +-- for the window is done on just the groups, rather than the merge join +-- sorting all of its input in the window's descending order. +CREATE TABLE groupwin_s (a int, b int); +INSERT INTO groupwin_s + SELECT i % 5, CASE WHEN i % 4 = 0 THEN NULL ELSE i % 3 END + FROM generate_series(1, 300) i; +CREATE TABLE groupwin_b (a int, b int); +INSERT INTO groupwin_b + SELECT i % 7, (i * 7) % 11 FROM generate_series(1, 20000) i; +CREATE INDEX groupwin_b_a_b_idx ON groupwin_b (a, b); +VACUUM ANALYZE groupwin_s, groupwin_b; +SET enable_nestloop = off; +SET enable_hashjoin = off; +EXPLAIN (COSTS OFF) +SELECT b.a, count(*), sum(count(*)) OVER (ORDER BY b.a DESC) +FROM groupwin_b b JOIN groupwin_s s ON b.a = s.a AND b.b = s.b +GROUP BY b.a; + QUERY PLAN +---------------------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY b.a) + -> Sort + Sort Key: b.a DESC + -> GroupAggregate + Group Key: b.a + -> Merge Join + Merge Cond: ((b.a = s.a) AND (b.b = s.b)) + -> Index Only Scan using groupwin_b_a_b_idx on groupwin_b b + -> Sort + Sort Key: s.a, s.b + -> Seq Scan on groupwin_s s +(12 rows) + +SELECT b.a, count(*), sum(count(*)) OVER (ORDER BY b.a DESC) +FROM groupwin_b b JOIN groupwin_s s ON b.a = s.a AND b.b = s.b +GROUP BY b.a; + a | count | sum +---+-------+------- + 4 | 11670 | 11670 + 3 | 11685 | 23355 + 2 | 11700 | 35055 + 1 | 11685 | 46740 + 0 | 11670 | 58410 +(5 rows) + +RESET enable_nestloop; +RESET enable_hashjoin; +DROP TABLE groupwin_s, groupwin_b; +DROP TABLE groupwin; +RESET enable_hashagg; CREATE INDEX empsalary_salary_empno_idx ON empsalary (salary, empno); SET enable_seqscan = 0; -- Ensure no sorting is done and that the IndexScan maintains all pathkeys diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index 87cea263780..eed796871f7 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -738,6 +738,18 @@ SELECT x FROM test3ci WHERE x ~ 'a'; SELECT x FROM test1ci UNION SELECT x FROM test2ci ORDER BY x; SELECT x FROM test2ci UNION SELECT x FROM test1ci ORDER BY x; SELECT x FROM test1ci INTERSECT SELECT x FROM test2ci ORDER BY x; + +-- GROUP BY must not adopt a window's sort operator from another opfamily: +-- text_pattern_ops' ~<~ sorts 'A' and 'a' apart although they are equal +SET enable_hashagg TO off; +EXPLAIN (COSTS OFF) +SELECT x, count(*), count(*) OVER (ORDER BY x USING ~<~) +FROM (VALUES ('a' COLLATE case_insensitive), ('B'), ('A'), ('b'), ('a')) v(x) +GROUP BY x; +SELECT x, count(*), count(*) OVER (ORDER BY x USING ~<~) +FROM (VALUES ('a' COLLATE case_insensitive), ('B'), ('A'), ('b'), ('a')) v(x) +GROUP BY x; +RESET enable_hashagg; SELECT x FROM test2ci INTERSECT SELECT x FROM test1ci ORDER BY x; SELECT x FROM test1ci EXCEPT SELECT x FROM test2ci; SELECT x FROM test2ci EXCEPT SELECT x FROM test1ci; diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index 8e6f92d94c7..df901331834 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -1659,6 +1659,212 @@ SELECT * FROM FROM empsalary) emp WHERE first_emp = 1 OR last_emp = 1; +-- Test reordering of the GROUP BY keys to match the first window's sort +-- order +SET enable_hashagg TO off; + +-- Ensure the GROUP BY keys are reordered to match the sort order of the +-- window's PARTITION BY clause, allowing the sort for the grouping step to +-- also provide the sorted input required by the WindowAgg. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, depname; + +-- As above, but include the window's ORDER BY keys too. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c +FROM empsalary +GROUP BY empno, depname; + +-- Ensure the GROUP BY keys are matched to the window's sort order in +-- preference to the query's ORDER BY, since the latter sort is performed +-- above the WindowAgg. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, depname +ORDER BY empno; + +-- Ensure the GROUP BY keys remaining after matching the window's sort order +-- are ordered to match the query's ORDER BY, allowing the final sort to be +-- avoided when the WindowAgg preserves the grouping step's output ordering. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, enroll_date, depname +ORDER BY depname, enroll_date; + +-- As above, but with an ORDER BY that does not lead with the window's sort +-- keys. Ensure the remaining GROUP BY keys still follow the query's ORDER +-- BY even though the final sort cannot be avoided. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER (PARTITION BY depname) depminsum +FROM empsalary +GROUP BY empno, enroll_date, depname +ORDER BY enroll_date, empno; + +-- Ensure the GROUP BY keys are still matched to the query's ORDER BY when +-- the window imposes no sort order of its own. +EXPLAIN (COSTS OFF) +SELECT empno, depname, min(salary) minsalary, + sum(min(salary)) OVER () salarysum +FROM empsalary +GROUP BY depname, empno +ORDER BY empno, depname; + +-- Ensure the GROUP BY keys adopt the sort direction of the window's ORDER BY +-- keys, so that a single sort still serves both steps. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c +FROM empsalary +GROUP BY empno, depname; + +-- As above, but with NULLS FIRST in the window's ORDER BY. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno NULLS FIRST) c +FROM empsalary +GROUP BY empno, depname; + +-- Ensure the window's sort direction is preferred even when the query's +-- ORDER BY has given the GROUP BY key a different one. The final sort can +-- then be done incrementally. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c +FROM empsalary +GROUP BY empno, depname +ORDER BY depname, empno; + +-- Likewise for a PARTITION BY key whose direction differs from that given +-- to the GROUP BY key by the query's ORDER BY. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c +FROM empsalary +GROUP BY empno, depname +ORDER BY depname DESC; + +-- Ensure the GROUP BY keys follow the sort order of the first window to be +-- evaluated when windows sort the same key in opposite directions, so that +-- the grouping step's sort serves that window and only the other window +-- needs a sort of its own. Check the results too, wrapping the query so that +-- the outer ORDER BY doesn't give the GROUP BY keys a sort order. +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c1, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c2 + FROM empsalary + GROUP BY empno, depname) s +ORDER BY depname, empno; +SELECT * FROM + (SELECT empno, depname, + count(*) OVER (PARTITION BY depname ORDER BY empno) c1, + count(*) OVER (PARTITION BY depname ORDER BY empno DESC) c2 + FROM empsalary + GROUP BY empno, depname) s +ORDER BY depname, empno; + +-- Ensure the GROUP BY key does not adopt the window's sort ordering when +-- the query's ORDER BY has given it a different equality operator. +EXPLAIN (COSTS OFF) +SELECT r, count(*) OVER (ORDER BY r DESC) c +FROM (SELECT row(depname, empno) AS r FROM empsalary) s +GROUP BY r +ORDER BY r USING *<; + +-- Ensure the GROUP BY keys do not adopt a window's ORDER BY that uses a +-- different operator family, even with the same equality operator. +EXPLAIN (COSTS OFF) +SELECT empno, depname, + count(*) OVER (PARTITION BY empno ORDER BY depname USING ~<~) c +FROM empsalary +GROUP BY depname, empno; + +CREATE TABLE groupwin (x int, y int, z int); +INSERT INTO groupwin + SELECT i % 4, i % 5, CASE WHEN i % 7 = 0 THEN NULL ELSE i % 3 END + FROM generate_series(1, 1000) i; +CREATE INDEX groupwin_x_y_idx ON groupwin (x, y); +ANALYZE groupwin; + +-- Ensure the GROUP BY keys adopting the window's sort direction doesn't +-- prevent grouping directly off a presorted input, then sorting just the +-- groups for the window. +EXPLAIN (COSTS OFF) +SELECT x, y, count(*), count(*) OVER (PARTITION BY x ORDER BY y DESC) c +FROM groupwin +GROUP BY x, y; + +-- Check the results with a descending window ORDER BY over NULLs. The query +-- is wrapped so that the outer ORDER BY doesn't give the GROUP BY keys a sort +-- order, leaving them to take the window's. +EXPLAIN (COSTS OFF) +SELECT * FROM + (SELECT x, z, count(*) c, + sum(count(*)) OVER (PARTITION BY x ORDER BY z DESC NULLS LAST) s + FROM groupwin + GROUP BY x, z) ss +ORDER BY x, z DESC NULLS LAST; +SELECT * FROM + (SELECT x, z, count(*) c, + sum(count(*)) OVER (PARTITION BY x ORDER BY z DESC NULLS LAST) s + FROM groupwin + GROUP BY x, z) ss +ORDER BY x, z DESC NULLS LAST; + +-- Ensure the GROUP BY keys are not reordered to match the window when +-- enable_group_by_reordering is off. +SET enable_group_by_reordering = off; +EXPLAIN (COSTS OFF) +SELECT y, z, count(*) OVER (PARTITION BY z) c +FROM groupwin +GROUP BY y, z; +RESET enable_group_by_reordering; + +-- Ensure the window's sort order is only offered as an additional ordering +-- of the GROUP BY keys, rather than replacing the original one, which also +-- guides the planning of the join below the grouping step. Here the +-- original ascending order lets the merge join use the index, and the sort +-- for the window is done on just the groups, rather than the merge join +-- sorting all of its input in the window's descending order. +CREATE TABLE groupwin_s (a int, b int); +INSERT INTO groupwin_s + SELECT i % 5, CASE WHEN i % 4 = 0 THEN NULL ELSE i % 3 END + FROM generate_series(1, 300) i; +CREATE TABLE groupwin_b (a int, b int); +INSERT INTO groupwin_b + SELECT i % 7, (i * 7) % 11 FROM generate_series(1, 20000) i; +CREATE INDEX groupwin_b_a_b_idx ON groupwin_b (a, b); +VACUUM ANALYZE groupwin_s, groupwin_b; + +SET enable_nestloop = off; +SET enable_hashjoin = off; + +EXPLAIN (COSTS OFF) +SELECT b.a, count(*), sum(count(*)) OVER (ORDER BY b.a DESC) +FROM groupwin_b b JOIN groupwin_s s ON b.a = s.a AND b.b = s.b +GROUP BY b.a; +SELECT b.a, count(*), sum(count(*)) OVER (ORDER BY b.a DESC) +FROM groupwin_b b JOIN groupwin_s s ON b.a = s.a AND b.b = s.b +GROUP BY b.a; + +RESET enable_nestloop; +RESET enable_hashjoin; + +DROP TABLE groupwin_s, groupwin_b; +DROP TABLE groupwin; + +RESET enable_hashagg; + CREATE INDEX empsalary_salary_empno_idx ON empsalary (salary, empno); SET enable_seqscan = 0; -- 2.50.1 (Apple Git-155)