Skip to content

Commit d439bf1

Browse files
jeevanchalkehackorum
authored andcommitted
Add support for ON EMPTY clause in aggregate and window functions
This commit introduces the ON EMPTY clause, allowing users to specify a default value to be returned when an aggregate or window function receives an empty input set (zero processed rows). The syntax follows the pattern: agg_function(args, default_value ON EMPTY) The ON EMPTY clause is distinct from standard NULL handling. It is only invoked when the aggregate processes no rows at all. If the aggregate processes rows that happen to be NULL, the normal transition logic applies and the ON EMPTY clause is ignored. Likewise, when all rows are removed by a FILTER clause the input set is empty and the ON EMPTY value is returned. For a window aggregate, the default is returned for any row whose frame contains no rows. Implementation adds an 'inputReceived' flag to each per-group aggregate state. A dedicated EEOP_AGG_INPUT_RECEIVED expression step sets the flag once per row that reaches the aggregate -- after any FILTER but before the strict-input NULL check -- so that a row whose input is NULL (and is thus skipped by a strict transition function) still counts as input. The step is emitted only for aggregates that carry an ON EMPTY default. At finalization, if the flag is false, the default expression is evaluated and returned in place of the normal result (and final function, if any). Because a partial aggregate's leader only sees workers' combined transition states rather than the original rows, this flag cannot distinguish the empty-input case under partial aggregation; such aggregates are therefore marked non-partial so they are not parallelized. This patch implements the new step in the expression interpreter only; the feature is fully functional without LLVM. LLVM/JIT support for the EEOP_AGG_INPUT_RECEIVED step is added by a separate follow-on patch. The default expression must be a constant-like expression coercible to the aggregate's result type. It may not reference columns (at any query level) or contain aggregates, window functions, subqueries, or volatile functions. ON EMPTY cannot be combined with DISTINCT, and is rejected for non-aggregate window functions; it may be used with an ordered-set (WITHIN GROUP) aggregate, written before the WITHIN GROUP clause. Patch also includes documentation and regression tests for aggregates and window functions, covering empty tables, filtered results, type coercion, all-NULL inputs to strict aggregates, and view deparsing. This commit adds fields to Aggref and WindowFunc (and FuncCall). These node types are serialized into pg_rewrite (views/rules) and other stored expression trees, which requires a CATALOG_VERSION_NO bump. That is omitted here to avoid conflicts with concurrent commits; the committer should bump it at commit time. Proposed-by: Peter Eisentraut Jeevan Chalke
1 parent 44056f6 commit d439bf1

24 files changed

Lines changed: 1297 additions & 8 deletions

File tree

doc/src/sgml/syntax.sgml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,10 @@ sqrt(2)
15551555
<primary>FILTER</primary>
15561556
</indexterm>
15571557

1558+
<indexterm zone="syntax-aggregates">
1559+
<primary>ON EMPTY</primary>
1560+
</indexterm>
1561+
15581562
<para>
15591563
An <firstterm>aggregate expression</firstterm> represents the
15601564
application of an aggregate function across the rows selected by a
@@ -1564,6 +1568,7 @@ sqrt(2)
15641568

15651569
<synopsis>
15661570
<replaceable>aggregate_name</replaceable> (<replaceable>expression</replaceable> [ , ... ] [ <replaceable>order_by_clause</replaceable> ] ) [ FILTER ( WHERE <replaceable>filter_clause</replaceable> ) ]
1571+
<replaceable>aggregate_name</replaceable> (<replaceable>expression</replaceable> [ , ... ] , <replaceable>default_expression</replaceable> ON EMPTY [ <replaceable>order_by_clause</replaceable> ] ) [ FILTER ( WHERE <replaceable>filter_clause</replaceable> ) ]
15671572
<replaceable>aggregate_name</replaceable> (ALL <replaceable>expression</replaceable> [ , ... ] [ <replaceable>order_by_clause</replaceable> ] ) [ FILTER ( WHERE <replaceable>filter_clause</replaceable> ) ]
15681573
<replaceable>aggregate_name</replaceable> (DISTINCT <replaceable>expression</replaceable> [ , ... ] [ <replaceable>order_by_clause</replaceable> ] ) [ FILTER ( WHERE <replaceable>filter_clause</replaceable> ) ]
15691574
<replaceable>aggregate_name</replaceable> ( * ) [ FILTER ( WHERE <replaceable>filter_clause</replaceable> ) ]
@@ -1744,6 +1749,36 @@ FROM generate_series(1,10) AS s(i);
17441749
</programlisting>
17451750
</para>
17461751

1752+
<para>
1753+
If <literal>ON EMPTY</literal> is specified, then
1754+
<replaceable>default_expression</replaceable> supplies the value of the
1755+
aggregate when it processes no input rows at all; in that case the
1756+
aggregate's normal final result (and its final function, if any) is not
1757+
computed. This is distinct from null handling: it is triggered only by
1758+
an empty input set, not by null inputs that are merely ignored during
1759+
aggregation. Note that when rows exist but are all removed by a
1760+
<replaceable>filter_clause</replaceable>, the input set is empty and the
1761+
<literal>ON EMPTY</literal> value is returned. For example:
1762+
<programlisting>
1763+
SELECT sum(i) AS plain, sum(i, -1 ON EMPTY) AS defaulted
1764+
FROM generate_series(1,10) AS s(i) WHERE i &gt; 100;
1765+
plain | defaulted
1766+
-------+-----------
1767+
| -1
1768+
(1 row)
1769+
</programlisting>
1770+
<replaceable>default_expression</replaceable> must be a constant-like
1771+
expression of a type coercible to the aggregate's result type: it may not
1772+
refer to columns or contain aggregates, window functions, subqueries, or
1773+
volatile functions. <literal>ON EMPTY</literal> cannot be combined with
1774+
<literal>DISTINCT</literal>. It may be used with an ordered-set
1775+
aggregate, written before the <literal>WITHIN GROUP</literal> clause.
1776+
Because a grouped query never produces empty groups,
1777+
<literal>ON EMPTY</literal> only takes effect for an ungrouped aggregate
1778+
over zero rows (or a group whose rows are all removed by
1779+
<replaceable>filter_clause</replaceable>).
1780+
</para>
1781+
17471782
<para>
17481783
The predefined aggregate functions are described in <xref
17491784
linkend="functions-aggregate"/>. Other aggregate functions can be added
@@ -1843,6 +1878,17 @@ EXCLUDE NO OTHERS
18431878
described in <xref linkend="functions-window"/>.
18441879
</para>
18451880

1881+
<para>
1882+
When an aggregate function is used as a window function, an
1883+
<literal>ON EMPTY</literal> clause may be given after its arguments
1884+
(using the same syntax as for a plain aggregate call, described in
1885+
<xref linkend="syntax-aggregates"/>). The
1886+
<replaceable>default_expression</replaceable> is then returned for any
1887+
row whose window frame contains no rows, including a frame that becomes
1888+
empty as the frame moves. <literal>ON EMPTY</literal> is not accepted for
1889+
non-aggregate window functions.
1890+
</para>
1891+
18461892
<para>
18471893
<replaceable>window_name</replaceable> is a reference to a named window
18481894
specification defined in the query's <literal>WINDOW</literal> clause.

src/backend/executor/execExpr.c

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3736,6 +3736,70 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase,
37363736
state->steps_len - 1);
37373737
}
37383738

3739+
/*
3740+
* If aggonempty present, emit step(s) to record that the group has
3741+
* received an input row. This is placed after any FILTER (so that
3742+
* filtered-out rows do not count as input) but before the
3743+
* strict-input NULL check emitted below (so that a row whose input is
3744+
* NULL still counts as input).
3745+
*
3746+
* The step is emitted once per concurrently-evaluated grouping set,
3747+
* mirroring the transition-function calls below. We skip it entirely
3748+
* for aggregates without ON EMPTY so the common case pays no
3749+
* overhead.
3750+
*/
3751+
{
3752+
bool has_aggonempty = false;
3753+
3754+
for (int aggno = 0; aggno < aggstate->numaggs; aggno++)
3755+
{
3756+
if (aggstate->peragg[aggno].transno == transno &&
3757+
aggstate->peragg[aggno].aggref->aggonempty != NULL)
3758+
{
3759+
has_aggonempty = true;
3760+
break;
3761+
}
3762+
}
3763+
3764+
if (has_aggonempty)
3765+
{
3766+
scratch.opcode = EEOP_AGG_INPUT_RECEIVED;
3767+
scratch.d.agg_input_received.transno = transno;
3768+
3769+
if (doSort)
3770+
{
3771+
int processGroupingSets = Max(phase->numsets, 1);
3772+
int setoff = 0;
3773+
3774+
for (int setno = 0; setno < processGroupingSets; setno++)
3775+
{
3776+
scratch.d.agg_input_received.setoff = setoff;
3777+
ExprEvalPushStep(state, &scratch);
3778+
setoff++;
3779+
}
3780+
}
3781+
3782+
if (doHash)
3783+
{
3784+
int numHashes = aggstate->num_hashes;
3785+
int setoff;
3786+
3787+
/* in MIXED mode, there'll be preceding transition values */
3788+
if (aggstate->aggstrategy != AGG_HASHED)
3789+
setoff = aggstate->maxsets;
3790+
else
3791+
setoff = 0;
3792+
3793+
for (int setno = 0; setno < numHashes; setno++)
3794+
{
3795+
scratch.d.agg_input_received.setoff = setoff;
3796+
ExprEvalPushStep(state, &scratch);
3797+
setoff++;
3798+
}
3799+
}
3800+
}
3801+
}
3802+
37393803
/*
37403804
* Evaluate arguments to aggregate/combine function.
37413805
*/

src/backend/executor/execExprInterp.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
592592
&&CASE_EEOP_AGG_STRICT_INPUT_CHECK_ARGS_1,
593593
&&CASE_EEOP_AGG_STRICT_INPUT_CHECK_NULLS,
594594
&&CASE_EEOP_AGG_PLAIN_PERGROUP_NULLCHECK,
595+
&&CASE_EEOP_AGG_INPUT_RECEIVED,
595596
&&CASE_EEOP_AGG_PLAIN_TRANS_INIT_STRICT_BYVAL,
596597
&&CASE_EEOP_AGG_PLAIN_TRANS_STRICT_BYVAL,
597598
&&CASE_EEOP_AGG_PLAIN_TRANS_BYVAL,
@@ -2106,6 +2107,27 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
21062107
EEO_NEXT();
21072108
}
21082109

2110+
/*
2111+
* Mark that an aggregate's per-group state has received an input row
2112+
* (used by the ON EMPTY clause). This is emitted once per row that
2113+
* reaches the aggregate, after any FILTER but before the strict-input
2114+
* NULL check, so that a row whose input is NULL still counts as
2115+
* input. Only emitted for aggregates that actually carry an ON EMPTY
2116+
* default.
2117+
*/
2118+
EEO_CASE(EEOP_AGG_INPUT_RECEIVED)
2119+
{
2120+
AggState *aggstate = castNode(AggState, state->parent);
2121+
AggStatePerGroup pergroup_allaggs =
2122+
aggstate->all_pergroups[op->d.agg_input_received.setoff];
2123+
2124+
/* pergroup may be NULL in the hashed/spilled case; just skip */
2125+
if (pergroup_allaggs != NULL)
2126+
pergroup_allaggs[op->d.agg_input_received.transno].inputReceived = true;
2127+
2128+
EEO_NEXT();
2129+
}
2130+
21092131
/*
21102132
* Different types of aggregate transition functions are implemented
21112133
* as different types of steps, to avoid incurring unnecessary

src/backend/executor/nodeAgg.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,12 @@ initialize_aggregate(AggState *aggstate, AggStatePerTrans pertrans,
649649
* still need to do this.
650650
*/
651651
pergroupstate->noTransValue = pertrans->initValueIsNull;
652+
653+
/*
654+
* Initialize the flag used for ON EMPTY to track whether any input rows
655+
* were received.
656+
*/
657+
pergroupstate->inputReceived = false;
652658
}
653659

654660
/*
@@ -1057,6 +1063,25 @@ finalize_aggregate(AggState *aggstate,
10571063

10581064
oldContext = MemoryContextSwitchTo(aggstate->ss.ps.ps_ExprContext->ecxt_per_tuple_memory);
10591065

1066+
/*
1067+
* If the ON EMPTY clause is specified with a default value, evaluate and
1068+
* return it in cases where no input rows were received.
1069+
*
1070+
* The inputReceived flag is used to detect the empty set case (zero rows
1071+
* processed).
1072+
*/
1073+
if (peragg->aggonemptystate != NULL && !pergroupstate->inputReceived)
1074+
{
1075+
*resultVal = ExecEvalExpr(peragg->aggonemptystate,
1076+
aggstate->ss.ps.ps_ExprContext,
1077+
resultIsNull);
1078+
1079+
/* Switch back to the caller's context before returning */
1080+
MemoryContextSwitchTo(oldContext);
1081+
1082+
return;
1083+
}
1084+
10601085
/*
10611086
* Evaluate any direct arguments. We do this even if there's no finalfn
10621087
* (which is unlikely anyway), so that side-effects happen as expected.
@@ -3959,6 +3984,13 @@ ExecInitAgg(Agg *node, EState *estate, int eflags)
39593984
get_func_name(transfn_oid));
39603985
InvokeFunctionExecuteHook(transfn_oid);
39613986

3987+
/* Build expression state for ON EMPTY default expression */
3988+
if (aggref->aggonempty)
3989+
peragg->aggonemptystate = ExecInitExpr(aggref->aggonempty,
3990+
(PlanState *) aggstate);
3991+
else
3992+
peragg->aggonemptystate = NULL;
3993+
39623994
/*
39633995
* initval is potentially null, so don't try to access it as a
39643996
* struct field. Must do it the hard way with SysCacheGetAttr.

src/backend/executor/nodeWindowAgg.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ typedef struct WindowStatePerAggData
171171

172172
/* Data local to eval_windowaggregates() */
173173
bool restart; /* need to restart this agg in this cycle? */
174+
175+
/* ON EMPTY support */
176+
bool inputReceived; /* true if any input row was received in
177+
* current frame */
178+
/* ExprState for evaluating ON EMPTY default value, or NULL */
179+
ExprState *aggonemptystate;
174180
} WindowStatePerAggData;
175181

176182
static void initialize_windowaggregate(WindowAggState *winstate,
@@ -278,6 +284,12 @@ initialize_windowaggregate(WindowAggState *winstate,
278284
peraggstate->transValueCount = 0;
279285
peraggstate->resultValue = (Datum) 0;
280286
peraggstate->resultValueIsNull = true;
287+
288+
/*
289+
* Initialize the flag used for ON EMPTY to track whether any input rows
290+
* were received in this frame.
291+
*/
292+
peraggstate->inputReceived = false;
281293
}
282294

283295
/*
@@ -325,6 +337,9 @@ advance_windowaggregate(WindowAggState *winstate,
325337
i++;
326338
}
327339

340+
/* Mark that this aggregate received input (used for ON EMPTY) */
341+
peraggstate->inputReceived = true;
342+
328343
if (peraggstate->transfn.fn_strict)
329344
{
330345
/*
@@ -634,6 +649,25 @@ finalize_windowaggregate(WindowAggState *winstate,
634649

635650
oldContext = MemoryContextSwitchTo(winstate->ss.ps.ps_ExprContext->ecxt_per_tuple_memory);
636651

652+
/*
653+
* If the ON EMPTY clause is specified with a default value, evaluate and
654+
* return it in cases where no input rows were received in the current
655+
* frame.
656+
*
657+
* The inputReceived flag is used to detect the empty frame case (zero
658+
* rows processed in this frame).
659+
*/
660+
if (peraggstate->aggonemptystate != NULL && !peraggstate->inputReceived)
661+
{
662+
*result = ExecEvalExpr(peraggstate->aggonemptystate,
663+
winstate->ss.ps.ps_ExprContext,
664+
isnull);
665+
666+
MemoryContextSwitchTo(oldContext);
667+
668+
return;
669+
}
670+
637671
/*
638672
* Apply the agg's finalfn if one is provided, else return transValue.
639673
*/
@@ -3131,6 +3165,13 @@ initialize_peragg(WindowAggState *winstate, WindowFunc *wfunc,
31313165
&peraggstate->transtypeLen,
31323166
&peraggstate->transtypeByVal);
31333167

3168+
/* Build expression state for ON EMPTY default expression */
3169+
if (wfunc->aggonempty)
3170+
peraggstate->aggonemptystate = ExecInitExpr(wfunc->aggonempty,
3171+
(PlanState *) winstate);
3172+
else
3173+
peraggstate->aggonemptystate = NULL;
3174+
31343175
/*
31353176
* initval is potentially null, so don't try to access it as a struct
31363177
* field. Must do it the hard way with SysCacheGetAttr.

src/backend/nodes/makefuncs.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,7 @@ makeFuncCall(List *name, List *args, CoercionForm funcformat, int location)
681681
n->args = args;
682682
n->agg_order = NIL;
683683
n->agg_filter = NULL;
684+
n->agg_on_empty = NULL;
684685
n->over = NULL;
685686
n->agg_within_group = false;
686687
n->agg_star = false;

src/backend/nodes/nodeFuncs.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,6 +2168,8 @@ expression_tree_walker_impl(Node *node,
21682168
return true;
21692169
if (WALK(expr->aggfilter))
21702170
return true;
2171+
if (WALK(expr->aggonempty))
2172+
return true;
21712173
}
21722174
break;
21732175
case T_GroupingFunc:
@@ -2187,6 +2189,8 @@ expression_tree_walker_impl(Node *node,
21872189
return true;
21882190
if (WALK(expr->aggfilter))
21892191
return true;
2192+
if (WALK(expr->aggonempty))
2193+
return true;
21902194
if (WALK(expr->runCondition))
21912195
return true;
21922196
}
@@ -3101,6 +3105,7 @@ expression_tree_mutator_impl(Node *node,
31013105
MUTATE(newnode->aggorder, aggref->aggorder, List *);
31023106
MUTATE(newnode->aggdistinct, aggref->aggdistinct, List *);
31033107
MUTATE(newnode->aggfilter, aggref->aggfilter, Expr *);
3108+
MUTATE(newnode->aggonempty, aggref->aggonempty, Expr *);
31043109
return (Node *) newnode;
31053110
}
31063111
break;
@@ -3135,6 +3140,7 @@ expression_tree_mutator_impl(Node *node,
31353140
FLATCOPY(newnode, wfunc, WindowFunc);
31363141
MUTATE(newnode->args, wfunc->args, List *);
31373142
MUTATE(newnode->aggfilter, wfunc->aggfilter, Expr *);
3143+
MUTATE(newnode->aggonempty, wfunc->aggonempty, Expr *);
31383144
return (Node *) newnode;
31393145
}
31403146
break;
@@ -4532,6 +4538,8 @@ raw_expression_tree_walker_impl(Node *node,
45324538
return true;
45334539
if (WALK(fcall->agg_filter))
45344540
return true;
4541+
if (WALK(fcall->agg_on_empty))
4542+
return true;
45354543
if (WALK(fcall->over))
45364544
return true;
45374545
/* function name is deemed uninteresting */

src/backend/optimizer/prep/prepagg.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ preprocess_aggref(Aggref *aggref, PlannerInfo *root)
215215

216216
ReleaseSysCache(aggTuple);
217217

218+
/*
219+
* An ON EMPTY default is returned only when the aggregate processes zero
220+
* input rows, which is detected via a per-group "input received" flag set
221+
* as the aggregate's input rows are processed. Under partial aggregation
222+
* the leader sees only the workers' combined partial states (via the
223+
* combine function), not the original rows, so that flag cannot reliably
224+
* distinguish the empty-input case. Disable partial aggregation for such
225+
* aggregates.
226+
*/
227+
if (aggref->aggonempty != NULL)
228+
root->hasNonPartialAggs = true;
229+
218230
/*
219231
* 1. See if this is identical to another aggregate function call that
220232
* we've seen already.

0 commit comments

Comments
 (0)