Proposal for aggregate-function cleanups in 7.1 - Mailing list pgsql-hackers

From Tom Lane
Subject Proposal for aggregate-function cleanups in 7.1
Date
Msg-id 25147.963442427@sss.pgh.pa.us
Whole thread Raw
List pgsql-hackers
There are quite a number of misfeatures in Postgres' current support for
aggregate functions (COUNT, SUM, etc).  We've had previous discussion
threads about them in the past, see pghackers around 1/23/00 and 6/16/99
for example.  I propose to fix a few things for 7.1:

* Hard-wired boundary condition handling in ExecAgg prevents
  user-defined aggregates from determining their treatment of nulls,
  no input rows, etc.
* Some SUM and AVG aggregates are vulnerable to overflow due to poor
  choice of datatypes for accumulators.
* Dual-transition-function design is bizarre and unnecessary.

The last of these might break user-defined aggregates, if there are
any out there that use two transition functions, so I'm soliciting
comments first.

Currently, ExecAgg evaluates each aggregate in the following steps:
(initcond1, initcond2 are the initial values and sfunc1, sfunc2, and
finalfunc are the transition functions.)

    value1 = initcond1
    value2 = initcond2
    foreach input_value do
        value1 = sfunc1(value1, input_value)
        value2 = sfunc2(value2)
    value1 = finalfunc(value1, value2)

(It's possible to omit one of sfunc1/sfunc2, and then optionally omit
finalfunc as well, but that doesn't affect things materially other than
creating a bunch of confusing rules about which combinations are legal.)

The following behaviors are currently hard-wired in ExecAgg and can't
be changed by the transition functions:
* If initcond1 is NULL then the first non-NULL input_value is assigned
  directly to value1.  sfunc1 isn't applied until value1 is non-NULL.
  (This behavior is useful for aggregates like min() and max() where
  you really want value1 to just be the least or greatest value so far.)
* When the current tuple's input_value is NULL, sfunc1 and sfunc2 are
  not applied, instead the previous value1 and value2 are kept.
* If value1 is still NULL at the end (ie, there were no non-null input
  values), then finalfunc is not invoked, instead a NULL result is
  forced.

These behaviors were necessary back when we didn't have a reasonable
design for NULL handling in the function manager, but now that we do
it is possible to allow the transition functions to control behavior
for NULL inputs and zero input rows, instead of hard-wiring the results.
I propose the following definition instead:

1. If sfunc1 is not marked "strict" in pg_proc, then it is invoked for
   every input tuple and must do all the right things for itself.
   ExecAgg will not protect it against a NULL value1 or NULL input.
   An sfunc1 coded in this style might use value1 = NULL as a flag
   for "no input data yet".  It is up to the function whether and
   how to change the state value when the input_value is NULL.

2. If sfunc1 is marked "strict" then it will never be called with
   NULL inputs.  Therefore, ExecAgg will automatically skip tuples
   with NULL input_values, preserving the previous value1/value2.
   Also, if value1 is initially NULL, then the first non-NULL
   input_value will automatically be inserted into value1; sfunc1
   will only be called beginning with the second non-NULL input.
   (This is essentially the same as current behavior.)

3. If finalfunc is not marked "strict" then it will be called in
   any case, even if value1 is NULL from its initial setting or
   as a result of sfunc1 having returned NULL.  It is up to the
   finalfunc to return an appropriate value or NULL.

4. If finalfunc is marked "strict" then it cannot be called with
   NULL inputs, so a NULL result will be substituted if value1
   or value2 is still NULL.  (Same as current behavior.)

sfunc2 is a somewhat peculiar appendage in this scheme.  To do AVG()
correctly, sfunc2 should only be called for the same input tuples that
sfunc1 is called for (ie, not for NULLs).  This implies that a "strict"
sfunc2 must not be called when input_value is NULL, even though
input_value is not actually being passed to sfunc2!  This seems like a
pretty weird abuse of the notion of function strictness.  It's also quite
unclear whether it makes any sense to have sfunc1 and sfunc2 with
different strictness settings.

What I would *like* to do about this is eliminate value2 and sfunc2 from
the picture completely, leaving us with a clean single-transition-value
design for aggregate handling.  That would mean that AVG() would need to
be redone with a specialized transition datatype (holding both the sum and
count) and specialized transition function and final function, rather than
using addition/increment/divide as it does now.  Given the AVG fixes I
intend to do anyway, this isn't significant extra work as far as the
standard distribution is concerned.  But it would break any existing
user-defined aggregates that may be out there, if they use two transition
functions.  Is there anyone who has such functions and wants to object?

As to the other point: currently, the SUM and AVG aggregates use an
accumulator of the same datatype as the input data.  This is almost
guaranteed to cause overflow if the input column is int2, and there's
a nontrivial risk for int4 as well.  It also seems bizarre to me that
AVG on an integer column delivers an integral result --- it should
yield a float or numeric result so as not to lose the fractional part.

What I propose to do is reimplement SUM and AVG so that there are
basically only two underlying implementations: one with a "float8"
accumulator and one with a "numeric" accumulator.  The float8 style
would be used for either float4 or float8 input and would deliver a
float8 result.  The numeric style would be used for all numeric and
integral input types and would yield a numeric result of appropriate
precision.

(It might also be worth reimplementing COUNT() to use a "numeric"
counter, so that it doesn't poop out at 2 billion rows.  Anyone have
a strong feeling pro or con about that?  It could possibly cause problems
for queries that are expecting COUNT() to deliver an int4.)

These changes were not practical before 7.0 because aggregates with
pass-by-reference transition data types leaked memory.  The leak problem
is gone, so we can change to wider accumulator datatypes without fear of
that.  The code will probably run a little slower than before, but that
seems an acceptable price for solving overflow problems.

Comments, objections, better ideas?

            regards, tom lane

pgsql-hackers by date:

Previous
From: Peter Eisentraut
Date:
Subject: Template matching, a different perspective
Next
From: The Hermit Hacker
Date:
Subject: [7.0.2] INDEX' TUPLES != HEAP' ..