From d66c0e134da6a1d599663c1d2b82bc5937cd530d Mon Sep 17 00:00:00 2001 From: zhenglong li Date: Wed, 28 Oct 2026 15:47:12 +0800 Subject: [PATCH v4 1/2] Ignore sort direction when matching GROUP BY keys to input pathkeys get_useful_group_keys_orderings() tries to reorder the GROUP BY keys to match the ordering of the input path, so that a presorted input such as an index scan can be used for the grouping step without an extra sort. However, both group_keys_reorder_by_pathkeys() and the GROUP BY check in truncate_useless_pathkeys() match pathkeys by pointer, which requires the input ordering to agree with the GROUP BY keys in sort direction and NULLS ordering as well. That is more than grouping needs. Grouping only requires equal values to be adjacent, which any ordering by the same equivalence class and operator family guarantees, whichever its direction. The sort direction of a GROUP BY key is merely taken from a matching ORDER BY item by the parser, to make it likely that one sort can serve both steps. So when the ORDER BY sorts in a direction the input path doesn't provide, as in CREATE INDEX ON t (b, c); SELECT b, c, count(*) FROM t GROUP BY b, c ORDER BY b, c DESC; the grouping keys are b ASC, c DESC, and the index ordering b ASC, c ASC is useful only for its first key. We then sort the whole input by b, c DESC incrementally, rather than grouping straight off the index and sorting only the (typically far fewer) groups for the ORDER BY. Teach group_keys_reorder_by_pathkeys() to accept an input pathkey that matches a GROUP BY pathkey in equivalence class and operator family, regardless of its sort direction and NULLS ordering. In that case the reordered GROUP BY keys use the input pathkey, along with a copy of the GROUP BY clause whose sort operator and NULLS ordering are adjusted to agree with it. Likewise, make truncate_useless_pathkeys() consider such pathkeys useful for GROUP BY, so that they survive to be matched. The aggregate pathkeys that may follow the grouping keys in group_pathkeys are still matched exactly, since an aggregate's ORDER BY does depend on the sort direction. Do the same for the partial aggregation that eager aggregation pushes below a join. generate_grouped_paths() required its input to be sorted exactly by the grouping pathkeys, so once the GROUP BY keys took a direction the input path doesn't provide, the partial aggregation had to sort its whole input, and then lost out to a plan without it, as the final aggregation can now group straight off the input. Partial aggregation compares the grouping keys only for equality, and its output keeps the ordering of its input, so let it take the sort direction from the input path, using the new function adjust_group_pathkeys_to_input(). If the operator family lacks the sort operator for the direction of the input pathkey, which is possible only with an operator class that does not follow the btree rules, stop matching there rather than raise an error, since the GROUP BY keys can still be sorted in their own direction. As before, this ordering is only offered as an additional candidate, so it is chosen only if it is cheaper. Adjust the assertions in get_useful_group_keys_orderings() accordingly, as the reordered clauses and pathkeys no longer need to be identical to the original ones, only to cover the same grouping keys. --- src/backend/optimizer/path/allpaths.c | 26 +- src/backend/optimizer/path/pathkeys.c | 273 +++++++++++++++++- src/include/optimizer/paths.h | 2 + src/test/regress/expected/aggregates.out | 81 ++++++ src/test/regress/expected/eager_aggregate.out | 138 +++++++++ src/test/regress/sql/aggregates.sql | 42 +++ src/test/regress/sql/eager_aggregate.sql | 89 ++++++ 7 files changed, 631 insertions(+), 20 deletions(-) diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index d52a6d40505..fcc1deb1be6 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -3608,6 +3608,7 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, { Path *input_path = (Path *) lfirst(lc); Path *path; + List *input_group_pathkeys; bool is_sorted; int presorted_keys; @@ -3619,7 +3620,16 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, input_path != cheapest_total_path) continue; - is_sorted = pathkeys_count_contained_in(group_pathkeys, + /* + * Partial aggregation compares the grouping keys only for + * equality, so the input can be sorted on them in any direction. + * Take the direction from the input path where it can provide the + * grouping order. + */ + input_group_pathkeys = + adjust_group_pathkeys_to_input(group_pathkeys, + input_path->pathkeys); + is_sorted = pathkeys_count_contained_in(input_group_pathkeys, input_path->pathkeys, &presorted_keys); @@ -3653,13 +3663,13 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, path = (Path *) create_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, -1.0); else path = (Path *) create_incremental_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, presorted_keys, -1.0); } @@ -3692,10 +3702,14 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, { Path *input_path = (Path *) lfirst(lc); Path *path; + List *input_group_pathkeys; bool is_sorted; int presorted_keys; - is_sorted = pathkeys_count_contained_in(group_pathkeys, + input_group_pathkeys = + adjust_group_pathkeys_to_input(group_pathkeys, + input_path->pathkeys); + is_sorted = pathkeys_count_contained_in(input_group_pathkeys, input_path->pathkeys, &presorted_keys); @@ -3729,13 +3743,13 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, path = (Path *) create_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, -1.0); else path = (Path *) create_incremental_sort_path(root, grouped_rel, path, - group_pathkeys, + input_group_pathkeys, presorted_keys, -1.0); } diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index d6a305aedac..27ff8f404ef 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -37,6 +37,7 @@ static bool matches_boolean_partition_clause(RestrictInfo *rinfo, int partkeycol); static Var *find_var_for_subquery_tle(RelOptInfo *rel, TargetEntry *tle); static bool right_merge_direction(PlannerInfo *root, PathKey *pathkey); +static bool pathkey_has_sort_operator(PathKey *pathkey); /**************************************************************************** @@ -353,6 +354,35 @@ pathkeys_contained_in(List *keys1, List *keys2) return false; } +/* + * find_grouping_pathkey + * Find a GROUP BY pathkey that 'pathkey' can stand in for. + * + * Searches the first 'num_groupby_pathkeys' items of 'group_pathkeys' for a + * pathkey with the same equivalence class and operator family as 'pathkey', + * ignoring sort direction and NULLS ordering. Returns NULL if there is none. + * Pathkeys are canonical, so there can be at most one. + */ +static PathKey * +find_grouping_pathkey(List *group_pathkeys, int num_groupby_pathkeys, + PathKey *pathkey) +{ + ListCell *lc; + + foreach(lc, group_pathkeys) + { + PathKey *gpk = (PathKey *) lfirst(lc); + + if (foreach_current_index(lc) >= num_groupby_pathkeys) + break; + if (gpk->pk_eclass == pathkey->pk_eclass && + gpk->pk_opfamily == pathkey->pk_opfamily) + return gpk; + } + + return NULL; +} + /* * group_keys_reorder_by_pathkeys * Reorder GROUP BY pathkeys and clauses to match the input pathkeys. @@ -364,6 +394,13 @@ pathkeys_contained_in(List *keys1, List *keys2) * 'num_groupby_pathkeys' is the number of first '*group_pathkeys' items to * search matching pathkeys. * + * An input pathkey matches a GROUP BY pathkey if both have the same + * equivalence class and operator family, even if they differ in sort + * direction or NULLS ordering. Grouping only requires that equal values be + * adjacent, which the input ordering then guarantees, so in that case we use + * the input pathkey in place of the GROUP BY pathkey and a copy of the GROUP + * BY clause with its sort ordering adjusted to agree with it. + * * Returns the number of GROUP BY keys with a matching pathkey. */ static int @@ -374,6 +411,8 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, List *new_group_pathkeys = NIL, *new_group_clauses = NIL; List *grouping_pathkeys; + List *replaced_pathkeys = NIL, + *replaced_clauses = NIL; ListCell *lc; int n; @@ -403,18 +442,24 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, foreach(lc, pathkeys) { PathKey *pathkey = (PathKey *) lfirst(lc); + PathKey *group_pathkey; SortGroupClause *sgc; + SortGroupClause *new_sgc; /* - * Pathkeys are built in a way that allows simply comparing pointers. - * Give up if we can't find the matching pointer. Also give up if + * Give up if we've already matched all the grouping pathkeys, or if * there is no sortclause reference for some reason. */ if (foreach_current_index(lc) >= num_groupby_pathkeys || - !list_member_ptr(grouping_pathkeys, pathkey) || pathkey->pk_eclass->ec_sortref == 0) break; + /* Give up if there's no grouping pathkey this one can stand in for */ + group_pathkey = find_grouping_pathkey(grouping_pathkeys, + num_groupby_pathkeys, pathkey); + if (group_pathkey == NULL) + break; + /* * Since 1349d27 pathkey coming from underlying node can be in the * root->group_pathkeys but not in the processed_groupClause. So, we @@ -432,23 +477,155 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, */ Assert(OidIsValid(sgc->sortop)); + /* + * If the input pathkey's ordering differs from that of the GROUP BY + * pathkey, make a copy of the clause that agrees with the input + * pathkey. + */ + new_sgc = sgc; + if (pathkey != group_pathkey) + { + bool reverse_sort = (pathkey->pk_cmptype == COMPARE_GT); + Oid sortop = sgc->sortop; + + if (sgc->reverse_sort != reverse_sort) + { + Oid lefttype, + righttype; + + op_input_types(sgc->sortop, &lefttype, &righttype); + sortop = get_opfamily_member_for_cmptype(pathkey->pk_opfamily, + lefttype, + righttype, + pathkey->pk_cmptype); + + /* + * Give up if the operator family lacks the operator to sort + * in the input pathkey's direction. That is possible only + * with an operator class that doesn't follow the btree rules, + * but we needn't fail on it, as the GROUP BY keys can still + * be sorted in their own direction. + */ + if (!OidIsValid(sortop)) + break; + } + + new_sgc = copyObject(sgc); + new_sgc->sortop = sortop; + new_sgc->reverse_sort = reverse_sort; + new_sgc->nulls_first = pathkey->pk_nulls_first; + } + + replaced_pathkeys = lappend(replaced_pathkeys, group_pathkey); + replaced_clauses = lappend(replaced_clauses, sgc); new_group_pathkeys = lappend(new_group_pathkeys, pathkey); - new_group_clauses = lappend(new_group_clauses, sgc); + new_group_clauses = lappend(new_group_clauses, new_sgc); } /* remember the number of pathkeys with a matching GROUP BY key */ n = list_length(new_group_pathkeys); /* append the remaining group pathkeys (will be treated as not sorted) */ - *group_pathkeys = list_concat_unique_ptr(new_group_pathkeys, - *group_pathkeys); - *group_clauses = list_concat_unique_ptr(new_group_clauses, - *group_clauses); + foreach(lc, *group_pathkeys) + { + PathKey *pathkey = (PathKey *) lfirst(lc); + + if (!list_member_ptr(replaced_pathkeys, pathkey)) + new_group_pathkeys = list_append_unique_ptr(new_group_pathkeys, + pathkey); + } + foreach(lc, *group_clauses) + { + SortGroupClause *sgc = (SortGroupClause *) lfirst(lc); + + if (!list_member_ptr(replaced_clauses, sgc)) + new_group_clauses = lappend(new_group_clauses, sgc); + } + *group_pathkeys = new_group_pathkeys; + *group_clauses = new_group_clauses; list_free(grouping_pathkeys); + list_free(replaced_pathkeys); + list_free(replaced_clauses); return n; } +/* + * adjust_group_pathkeys_to_input + * Make grouping pathkeys agree in sort direction with the input pathkeys. + * + * Returns a copy of 'group_pathkeys' in which each leading pathkey that has + * the same equivalence class and operator family as the pathkey at the same + * position of 'pathkeys' is replaced by the latter, even if they differ in + * sort direction or NULLS ordering. Grouping only requires that equal values + * be adjacent, so sorting by the result is as good for grouping as sorting by + * 'group_pathkeys', and an input path sorted by 'pathkeys' may satisfy it + * where it would not satisfy 'group_pathkeys'. + * + * This is meant for grouping steps that don't care about the sort operators + * of their grouping clauses, such as partial aggregation, which compares the + * grouping keys only for equality and leaves the final aggregation to provide + * any ordering the query requires. + * + * As the result may be used to sort the input explicitly, stop at a pathkey + * whose operator family lacks the sort operator for the input pathkey's + * direction. That is possible only with an operator class that doesn't + * follow the btree rules, as an index can provide such an ordering without + * the operator; but we needn't fail on it, as the remaining grouping pathkeys + * can still be sorted in their own direction. + */ +List * +adjust_group_pathkeys_to_input(List *group_pathkeys, List *pathkeys) +{ + List *result = list_copy(group_pathkeys); + ListCell *lc1, + *lc2; + + forboth(lc1, result, lc2, pathkeys) + { + PathKey *group_pathkey = (PathKey *) lfirst(lc1); + PathKey *pathkey = (PathKey *) lfirst(lc2); + + if (group_pathkey->pk_eclass != pathkey->pk_eclass || + group_pathkey->pk_opfamily != pathkey->pk_opfamily) + break; + if (group_pathkey->pk_cmptype != pathkey->pk_cmptype && + !pathkey_has_sort_operator(pathkey)) + break; + lfirst(lc1) = pathkey; + } + + return result; +} + +/* + * pathkey_has_sort_operator + * Check that an explicit sort by 'pathkey' could be performed. + * + * prepare_sort_from_pathkeys() looks up the sort operator in the pathkey's + * operator family using the nominal type of the equivalence member it sorts + * by. We don't know which member that will be, so require the operator for + * the type of every member. + */ +static bool +pathkey_has_sort_operator(PathKey *pathkey) +{ + ListCell *lc; + + foreach(lc, pathkey->pk_eclass->ec_members) + { + EquivalenceMember *em = (EquivalenceMember *) lfirst(lc); + + if (!OidIsValid(get_opfamily_member_for_cmptype(pathkey->pk_opfamily, + em->em_datatype, + em->em_datatype, + pathkey->pk_cmptype))) + return false; + } + + return true; +} + /* * get_useful_group_keys_orderings * Determine which orderings of GROUP BY keys are potentially interesting. @@ -461,7 +638,9 @@ group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys, * * - GROUP BY keys as ordered by preprocess_groupclause() to match target * ORDER BY clause (as much as possible), - * - GROUP BY keys reordered to match 'path' ordering (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. */ List * get_useful_group_keys_orderings(PlannerInfo *root, Path *path) @@ -533,8 +712,39 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path) Assert(list_length(info->clauses) == list_length(pinfo->clauses)); Assert(list_length(info->pathkeys) == list_length(pinfo->pathkeys)); - Assert(list_difference(info->clauses, pinfo->clauses) == NIL); - Assert(list_difference_ptr(info->pathkeys, pinfo->pathkeys) == NIL); + + /* + * The clauses and pathkeys may differ from the original ones in + * sort ordering, but they must cover the same GROUP BY items and + * equivalence classes. + */ + foreach(lc1, info->clauses) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc1); + SortGroupClause *psgc; + + psgc = get_sortgroupref_clause(sgc->tleSortGroupRef, + pinfo->clauses); + Assert(sgc->eqop == psgc->eqop); + } + foreach(lc1, info->pathkeys) + { + PathKey *pk = lfirst_node(PathKey, lc1); + bool found = false; + + foreach(lc2, pinfo->pathkeys) + { + PathKey *ppk = lfirst_node(PathKey, lc2); + + if (ppk->pk_eclass == pk->pk_eclass && + ppk->pk_opfamily == pk->pk_opfamily) + { + found = true; + break; + } + } + Assert(found); + } forboth(lc1, info->clauses, lc2, info->pathkeys) { @@ -542,6 +752,8 @@ get_useful_group_keys_orderings(PlannerInfo *root, Path *path) PathKey *pk = lfirst_node(PathKey, lc2); Assert(pk->pk_eclass->ec_sortref == sgc->tleSortGroupRef); + Assert(sgc->reverse_sort == (pk->pk_cmptype == COMPARE_GT)); + Assert(sgc->nulls_first == pk->pk_nulls_first); } } } @@ -2190,6 +2402,38 @@ count_common_leading_pathkeys_unordered(List *keys1, List *keys2) return ncommon; } +/* + * count_common_leading_group_pathkeys + * Returns the number of leading PathKeys in 'pathkeys' which are useful + * for GROUP BY. + * + * This is like count_common_leading_pathkeys_unordered() applied to + * root->group_pathkeys, except that a pathkey matching a grouping key in + * everything but sort direction and NULLS ordering counts as useful too. + */ +static int +count_common_leading_group_pathkeys(PlannerInfo *root, List *pathkeys) +{ + int ncommon = 0; + + /* No point in searching pathkeys when there are no GROUP BY pathkeys */ + if (root->group_pathkeys == NIL) + return 0; + + foreach_node(PathKey, pathkey, pathkeys) + { + if (!list_member_ptr(root->group_pathkeys, pathkey) && + find_grouping_pathkey(root->group_pathkeys, + root->num_groupby_pathkeys, + pathkey) == NULL) + break; + + ncommon++; + } + + return ncommon; +} + /* * truncate_useless_pathkeys * Shorten the given PathKey List to just the useful PathKeys. If all @@ -2238,10 +2482,11 @@ truncate_useless_pathkeys(PlannerInfo *root, * Check if these pathkeys are useful for GROUP BY or DISTINCT. The order * of the pathkeys does not matter here as Unique and GroupAggregate for * these operations can take advantage of Paths presorted by any of the - * GROUP BY/DISTINCT pathkeys. + * GROUP BY/DISTINCT pathkeys. For GROUP BY, the sort direction and NULLS + * ordering of the grouping keys don't matter either, as + * group_keys_reorder_by_pathkeys() can make them follow the input path. */ - nuseful2 = count_common_leading_pathkeys_unordered(root->group_pathkeys, - pathkeys); + nuseful2 = count_common_leading_group_pathkeys(root, pathkeys); if (nuseful2 == ntotal) return pathkeys; diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index 64f87da1674..a47e4ee54df 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -226,6 +226,8 @@ typedef enum extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2); 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 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/aggregates.out b/src/test/regress/expected/aggregates.out index 7d07619956f..b4afb005ea0 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -3428,6 +3428,87 @@ SELECT count(*) FROM btg GROUP BY w, x, y, z ORDER BY x*x, z; -> Index Scan using btg_x_y_idx on btg (8 rows) +-- Utilize the ordering of index scan even if it differs in sort direction +-- from the ordering that the GROUP BY keys took from ORDER BY +CREATE TABLE btg_dir AS + SELECT i % 4 AS x, i % 5 AS y, i AS w FROM generate_series(1, 1000) AS i; +CREATE INDEX btg_dir_x_y_idx ON btg_dir(x, y); +ANALYZE btg_dir; +EXPLAIN (COSTS OFF) +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC; + QUERY PLAN +-------------------------------------------------------------- + Incremental Sort + Sort Key: x, y DESC + Presorted Key: x + -> GroupAggregate + Group Key: x, y + -> Index Only Scan using btg_dir_x_y_idx on btg_dir +(6 rows) + +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC LIMIT 6; + x | y | count +---+---+------- + 0 | 4 | 50 + 0 | 3 | 50 + 0 | 2 | 50 + 0 | 1 | 50 + 0 | 0 | 50 + 1 | 4 | 50 +(6 rows) + +-- Likewise for a backward index scan that also provides the ordering +-- required by the aggregate +EXPLAIN (COSTS OFF) +SELECT x, array_agg(y ORDER BY y DESC) FROM btg_dir GROUP BY x; + QUERY PLAN +----------------------------------------------------------------- + GroupAggregate + Group Key: x + -> Index Only Scan Backward using btg_dir_x_y_idx on btg_dir +(3 rows) + +SELECT x, array_agg(DISTINCT y ORDER BY y DESC) FROM btg_dir GROUP BY x ORDER BY x; + x | array_agg +---+------------- + 0 | {4,3,2,1,0} + 1 | {4,3,2,1,0} + 2 | {4,3,2,1,0} + 3 | {4,3,2,1,0} +(4 rows) + +DROP TABLE btg_dir; +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error, though such an operator class is not valid +BEGIN; +CREATE TYPE btg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION btg_color_lt(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION btg_color_eq(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION btg_color_cmp(btg_color, btg_color) RETURNS int + LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_lt); +CREATE OPERATOR === (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_eq); +CREATE OPERATOR CLASS btg_color_ops DEFAULT FOR TYPE btg_color USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 btg_color_cmp(btg_color, btg_color); +CREATE TABLE btg_color_tab AS + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::btg_color AS c + FROM generate_series(1, 300) AS i; +CREATE INDEX ON btg_color_tab (c); +ANALYZE btg_color_tab; +SELECT c, count(*) FROM btg_color_tab GROUP BY c ORDER BY c; + c | count +---+------- + r | 100 + g | 100 + b | 100 +(3 rows) + +ROLLBACK; -- Test the case where the number of incoming subtree path keys is more than -- the number of grouping keys. CREATE INDEX btg_y_x_w_idx ON btg(y, x, w); diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out index b420a125e1c..1a49c2e7234 100644 --- a/src/test/regress/expected/eager_aggregate.out +++ b/src/test/regress/expected/eager_aggregate.out @@ -600,6 +600,144 @@ GROUP BY s1.a; DROP TABLE eager_agg_s1; DROP TABLE eager_agg_s2; +-- Ensure the partial aggregation can use an index scan that differs in sort +-- direction from the ordering that the grouping keys took from ORDER BY +CREATE TABLE eager_agg_d1 (a int, b int, c int); +CREATE TABLE eager_agg_d2 (a int); +INSERT INTO eager_agg_d1 + SELECT i % 10, (i / 10) % 100, i FROM generate_series(1, 10000) i; +INSERT INTO eager_agg_d2 SELECT i FROM generate_series(0, 9) i; +CREATE INDEX eager_agg_d1_a_b_idx ON eager_agg_d1 (a, b); +ANALYZE eager_agg_d1, eager_agg_d2; +SET enable_hashagg TO off; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + QUERY PLAN +----------------------------------------------------------------------------------------------- + Limit + Output: d1.a, d1.b, (sum(d1.c)) + -> Incremental Sort + Output: d1.a, d1.b, (sum(d1.c)) + Sort Key: d1.a, d1.b DESC + Presorted Key: d1.a + -> Finalize GroupAggregate + Output: d1.a, d1.b, sum(d1.c) + Group Key: d1.a, d1.b + -> Merge Join + Output: d1.a, d1.b, (PARTIAL sum(d1.c)) + Merge Cond: (d1.a = d2.a) + -> Partial GroupAggregate + Output: d1.a, d1.b, PARTIAL sum(d1.c) + Group Key: d1.a, d1.b + -> Index Scan using eager_agg_d1_a_b_idx on public.eager_agg_d1 d1 + Output: d1.a, d1.b, d1.c + -> Sort + Output: d2.a + Sort Key: d2.a + -> Seq Scan on public.eager_agg_d2 d2 + Output: d2.a +(22 rows) + +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + a | b | sum +---+----+------- + 0 | 99 | 54900 + 0 | 98 | 54800 + 0 | 97 | 54700 + 0 | 96 | 54600 + 0 | 95 | 54500 +(5 rows) + +RESET enable_hashagg; +DROP TABLE eager_agg_d1; +DROP TABLE eager_agg_d2; +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error in the partial aggregation, though such an operator +-- class is not valid +CREATE TYPE eager_agg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color) + RETURNS int LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_lt); +CREATE OPERATOR === (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_eq); +-- FUNCTION 4 is needed for the grouping key to be usable in eager aggregation +CREATE OPERATOR CLASS eager_agg_color_ops DEFAULT FOR TYPE eager_agg_color + USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 eager_agg_color_cmp(eager_agg_color, eager_agg_color), + FUNCTION 4 btequalimage(oid); +CREATE TABLE eager_agg_c1 (c eager_agg_color, d int, x int); +CREATE TABLE eager_agg_c2 (k int); +INSERT INTO eager_agg_c1 + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::eager_agg_color, i % 50, i + FROM generate_series(1, 3000) i; +INSERT INTO eager_agg_c2 SELECT generate_series(0, 49); +CREATE INDEX ON eager_agg_c1 (c DESC) INCLUDE (d, x); +VACUUM ANALYZE eager_agg_c1, eager_agg_c2; +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_index_scan_size = 0; +SET enable_hashagg = off; +EXPLAIN (COSTS OFF) +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Finalize GroupAggregate + Group Key: c1.c + -> Gather Merge + Workers Planned: 2 + -> Sort + Sort Key: c1.c + -> Hash Join + Hash Cond: (c1.d = c2.k) + -> Partial GroupAggregate + Group Key: c1.c, c1.d + -> Sort + Sort Key: c1.c, c1.d + -> Parallel Index Only Scan using eager_agg_c1_c_d_x_idx on eager_agg_c1 c1 + -> Hash + -> Seq Scan on eager_agg_c2 c2 +(15 rows) + +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; + c | sum +---+--------- + r | 1501500 + g | 1499500 + b | 1500500 +(3 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_index_scan_size; +RESET enable_hashagg; +DROP TABLE eager_agg_c1; +DROP TABLE eager_agg_c2; +DROP OPERATOR CLASS eager_agg_color_ops USING btree; +DROP OPERATOR FAMILY eager_agg_color_ops USING btree; +DROP OPERATOR <<< (eager_agg_color, eager_agg_color); +DROP OPERATOR === (eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color); +DROP TYPE eager_agg_color; DROP TABLE eager_agg_t1; DROP TABLE eager_agg_t2; DROP TABLE eager_agg_t3; diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index 91f8342166f..bd349c55867 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -1457,6 +1457,48 @@ SELECT count(*) FROM btg GROUP BY w, x, z, y ORDER BY y, x, z, w; EXPLAIN (COSTS OFF) SELECT count(*) FROM btg GROUP BY w, x, y, z ORDER BY x*x, z; +-- Utilize the ordering of index scan even if it differs in sort direction +-- from the ordering that the GROUP BY keys took from ORDER BY +CREATE TABLE btg_dir AS + SELECT i % 4 AS x, i % 5 AS y, i AS w FROM generate_series(1, 1000) AS i; +CREATE INDEX btg_dir_x_y_idx ON btg_dir(x, y); +ANALYZE btg_dir; +EXPLAIN (COSTS OFF) +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC; +SELECT x, y, count(*) FROM btg_dir GROUP BY x, y ORDER BY x, y DESC LIMIT 6; + +-- Likewise for a backward index scan that also provides the ordering +-- required by the aggregate +EXPLAIN (COSTS OFF) +SELECT x, array_agg(y ORDER BY y DESC) FROM btg_dir GROUP BY x; +SELECT x, array_agg(DISTINCT y ORDER BY y DESC) FROM btg_dir GROUP BY x ORDER BY x; +DROP TABLE btg_dir; + +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error, though such an operator class is not valid +BEGIN; +CREATE TYPE btg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION btg_color_lt(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION btg_color_eq(btg_color, btg_color) RETURNS bool + LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION btg_color_cmp(btg_color, btg_color) RETURNS int + LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_lt); +CREATE OPERATOR === (LEFTARG = btg_color, RIGHTARG = btg_color, + FUNCTION = btg_color_eq); +CREATE OPERATOR CLASS btg_color_ops DEFAULT FOR TYPE btg_color USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 btg_color_cmp(btg_color, btg_color); +CREATE TABLE btg_color_tab AS + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::btg_color AS c + FROM generate_series(1, 300) AS i; +CREATE INDEX ON btg_color_tab (c); +ANALYZE btg_color_tab; +SELECT c, count(*) FROM btg_color_tab GROUP BY c ORDER BY c; +ROLLBACK; + -- Test the case where the number of incoming subtree path keys is more than -- the number of grouping keys. CREATE INDEX btg_y_x_w_idx ON btg(y, x, w); diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql index 7e3623aa6fd..6010506d274 100644 --- a/src/test/regress/sql/eager_aggregate.sql +++ b/src/test/regress/sql/eager_aggregate.sql @@ -226,6 +226,95 @@ GROUP BY s1.a; DROP TABLE eager_agg_s1; DROP TABLE eager_agg_s2; +-- Ensure the partial aggregation can use an index scan that differs in sort +-- direction from the ordering that the grouping keys took from ORDER BY +CREATE TABLE eager_agg_d1 (a int, b int, c int); +CREATE TABLE eager_agg_d2 (a int); +INSERT INTO eager_agg_d1 + SELECT i % 10, (i / 10) % 100, i FROM generate_series(1, 10000) i; +INSERT INTO eager_agg_d2 SELECT i FROM generate_series(0, 9) i; +CREATE INDEX eager_agg_d1_a_b_idx ON eager_agg_d1 (a, b); +ANALYZE eager_agg_d1, eager_agg_d2; + +SET enable_hashagg TO off; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + +SELECT d1.a, d1.b, sum(d1.c) + FROM eager_agg_d1 d1 + JOIN eager_agg_d2 d2 ON d1.a = d2.a +GROUP BY d1.a, d1.b ORDER BY d1.a, d1.b DESC LIMIT 5; + +RESET enable_hashagg; + +DROP TABLE eager_agg_d1; +DROP TABLE eager_agg_d2; + +-- An index scan in the direction that the operator class has no operator for +-- must not cause an error in the partial aggregation, though such an operator +-- class is not valid +CREATE TYPE eager_agg_color AS ENUM ('r', 'g', 'b'); +CREATE FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_lt'; +CREATE FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color) + RETURNS bool LANGUAGE internal IMMUTABLE STRICT AS 'enum_eq'; +CREATE FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color) + RETURNS int LANGUAGE internal IMMUTABLE STRICT AS 'enum_cmp'; +CREATE OPERATOR <<< (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_lt); +CREATE OPERATOR === (LEFTARG = eager_agg_color, RIGHTARG = eager_agg_color, + FUNCTION = eager_agg_color_eq); +-- FUNCTION 4 is needed for the grouping key to be usable in eager aggregation +CREATE OPERATOR CLASS eager_agg_color_ops DEFAULT FOR TYPE eager_agg_color + USING btree AS + OPERATOR 1 <<<, OPERATOR 3 ===, + FUNCTION 1 eager_agg_color_cmp(eager_agg_color, eager_agg_color), + FUNCTION 4 btequalimage(oid); +CREATE TABLE eager_agg_c1 (c eager_agg_color, d int, x int); +CREATE TABLE eager_agg_c2 (k int); +INSERT INTO eager_agg_c1 + SELECT (ARRAY['r', 'g', 'b'])[1 + i % 3]::eager_agg_color, i % 50, i + FROM generate_series(1, 3000) i; +INSERT INTO eager_agg_c2 SELECT generate_series(0, 49); +CREATE INDEX ON eager_agg_c1 (c DESC) INCLUDE (d, x); +VACUUM ANALYZE eager_agg_c1, eager_agg_c2; + + +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_index_scan_size = 0; +SET enable_hashagg = off; + +EXPLAIN (COSTS OFF) +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; + +SELECT c1.c, sum(c1.x) + FROM eager_agg_c1 c1 + JOIN eager_agg_c2 c2 ON c1.d = c2.k +GROUP BY c1.c; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_index_scan_size; +RESET enable_hashagg; + +DROP TABLE eager_agg_c1; +DROP TABLE eager_agg_c2; +DROP OPERATOR CLASS eager_agg_color_ops USING btree; +DROP OPERATOR FAMILY eager_agg_color_ops USING btree; +DROP OPERATOR <<< (eager_agg_color, eager_agg_color); +DROP OPERATOR === (eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_lt(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_eq(eager_agg_color, eager_agg_color); +DROP FUNCTION eager_agg_color_cmp(eager_agg_color, eager_agg_color); +DROP TYPE eager_agg_color; + DROP TABLE eager_agg_t1; DROP TABLE eager_agg_t2; DROP TABLE eager_agg_t3; -- 2.50.1 (Apple Git-155)