Las funciones de agregación en PostgreSQL se definen en términos de valores de estado (state values) y funciones de transición de estado (state transition functions). Es decir, un agregado opera utilizando un valor de estado que se actualiza a medida que se procesa cada fila de entrada sucesiva. Para definir una nueva función de agregación, se selecciona un tipo de datos para el valor de estado, un valor inicial para el estado y una función de transición de estado. La función de transición de estado toma el valor de estado anterior y el/los valor(es) de entrada del agregado para la fila actual, y devuelve un nuevo valor de estado. También se puede especificar una función final (final function), en caso de que el resultado deseado del agregado sea diferente de los datos que deben mantenerse en el valor de estado acumulado. La función final toma el valor de estado final y devuelve lo que se desee como resultado del agregado. En principio, las funciones de transición y final son simplemente funciones ordinarias que también podrían usarse fuera del contexto del agregado. (En la práctica, a menudo es útil por razones de rendimiento crear funciones de transición especializadas que solo pueden funcionar cuando se llaman como parte de un agregado).
Por lo tanto, además de los tipos de datos de argumento y resultado vistos por un usuario del agregado, existe un tipo de datos de valor de estado interno que podría ser diferente de los tipos de argumento y de resultado.
Si definimos un agregado que no utiliza una función final, tendremos un agregado que
calcula una función acumulada de los valores de columna de cada fila.
sum es un ejemplo de este tipo de agregado.
sum comienza en cero y siempre suma el valor de la fila actual
a su total acumulado. Por ejemplo, si queremos hacer que un agregado
sum funcione en un tipo de datos para números complejos,
solo necesitamos la función de adición para ese tipo de datos.
La definición del agregado sería:
CREATE AGGREGATE sum (complex)
(
sfunc = complex_add,
stype = complex,
initcond = '(0,0)'
);
la cual podríamos usar así:
SELECT sum(a) FROM test_complex; sum ----------- (34,53.9)
(Ten en cuenta que nos apoyamos en la sobrecarga de funciones: hay más de un
agregado llamado sum, pero
PostgreSQL puede determinar qué tipo de suma se
aplica a una columna de tipo complex).
La definición anterior de sum devolverá cero
(el valor de estado inicial) si no hay valores de entrada que no sean nulos.
Tal vez deseemos devolver nulo en ese caso en su lugar — el estándar SQL
espera que sum se comporte de esa manera. Podemos hacer esto simplemente
omitiendo la frase initcond, de modo que el valor de estado inicial
sea nulo. Normalmente esto significaría que la función de transición (sfunc)
necesitaría comprobar si el valor de estado de entrada es nulo. Pero para
sum y algunos otros agregados simples como
max y min,
es suficiente insertar el primer valor de entrada no nulo en la variable de estado
y luego comenzar a aplicar la función de transición en el segundo valor de entrada
no nulo. PostgreSQL hará eso automáticamente si el
valor de estado inicial es nulo y la función de transición está marcada como
“strict” (es decir, estricta, que significa que no debe llamarse para entradas nulas).
Otro comportamiento predeterminado para una función de transición “strict” es que el valor de estado anterior se conserva sin cambios cada vez que se encuentra un valor de entrada nulo. Por lo tanto, los valores nulos se ignoran. Si necesitas algún otro comportamiento para las entradas nulas, no declares tu función de transición como estricta; en su lugar, prográmala para verificar si hay entradas nulas y realizar lo que sea necesario.
avg (promedio) es un ejemplo más complejo de un agregado.
Requiere dos piezas de estado acumulado: la suma de las entradas y el conteo
del número de entradas. El resultado final se obtiene dividiendo estas cantidades.
El promedio típicamente se implementa utilizando un arreglo como valor de estado.
Por ejemplo, la implementación integrada de avg(float8) se ve así:
CREATE AGGREGATE avg (float8)
(
sfunc = float8_accum,
stype = float8[],
finalfunc = float8_avg,
initcond = '{0,0,0}'
);
float8_accum requiere un arreglo de tres elementos, no solo
dos elementos, porque acumula la suma de cuadrados así como la suma y el conteo
de las entradas. Esto es para que pueda ser utilizado para otros agregados además
de avg.
Las llamadas a funciones de agregación en SQL permiten las opciones
DISTINCT y ORDER BY que controlan qué filas se envían
a la función de transición del agregado y en qué orden. Estas opciones se
implementan entre bastidores y no son de la incumbencia de las funciones de soporte
del agregado.
Para más detalles, consulta el comando CREATE AGGREGATE.
Las funciones de agregación pueden admitir opcionalmente el modo de agregado móvil
(moving-aggregate mode), que permite una ejecución sustancialmente más rápida de las funciones
de agregación dentro de ventanas con puntos de inicio de marco móviles.
(Consulta la Section 3.5 y la Section 4.2.8
para obtener información sobre el uso de funciones de agregación como funciones de ventana).
La idea básica es que, además de una función de transición “hacia adelante” (forward)
normal, el agregado proporciona una función de transición inversa (inverse
transition function), la cual permite eliminar filas del valor de estado acumulado del agregado
cuando salen del marco de la ventana. Por ejemplo, un agregado sum,
que utiliza la suma como la función de transición hacia adelante, utilizaría la resta como la
función de transición inversa. Sin una función de transición inversa, el mecanismo de funciones
de ventana debe recalcular el agregado desde cero cada vez que el punto de inicio del marco
se mueve, lo que resulta en un tiempo de ejecución proporcional al número de filas de entrada
multiplicado por la longitud promedio del marco. Con una función de transición inversa, el tiempo
de ejecución es únicamente proporcional al número de filas de entrada.
A la función de transición inversa se le pasa el valor de estado actual y el/los valor(es) de entrada del agregado para la fila más antigua incluida en el estado actual. Debe reconstruir cuál habría sido el valor de estado si la fila de entrada dada nunca se hubiera agregado, sino solo las filas que la siguen. Esto a veces requiere que la función de transición hacia adelante mantenga más estado del necesario para el modo de agregación simple. Por lo tanto, el modo de agregado móvil utiliza una implementación completamente separada del modo simple: tiene su propio tipo de datos de estado, su propia función de transición hacia adelante y su propia función final si es necesaria. Estos pueden ser los mismos que el tipo de datos y las funciones del modo simple, si no hay necesidad de un estado adicional.
Como ejemplo, podríamos extender el agregado sum proporcionado anteriormente
para admitir el modo de agregado móvil de la siguiente manera:
CREATE AGGREGATE sum (complex)
(
sfunc = complex_add,
stype = complex,
initcond = '(0,0)',
msfunc = complex_add,
minvfunc = complex_sub,
mstype = complex,
minitcond = '(0,0)'
);
Los parámetros cuyos nombres comienzan con m definen la implementación
del agregado móvil. Excepto por la función de transición inversa minvfunc,
corresponden a los parámetros del agregado simple sin la letra m.
La función de transición hacia adelante para el modo de agregado móvil no tiene permitido devolver
nulo como el nuevo valor de estado. Si la función de transición inversa devuelve nulo, esto se
toma como una indicación de que la función inversa no puede revertir el cálculo de estado para esta
entrada en particular, por lo que el cálculo del agregado se volverá a realizar desde cero para la
posición de inicio del marco actual. Esta convención permite utilizar el modo de agregado móvil en
situaciones en las que existen algunos casos poco frecuentes que son poco prácticos de revertir
del valor de estado acumulado. La función de transición inversa puede “darse por vencida”
(punt) en estos casos y, aun así, salir ganando en rendimiento siempre que funcione para la mayoría
de los casos. Como ejemplo, un agregado que trabaja con números de coma flotante podría optar por darse
por vencido cuando se deba eliminar una entrada NaN (no es un número) del valor
de estado acumulado.
Al escribir funciones de soporte de agregado móvil, es importante asegurarse de que la función de
transición inversa pueda reconstruir exactamente el valor de estado correcto. De lo contrario,
podría haber diferencias visibles para el usuario en los resultados según se utilice o no el modo
de agregado móvil. Un ejemplo de un agregado para el cual agregar una función de transición inversa
parece fácil al principio, pero donde no se puede cumplir con este requisito, es sum
sobre entradas de tipo float4 o float8. Una declaración ingenua de
sum( podría ser:
float8)
CREATE AGGREGATE unsafe_sum (float8)
(
stype = float8,
sfunc = float8pl,
mstype = float8,
msfunc = float8pl,
minvfunc = float8mi
);
Sin embargo, este agregado puede dar resultados muy diferentes de los que daría sin la función de transición inversa. Por ejemplo, considera:
SELECT
unsafe_sum(x) OVER (ORDER BY n ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
FROM (VALUES (1, 1.0e20::float8),
(2, 1.0::float8)) AS v (n,x);
Esta consulta devuelve 0 como su segundo resultado, en lugar de la respuesta
esperada de 1. La causa es la precisión limitada de los valores de coma flotante:
sumar 1 a 1e20 da como resultado 1e20 nuevamente,
por lo que restar 1e20 de eso produce 0 en lugar de 1.
Ten en cuenta que esta es una limitación de la aritmética de coma flotante en general, no una limitación
de PostgreSQL.
Las funciones de agregación pueden utilizar funciones de transición de estado polimórficas o funciones finales polimórficas, de modo que las mismas funciones puedan utilizarse para implementar múltiples agregados. Consulta la Section 36.2.5 para obtener una explicación de las funciones polimórficas. Dando un paso más allá, la función de agregación en sí misma puede ser especificada con tipo(s) de entrada y tipo de estado polimórficos, lo que permite que una sola definición de agregado sirva para múltiples tipos de datos de entrada. Aquí hay un ejemplo de un agregado polimórfico:
CREATE AGGREGATE array_accum (anycompatible)
(
sfunc = array_append,
stype = anycompatiblearray,
initcond = '{}'
);
Aquí, el tipo de estado real para cualquier llamada de agregado dada es el tipo de arreglo que tiene el
tipo de entrada real como elementos. El comportamiento del agregado es concatenar todas las entradas
en un arreglo de ese tipo. (Nota: el agregado integrado array_agg proporciona una
funcionalidad similar, con mejor rendimiento del que tendría esta definición).
A continuación se muestra la salida utilizando dos tipos de datos reales diferentes como argumentos:
SELECT attrelid::regclass, array_accum(attname)
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 'pg_tablespace'::regclass
GROUP BY attrelid;
attrelid | array_accum
---------------+---------------------------------------
pg_tablespace | {spcname,spcowner,spcacl,spcoptions}
(1 row)
SELECT attrelid::regclass, array_accum(atttypid::regtype)
FROM pg_attribute
WHERE attnum > 0 AND attrelid = 'pg_tablespace'::regclass
GROUP BY attrelid;
attrelid | array_accum
---------------+---------------------------
pg_tablespace | {name,oid,aclitem[],text[]}
(1 row)
Normalmente, una función de agregación con un tipo de resultado polimórfico tiene un tipo de estado
polimórfico, como en el ejemplo anterior. Esto es necesario porque de lo contrario la función final
no se puede declarar con sentido: necesitaría tener un tipo de resultado polimórfico pero ningún tipo
de argumento polimórfico, lo cual será rechazado por CREATE FUNCTION argumentando que
el tipo de resultado no se puede deducir de una llamada. Pero a veces es inconveniente utilizar un
tipo de estado polimórfico. El caso más común es cuando las funciones de soporte del agregado se van a
escribir en C y el tipo de estado debe declararse como internal porque no existe un equivalente
a nivel SQL para él. Para abordar este caso, es posible declarar la función final como receptora de argumentos
“dummy” (ficticios) adicionales que coincidan con los argumentos de entrada del agregado.
Dichos argumentos ficticios siempre se pasan como valores nulos, ya que no hay ningún valor específico
disponible cuando se llama a la función final. Su único uso es permitir conectar el tipo de resultado de
una función final polimórfica al tipo de entrada del agregado. Por ejemplo, la definición del agregado
integrado array_agg es equivalente a:
CREATE FUNCTION array_agg_transfn(internal, anynonarray)
RETURNS internal ...;
CREATE FUNCTION array_agg_finalfn(internal, anynonarray)
RETURNS anyarray ...;
CREATE AGGREGATE array_agg (anynonarray)
(
sfunc = array_agg_transfn,
stype = internal,
finalfunc = array_agg_finalfn,
finalfunc_extra
);
Aquí, la opción finalfunc_extra especifica que la función final recibe, además del
valor de estado, argumento(s) ficticios adicionales que corresponden a los argumentos de entrada del agregado.
El argumento anynonarray adicional permite que la declaración de
array_agg_finalfn sea válida.
Se puede hacer que una función de agregación acepte un número variable de argumentos declarando su último
argumento como un arreglo VARIADIC, de la misma manera que para las funciones ordinarias;
consulta la Section 36.5.6. Las funciones de transición del agregado deben
tener el mismo tipo de arreglo como su último argumento. Las funciones de transición típicamente también
estarían marcadas como VARIADIC, pero esto no es estrictamente obligatorio.
Los agregados variádicos se usan incorrectamente con facilidad en relación con la opción
ORDER BY (consulta la Section 4.2.7), dado que el analizador no puede
saber si se ha proporcionado el número incorrecto de argumentos reales en tal combinación. Ten en cuenta que
todo lo que se encuentra a la derecha de ORDER BY es una clave de ordenamiento, no un
argumento para el agregado. Por ejemplo, en:
SELECT myaggregate(a ORDER BY a, b, c) FROM ...
el analizador verá esto como un único argumento de función de agregación y tres claves de ordenamiento. Sin embargo, el usuario podría haber querido escribir:
SELECT myaggregate(a, b, c ORDER BY a) FROM ...
Si myaggregate es variádico, ambas llamadas podrían ser perfectamente válidas.
Por la misma razón, es prudente pensarlo dos veces antes de crear funciones de agregación con los mismos nombres y diferentes números de argumentos regulares.
Los agregados que hemos estado describiendo hasta ahora son agregados “normales”.
PostgreSQL también admite agregados de conjunto ordenado
(ordered-set aggregates), los cuales difieren de los agregados normales en dos aspectos clave. Primero,
además de los argumentos agregados ordinarios que se evalúan una vez por fila de entrada, un agregado de
conjunto ordenado puede tener argumentos “directos” que se evalúan solo una vez por operación
de agregación. Segundo, la sintaxis para los argumentos agregados ordinarios especifica explícitamente un
ordenamiento para ellos. Un agregado de conjunto ordenado se utiliza normalmente para implementar un cálculo
que depende de un orden de filas específico, por ejemplo, el rango o el percentil, de modo que el ordenamiento
es un aspecto obligatorio de cualquier llamada. Por ejemplo, la definición integrada de
percentile_disc es equivalente a:
CREATE FUNCTION ordered_set_transition(internal, anyelement)
RETURNS internal ...;
CREATE FUNCTION percentile_disc_final(internal, float8, anyelement)
RETURNS anyelement ...;
CREATE AGGREGATE percentile_disc (float8 ORDER BY anyelement)
(
sfunc = ordered_set_transition,
stype = internal,
finalfunc = percentile_disc_final,
finalfunc_extra
);
Este agregado toma un argumento directo float8 (la fracción del percentil) y una entrada
agregada que puede ser de cualquier tipo de datos ordenable. Podría usarse para obtener una mediana de
ingresos familiares como esto:
SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY income) FROM households;
percentile_disc
-----------------
50489
Aquí, 0.5 es un argumento directo; no tendría sentido que la fracción de percentil
fuera un valor que varíe entre filas.
A diferencia del caso de los agregados normales, el ordenamiento de las filas de entrada para un agregado de
conjunto ordenado no se realiza entre bastidores, sino que es responsabilidad de las
funciones de soporte del agregado. El enfoque de implementación típico consiste en mantener una referencia a
un objeto “tuplesort” en el valor de estado del agregado, alimentar las filas entrantes a ese
objeto, y luego completar el ordenamiento y leer los datos en la función final. Este diseño permite que la
función final realice operaciones especiales como inyectar filas “hipotéticas” adicionales en
los datos a ordenar. Mientras que los agregados normales a menudo se pueden implementar con funciones de
soporte escritas en PL/pgSQL u otro lenguaje PL, los agregados de conjunto ordenado
generally tiene que escribirse en C, ya que sus valores de estado no son definibles como ningún tipo de
datos SQL. (En el ejemplo anterior, observa que el valor de estado se declara como tipo internal
— esto es típico). Además, debido a que la función final realiza el ordenamiento, no es posible continuar
agregando filas de entrada ejecutando la función de transición nuevamente más tarde. Esto significa que la
función final no es READ_ONLY; debe declararse en
CREATE AGGREGATE como READ_WRITE,
o como SHAREABLE si es posible que llamadas adicionales a la función final hagan uso del
estado ya ordenado.
La función de transición de estado para un agregado de conjunto ordenado recibe el valor de estado actual
más los valores de entrada agregados para cada fila, y devuelve el valor de estado actualizado. Esta es la
misma definición que para los agregados normales, pero ten en cuenta que los argumentos directos (si los hay)
no se proporcionan. La función final recibe el último valor de estado, los valores de los argumentos directos
si los hay, y (si se especifica finalfunc_extra) valores nulos correspondientes a las
entradas agregadas. Al igual que con los agregados normales, finalfunc_extra solo es útil
si el agregado es polimórfico; entonces se necesitan los argumentos ficticios adicionales para conectar el
tipo de resultado de la función final con el tipo de entrada del agregado.
Actualmente, los agregados de conjunto ordenado no se pueden usar como funciones de ventana, por lo que no es necesario que admitan el modo de agregado móvil.
Opcionalmente, una función de agregación puede admitir la agregación parcial (partial aggregation). La idea de la agregación parcial es ejecutar la función de transición de estado del agregado sobre diferentes subconjuntos de los datos de entrada de forma independiente, y luego combinar los valores de estado resultantes de esos subconjuntos para producir el mismo valor de estado que habría resultado de escanear todas las entradas en una sola operación. Este modo se puede utilizar para la agregación paralela haciendo que diferentes procesos de trabajador (worker) escaneen diferentes porciones de una tabla. Cada trabajador produce a un valor de estado parcial y, al final, esos valores de estado se combinan para producir un valor de estado final. (En el futuro, este modo también podría utilizarse para fines como la combinación de agregaciones en tablas locales y remotas; pero eso aún no está implementado).
Para admitir la agregación parcial, la definición del agregado debe proporcionar una función de combinación (combine function), la cual toma dos valores del tipo de estado del agregado (que representan los resultados de la agregación sobre dos subconjuntos de las filas de entrada) y produce un nuevo valor del tipo de estado, que representa cuál habría sido el estado después de agregar sobre la combinación de esos conjuntos de filas. No se especifica cuál habría sido el orden relativo de las filas de entrada de los dos conjuntos. Esto significa que normalmente es imposible definir una función de combinación útil para agregados que son sensibles al orden de las filas de entrada.
Como ejemplos simples, se puede hacer que los agregados MAX y MIN admitan
la agregación parcial especificando que la función de combinación sea la misma función de comparación del
mayor de dos o menor de dos que se utiliza como su función de transición. Los agregados SUM
solo necesitan una función de adición como función de combinación. (Nuevamente, esta es la misma que su función de
transición, a menos que el tipo del valor de estado sea más amplio que el tipo de datos de entrada).
La función de combinación se trata de manera muy similar a una función de transición que resulta tomar un valor
del tipo de estado, no del tipo de entrada subyacente, como su segundo argumento. En particular, las reglas para
manejar valores nulos y funciones estrictas son similares. Además, si la definición del agregado especifica un
valor initcond no nulo, ten en cuenta que se utilizará no solo como el estado inicial para
cada ejecución de agregación parcial, sino también como el estado inicial para la función de combinación, la cual
se llamará para combinar cada resultado parcial en ese estado.
Si el tipo de estado del agregado se declara como internal, es responsabilidad de la función de
combinación que su resultado se asigne en el contexto de memoria correcto para los valores de estado del agregado.
Esto significa en particular que cuando la primera entrada es NULL, no es válido simplemente
devolver la segunda entrada, ya que ese valor estará en el contexto incorrecto y no tendrá una duración de vida
suficiente.
Cuando el tipo de estado del agregado se declara como internal, generalmente también es apropiado que
la definición del agregado preocione una función de serialización (serialization function)
y una función de deserialización (deserialization function), las cuales permiten que dicho
valor de estado se copie de un proceso a otro. Sin estas funciones, no se puede realizar la agregación paralela,
y las futuras aplicaciones como la agregación local/remota probablemente tampoco funcionarán.
Una función de serialización debe tomar un único argumento de tipo internal y devolver un resultado
de tipo bytea, el cual representa el valor de estado empaquetado en un comando plano de bytes (flat blob).
Por el contrario, una función de deserialización invierte esa conversión. Debe tomar dos argumentos de tipos
bytea e internal, y devolver un resultado de tipo internal. (El segundo
argumento no se utiliza y siempre es cero, pero se requiere por razones de seguridad de tipos). El resultado de la
función de deserialización simplemente debe asignarse en el contexto de memoria actual, ya que a diferencia de
resultado de la función de combinación, no es de larga duración.
También vale la pena señalar que para que un agregado se ejecute en paralelo, el agregado en sí debe estar marcado como
PARALLEL SAFE. No se consultan las marcas de seguridad en paralelo de sus funciones de soporte.
Una función escrita en C puede detectar que está siendo llamada como una función de soporte de agregado
llamando a AggCheckCallContext, por ejemplo:
if (AggCheckCallContext(fcinfo, NULL))
Una razón para verificar esto es que cuando es true, la primera entrada debe ser un valor de estado temporal y, por
lo tanto, puede modificarse de manera segura en el lugar (in-place) en lugar de asignar una nueva copia.
Consulta int8inc() para ver un ejemplo. (Aunque a las funciones de transición de agregado
siempre se les permite modificar el valor de transición en el lugar, generalmente se desaconseja que las funciones
finales de agregado lo hagan; si lo hacen, el comportamiento debe declararse al crear el agregado. Consulta la
CREATE AGGREGATE para más detalles).
El segundo argumento de AggCheckCallContext puede usarse para recuperar el contexto de memoria en
el que se mantienen los valores de estado del agregado. Esto es útil para las funciones de transición que desean
utilizar objetos “expandidos” (expanded objects; consulta la Section 36.13.1) como sus
valores de estado. En la primera llamada, la función de transición debe devolver un objeto expandido cuyo contexto
de memoria sea hijo del contexto del estado del agregado, y luego continuar devolviendo el mismo objeto expandido en
las llamadas subsiguientes. Consulta array_append() para ver un ejemplo.
(array_append() no es la función de transición de ningún agregado integrado, pero está escrita
para comportarse de manera eficiente cuando se usa como función de transición de un agregado personalizado).
Otra rutina de soporte disponible para las funciones de agregación escritas en C es AggGetAggref,
la cual devuelve el nodo de análisis Aggref que define la llamada al agregado. Esto es principalmente
útil para los agregados de conjunto ordenado, los cuales pueden inspeccionar la subestructura del nodo
Aggref para averiguar qué ordenamiento de clasificación se supone que deben implementar.
Se pueden encontrar ejemplos en orderedsetaggs.c en el código fuente de
PostgreSQL.