mirror of
https://github.com/postgres/postgres.git
synced 2025-07-12 21:01:52 +03:00
Support GROUPING SETS, CUBE and ROLLUP.
This SQL standard functionality allows to aggregate data by different GROUP BY clauses at once. Each grouping set returns rows with columns grouped by in other sets set to NULL. This could previously be achieved by doing each grouping as a separate query, conjoined by UNION ALLs. Besides being considerably more concise, grouping sets will in many cases be faster, requiring only one scan over the underlying data. The current implementation of grouping sets only supports using sorting for input. Individual sets that share a sort order are computed in one pass. If there are sets that don't share a sort order, additional sort & aggregation steps are performed. These additional passes are sourced by the previous sort step; thus avoiding repeated scans of the source data. The code is structured in a way that adding support for purely using hash aggregation or a mix of hashing and sorting is possible. Sorting was chosen to be supported first, as it is the most generic method of implementation. Instead of, as in an earlier versions of the patch, representing the chain of sort and aggregation steps as full blown planner and executor nodes, all but the first sort are performed inside the aggregation node itself. This avoids the need to do some unusual gymnastics to handle having to return aggregated and non-aggregated tuples from underlying nodes, as well as having to shut down underlying nodes early to limit memory usage. The optimizer still builds Sort/Agg node to describe each phase, but they're not part of the plan tree, but instead additional data for the aggregation node. They're a convenient and preexisting way to describe aggregation and sorting. The first (and possibly only) sort step is still performed as a separate execution step. That retains similarity with existing group by plans, makes rescans fairly simple, avoids very deep plans (leading to slow explains) and easily allows to avoid the sorting step if the underlying data is sorted by other means. A somewhat ugly side of this patch is having to deal with a grammar ambiguity between the new CUBE keyword and the cube extension/functions named cube (and rollup). To avoid breaking existing deployments of the cube extension it has not been renamed, neither has cube been made a reserved keyword. Instead precedence hacking is used to make GROUP BY cube(..) refer to the CUBE grouping sets feature, and not the function cube(). To actually group by a function cube(), unlikely as that might be, the function name has to be quoted. Needs a catversion bump because stored rules may change. Author: Andrew Gierth and Atri Sharma, with contributions from Andres Freund Reviewed-By: Andres Freund, Noah Misch, Tom Lane, Svenne Krap, Tomas Vondra, Erik Rijkers, Marti Raudsepp, Pavel Stehule Discussion: CAOeZVidmVRe2jU6aMk_5qkxnB7dfmPROzM7Ur8JPW5j8Y5X-Lw@mail.gmail.com
This commit is contained in:
@ -1290,6 +1290,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel,
|
||||
*/
|
||||
if (parse->hasAggs ||
|
||||
parse->groupClause ||
|
||||
parse->groupingSets ||
|
||||
parse->havingQual ||
|
||||
parse->distinctClause ||
|
||||
parse->sortClause ||
|
||||
@ -2150,7 +2151,7 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual)
|
||||
* subquery uses grouping or aggregation, put it in HAVING (since the
|
||||
* qual really refers to the group-result rows).
|
||||
*/
|
||||
if (subquery->hasAggs || subquery->groupClause || subquery->havingQual)
|
||||
if (subquery->hasAggs || subquery->groupClause || subquery->groupingSets || subquery->havingQual)
|
||||
subquery->havingQual = make_and_qual(subquery->havingQual, qual);
|
||||
else
|
||||
subquery->jointree->quals =
|
||||
|
@ -1954,7 +1954,8 @@ adjust_rowcount_for_semijoins(PlannerInfo *root,
|
||||
nraw = approximate_joinrel_size(root, sjinfo->syn_righthand);
|
||||
nunique = estimate_num_groups(root,
|
||||
sjinfo->semi_rhs_exprs,
|
||||
nraw);
|
||||
nraw,
|
||||
NULL);
|
||||
if (rowcount > nunique)
|
||||
rowcount = nunique;
|
||||
}
|
||||
|
@ -581,6 +581,7 @@ query_supports_distinctness(Query *query)
|
||||
{
|
||||
if (query->distinctClause != NIL ||
|
||||
query->groupClause != NIL ||
|
||||
query->groupingSets != NIL ||
|
||||
query->hasAggs ||
|
||||
query->havingQual ||
|
||||
query->setOperations)
|
||||
@ -649,10 +650,10 @@ query_is_distinct_for(Query *query, List *colnos, List *opids)
|
||||
}
|
||||
|
||||
/*
|
||||
* Similarly, GROUP BY guarantees uniqueness if all the grouped columns
|
||||
* appear in colnos and operator semantics match.
|
||||
* Similarly, GROUP BY without GROUPING SETS guarantees uniqueness if all
|
||||
* the grouped columns appear in colnos and operator semantics match.
|
||||
*/
|
||||
if (query->groupClause)
|
||||
if (query->groupClause && !query->groupingSets)
|
||||
{
|
||||
foreach(l, query->groupClause)
|
||||
{
|
||||
@ -668,6 +669,27 @@ query_is_distinct_for(Query *query, List *colnos, List *opids)
|
||||
if (l == NULL) /* had matches for all? */
|
||||
return true;
|
||||
}
|
||||
else if (query->groupingSets)
|
||||
{
|
||||
/*
|
||||
* If we have grouping sets with expressions, we probably
|
||||
* don't have uniqueness and analysis would be hard. Punt.
|
||||
*/
|
||||
if (query->groupClause)
|
||||
return false;
|
||||
|
||||
/*
|
||||
* If we have no groupClause (therefore no grouping expressions),
|
||||
* we might have one or many empty grouping sets. If there's just
|
||||
* one, then we're returning only one row and are certainly unique.
|
||||
* But otherwise, we know we're certainly not unique.
|
||||
*/
|
||||
if (list_length(query->groupingSets) == 1 &&
|
||||
((GroupingSet *)linitial(query->groupingSets))->kind == GROUPING_SET_EMPTY)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
|
@ -1042,6 +1042,7 @@ create_unique_plan(PlannerInfo *root, UniquePath *best_path)
|
||||
numGroupCols,
|
||||
groupColIdx,
|
||||
groupOperators,
|
||||
NIL,
|
||||
numGroups,
|
||||
subplan);
|
||||
}
|
||||
@ -4492,6 +4493,7 @@ Agg *
|
||||
make_agg(PlannerInfo *root, List *tlist, List *qual,
|
||||
AggStrategy aggstrategy, const AggClauseCosts *aggcosts,
|
||||
int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators,
|
||||
List *groupingSets,
|
||||
long numGroups,
|
||||
Plan *lefttree)
|
||||
{
|
||||
@ -4521,10 +4523,12 @@ make_agg(PlannerInfo *root, List *tlist, List *qual,
|
||||
* group otherwise.
|
||||
*/
|
||||
if (aggstrategy == AGG_PLAIN)
|
||||
plan->plan_rows = 1;
|
||||
plan->plan_rows = groupingSets ? list_length(groupingSets) : 1;
|
||||
else
|
||||
plan->plan_rows = numGroups;
|
||||
|
||||
node->groupingSets = groupingSets;
|
||||
|
||||
/*
|
||||
* We also need to account for the cost of evaluation of the qual (ie, the
|
||||
* HAVING clause) and the tlist. Note that cost_qual_eval doesn't charge
|
||||
@ -4545,6 +4549,7 @@ make_agg(PlannerInfo *root, List *tlist, List *qual,
|
||||
|
||||
plan->qual = qual;
|
||||
plan->targetlist = tlist;
|
||||
|
||||
plan->lefttree = lefttree;
|
||||
plan->righttree = NULL;
|
||||
|
||||
|
@ -96,7 +96,7 @@ preprocess_minmax_aggregates(PlannerInfo *root, List *tlist)
|
||||
* performs assorted processing related to these features between calling
|
||||
* preprocess_minmax_aggregates and optimize_minmax_aggregates.)
|
||||
*/
|
||||
if (parse->groupClause || parse->hasWindowFuncs)
|
||||
if (parse->groupClause || list_length(parse->groupingSets) > 1 || parse->hasWindowFuncs)
|
||||
return;
|
||||
|
||||
/*
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -140,7 +140,6 @@ static bool fix_opfuncids_walker(Node *node, void *context);
|
||||
static bool extract_query_dependencies_walker(Node *node,
|
||||
PlannerInfo *context);
|
||||
|
||||
|
||||
/*****************************************************************************
|
||||
*
|
||||
* SUBPLAN REFERENCES
|
||||
@ -656,6 +655,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
|
||||
}
|
||||
break;
|
||||
case T_Agg:
|
||||
set_upper_references(root, plan, rtoffset);
|
||||
break;
|
||||
case T_Group:
|
||||
set_upper_references(root, plan, rtoffset);
|
||||
break;
|
||||
@ -1229,6 +1230,7 @@ copyVar(Var *var)
|
||||
* We must look up operator opcode info for OpExpr and related nodes,
|
||||
* add OIDs from regclass Const nodes into root->glob->relationOids, and
|
||||
* add catalog TIDs for user-defined functions into root->glob->invalItems.
|
||||
* We also fill in column index lists for GROUPING() expressions.
|
||||
*
|
||||
* We assume it's okay to update opcode info in-place. So this could possibly
|
||||
* scribble on the planner's input data structures, but it's OK.
|
||||
@ -1292,6 +1294,31 @@ fix_expr_common(PlannerInfo *root, Node *node)
|
||||
lappend_oid(root->glob->relationOids,
|
||||
DatumGetObjectId(con->constvalue));
|
||||
}
|
||||
else if (IsA(node, GroupingFunc))
|
||||
{
|
||||
GroupingFunc *g = (GroupingFunc *) node;
|
||||
AttrNumber *grouping_map = root->grouping_map;
|
||||
|
||||
/* If there are no grouping sets, we don't need this. */
|
||||
|
||||
Assert(grouping_map || g->cols == NIL);
|
||||
|
||||
if (grouping_map)
|
||||
{
|
||||
ListCell *lc;
|
||||
List *cols = NIL;
|
||||
|
||||
foreach(lc, g->refs)
|
||||
{
|
||||
cols = lappend_int(cols, grouping_map[lfirst_int(lc)]);
|
||||
}
|
||||
|
||||
Assert(!g->cols || equal(cols, g->cols));
|
||||
|
||||
if (!g->cols)
|
||||
g->cols = cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -2186,6 +2213,7 @@ set_returning_clause_references(PlannerInfo *root,
|
||||
return rlist;
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************
|
||||
* OPERATOR REGPROC LOOKUP
|
||||
*****************************************************************************/
|
||||
|
@ -335,6 +335,48 @@ replace_outer_agg(PlannerInfo *root, Aggref *agg)
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate a Param node to replace the given GroupingFunc expression which is
|
||||
* expected to have agglevelsup > 0 (ie, it is not local).
|
||||
*/
|
||||
static Param *
|
||||
replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp)
|
||||
{
|
||||
Param *retval;
|
||||
PlannerParamItem *pitem;
|
||||
Index levelsup;
|
||||
|
||||
Assert(grp->agglevelsup > 0 && grp->agglevelsup < root->query_level);
|
||||
|
||||
/* Find the query level the GroupingFunc belongs to */
|
||||
for (levelsup = grp->agglevelsup; levelsup > 0; levelsup--)
|
||||
root = root->parent_root;
|
||||
|
||||
/*
|
||||
* It does not seem worthwhile to try to match duplicate outer aggs. Just
|
||||
* make a new slot every time.
|
||||
*/
|
||||
grp = (GroupingFunc *) copyObject(grp);
|
||||
IncrementVarSublevelsUp((Node *) grp, -((int) grp->agglevelsup), 0);
|
||||
Assert(grp->agglevelsup == 0);
|
||||
|
||||
pitem = makeNode(PlannerParamItem);
|
||||
pitem->item = (Node *) grp;
|
||||
pitem->paramId = root->glob->nParamExec++;
|
||||
|
||||
root->plan_params = lappend(root->plan_params, pitem);
|
||||
|
||||
retval = makeNode(Param);
|
||||
retval->paramkind = PARAM_EXEC;
|
||||
retval->paramid = pitem->paramId;
|
||||
retval->paramtype = exprType((Node *) grp);
|
||||
retval->paramtypmod = -1;
|
||||
retval->paramcollid = InvalidOid;
|
||||
retval->location = grp->location;
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate a new Param node that will not conflict with any other.
|
||||
*
|
||||
@ -1494,14 +1536,16 @@ simplify_EXISTS_query(PlannerInfo *root, Query *query)
|
||||
{
|
||||
/*
|
||||
* We don't try to simplify at all if the query uses set operations,
|
||||
* aggregates, modifying CTEs, HAVING, OFFSET, or FOR UPDATE/SHARE; none
|
||||
* of these seem likely in normal usage and their possible effects are
|
||||
* complex. (Note: we could ignore an "OFFSET 0" clause, but that
|
||||
* traditionally is used as an optimization fence, so we don't.)
|
||||
* aggregates, grouping sets, modifying CTEs, HAVING, OFFSET, or FOR
|
||||
* UPDATE/SHARE; none of these seem likely in normal usage and their
|
||||
* possible effects are complex. (Note: we could ignore an "OFFSET 0"
|
||||
* clause, but that traditionally is used as an optimization fence, so we
|
||||
* don't.)
|
||||
*/
|
||||
if (query->commandType != CMD_SELECT ||
|
||||
query->setOperations ||
|
||||
query->hasAggs ||
|
||||
query->groupingSets ||
|
||||
query->hasWindowFuncs ||
|
||||
query->hasModifyingCTE ||
|
||||
query->havingQual ||
|
||||
@ -1851,6 +1895,11 @@ replace_correlation_vars_mutator(Node *node, PlannerInfo *root)
|
||||
if (((Aggref *) node)->agglevelsup > 0)
|
||||
return (Node *) replace_outer_agg(root, (Aggref *) node);
|
||||
}
|
||||
if (IsA(node, GroupingFunc))
|
||||
{
|
||||
if (((GroupingFunc *) node)->agglevelsup > 0)
|
||||
return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
|
||||
}
|
||||
return expression_tree_mutator(node,
|
||||
replace_correlation_vars_mutator,
|
||||
(void *) root);
|
||||
|
@ -1412,6 +1412,7 @@ is_simple_subquery(Query *subquery, RangeTblEntry *rte,
|
||||
if (subquery->hasAggs ||
|
||||
subquery->hasWindowFuncs ||
|
||||
subquery->groupClause ||
|
||||
subquery->groupingSets ||
|
||||
subquery->havingQual ||
|
||||
subquery->sortClause ||
|
||||
subquery->distinctClause ||
|
||||
|
@ -268,13 +268,15 @@ recurse_set_operations(Node *setOp, PlannerInfo *root,
|
||||
*/
|
||||
if (pNumGroups)
|
||||
{
|
||||
if (subquery->groupClause || subquery->distinctClause ||
|
||||
if (subquery->groupClause || subquery->groupingSets ||
|
||||
subquery->distinctClause ||
|
||||
subroot->hasHavingQual || subquery->hasAggs)
|
||||
*pNumGroups = subplan->plan_rows;
|
||||
else
|
||||
*pNumGroups = estimate_num_groups(subroot,
|
||||
get_tlist_exprs(subquery->targetList, false),
|
||||
subplan->plan_rows);
|
||||
subplan->plan_rows,
|
||||
NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -771,6 +773,7 @@ make_union_unique(SetOperationStmt *op, Plan *plan,
|
||||
extract_grouping_cols(groupList,
|
||||
plan->targetlist),
|
||||
extract_grouping_ops(groupList),
|
||||
NIL,
|
||||
numGroups,
|
||||
plan);
|
||||
/* Hashed aggregation produces randomly-ordered results */
|
||||
|
@ -4353,6 +4353,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
|
||||
querytree->jointree->fromlist ||
|
||||
querytree->jointree->quals ||
|
||||
querytree->groupClause ||
|
||||
querytree->groupingSets ||
|
||||
querytree->havingQual ||
|
||||
querytree->windowClause ||
|
||||
querytree->distinctClause ||
|
||||
|
@ -1214,7 +1214,8 @@ create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
|
||||
/* Estimate number of output rows */
|
||||
pathnode->path.rows = estimate_num_groups(root,
|
||||
sjinfo->semi_rhs_exprs,
|
||||
rel->rows);
|
||||
rel->rows,
|
||||
NULL);
|
||||
numCols = list_length(sjinfo->semi_rhs_exprs);
|
||||
|
||||
if (sjinfo->semi_can_btree)
|
||||
|
@ -394,6 +394,28 @@ get_sortgrouplist_exprs(List *sgClauses, List *targetList)
|
||||
* functions just above, and they don't seem to deserve their own file.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* get_sortgroupref_clause
|
||||
* Find the SortGroupClause matching the given SortGroupRef index,
|
||||
* and return it.
|
||||
*/
|
||||
SortGroupClause *
|
||||
get_sortgroupref_clause(Index sortref, List *clauses)
|
||||
{
|
||||
ListCell *l;
|
||||
|
||||
foreach(l, clauses)
|
||||
{
|
||||
SortGroupClause *cl = (SortGroupClause *) lfirst(l);
|
||||
|
||||
if (cl->tleSortGroupRef == sortref)
|
||||
return cl;
|
||||
}
|
||||
|
||||
elog(ERROR, "ORDER/GROUP BY expression not found in list");
|
||||
return NULL; /* keep compiler quiet */
|
||||
}
|
||||
|
||||
/*
|
||||
* extract_grouping_ops - make an array of the equality operator OIDs
|
||||
* for a SortGroupClause list
|
||||
|
@ -564,6 +564,30 @@ pull_var_clause_walker(Node *node, pull_var_clause_context *context)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (IsA(node, GroupingFunc))
|
||||
{
|
||||
if (((GroupingFunc *) node)->agglevelsup != 0)
|
||||
elog(ERROR, "Upper-level GROUPING found where not expected");
|
||||
switch (context->aggbehavior)
|
||||
{
|
||||
case PVC_REJECT_AGGREGATES:
|
||||
elog(ERROR, "GROUPING found where not expected");
|
||||
break;
|
||||
case PVC_INCLUDE_AGGREGATES:
|
||||
context->varlist = lappend(context->varlist, node);
|
||||
/* we do NOT descend into the contained expression */
|
||||
return false;
|
||||
case PVC_RECURSE_AGGREGATES:
|
||||
/*
|
||||
* we do NOT descend into the contained expression,
|
||||
* even if the caller asked for it, because we never
|
||||
* actually evaluate it - the result is driven entirely
|
||||
* off the associated GROUP BY clause, so we never need
|
||||
* to extract the actual Vars here.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (IsA(node, PlaceHolderVar))
|
||||
{
|
||||
if (((PlaceHolderVar *) node)->phlevelsup != 0)
|
||||
|
Reference in New Issue
Block a user