No PostgreSQL as funções de agregação são definidas em termos de valores de estado e funções de transição de estado. Ou seja, uma agregação opera usando um valor de estado, atualizado à medida que cada linha de entrada sucessiva é processada. Para definir uma nova função de agregação, seleciona-se um tipo de dados para o valor de estado, um valor inicial para o estado, e uma função de transição de estado. A função de transição de estado pega o valor de estado anterior, e o(s) valor(es) de entrada da agregação para a linha corrente, e retorna um novo valor de estado. Também pode ser especificada uma função final, caso o resultado desejado para a agregação difira dos dados que precisam ser mantidos no valor de estado corrente. A função final pega o valor de estado final e retorna o que for desejado como resultado da agregação. A princípio, as funções de transição e final são apenas funções comuns, que também podem ser usadas fora do contexto da agregação. (Na prática, é geralmente útil, por motivos de desempenho, criar funções de transição especializadas que só funcionam quando chamadas como parte de uma agregação.)
Portanto, além dos tipos de dados do argumento e do resultado vistos pelo usuário da agregação, há um tipo de dados do valor de estado interno que pode ser diferente dos tipos de dados do argumento e do resultado.
Se for definida uma agregação que não usa uma função final, teremos
uma agregação que calcula uma função sobre o valor da coluna de
cada linha.
A função sum é um exemplo desse tipo de agregação.
sum começa em zero, e sempre adiciona o valor
da linha corrente ao seu total corrente.
Por exemplo, se for desejado fazer uma agregação sum
para trabalhar em tipo de dados para números complexos, é necessária
apenas a função de adição para este tipo de dados.
A definição da agregação seria
CREATE AGGREGATE sum (complex)
(
sfunc = complex_add,
stype = complex,
initcond = '(0,0)'
);
podendo ser usada assim:
SELECT sum(a) FROM test_complex;
sum ----------- (34,53.9)
(Note que estamos contando com sobrecarga de função:
existe mais de uma agregação chamada sum,
mas o PostgreSQL pode descobrir qual tipo
de soma se aplica a uma coluna do tipo de dados complex.)
A definição acima de sum retornará zero (o valor
de estado inicial), caso não haja valores de entrada não nulos.
Talvez fosse desejado retornar nulo neste caso — o padrão
SQL espera que sum
se comporte dessa maneira.
Pode-se fazer isto simplesmente omitindo initcond,
para que o valor de estado inicial seja nulo.
Normalmente, isto significaria que o sfunc
precisaria verificar se há uma entrada de valor de estado nulo.
Mas para sum e algumas outras agregações simples,
como max e min, é suficiente
inserir o primeiro valor de entrada não nulo na variável de estado,
e então começar a aplicar a função de transição no segundo valor de
entrada não nulo.
O PostgreSQL fará isto automaticamente
se o valor de estado inicial for nulo, e a função de transição
estiver marcada como “STRICT”
(ou seja, não deve ser chamada para entradas nulas).
Outro comportamento padrão para uma função de transição “estrita”, é que o valor de estado anterior é mantido inalterado sempre que é encontrado um valor de entrada nulo. Assim, os valores nulos são ignorados. Se houver necessidade de algum outro comportamento para entradas nulas, não se deve declarar a função de transição como estrita; em vez disso, deve ser codificada para testar entradas nulas e fazer o que for necessário.
A função avg (média) é um exemplo mais complexo
de agregação.
Ela requer dois estados correntes:
a soma das entradas e a contagem do número de entradas.
O resultado final é obtido dividindo estas quantidades.
A média é normalmente implementada usando uma matriz como
valor de estado.
Por exemplo, a implementação interna de avg(float8)
se parece com:
CREATE AGGREGATE avg (float8)
(
sfunc = float8_accum,
stype = float8[],
finalfunc = float8_avg,
initcond = '{0,0,0}'
);
A função float8_accum requer uma matriz de três
elementos, e não apenas dois elementos, porque acumula a soma dos
quadrados, bem como a soma e a contagem das entradas.
Isto se dá para poder ser usada em algumas outras agregações,
bem como para avg.
As chamadas de função de agregação no SQL permitem
as opções DISTINCT e ORDER BY,
que controlam quais linhas são enviadas para a função de transição
de agregação, e em que ordem.
Estas opções são implementadas às escondidas, não sendo de
responsabilidade das funções de suporte da agregação.
Para obter mais detalhes, veja o comando CREATE AGGREGATE.
As funções de agregação podem, opcionalmente, oferecer suporte ao
modo de agregação móvel, que permite uma
execução substancialmente mais rápida para funções de agregação em
janelas com pontos iniciais de quadros móveis.
(Veja Funções de janela e
Chamadas de função de janela para obter informações
sobre o uso de funções de agregação como funções de janela.)
A ideia básica é que, além da função de transição
“para frente” normal, a agregação forneça uma
função de transição inversa, que permita que
as linhas sejam removidas do valor de estado da execução da agregação
quando elas saírem do quadro da janela.
Por exemplo, a agregação sum, que usa adição
como função de transição para frente, usaria subtração como função
de transição inversa.
Sem uma função de transição inversa, o mecanismo de função de janela
deve recalcular a agregação a partir do zero sempre que o ponto inicial
do quadro se mover, produzindo um tempo de execução proporcional ao
número de linhas de entrada vezes o comprimento médio do quadro.
Com uma função de transição inversa, o tempo de execução é apenas
proporcional ao número de linhas da entrada.
A função de transição inversa recebe o valor de estado corrente e o(s) valor(es) de entrada da agregação para a primeira linha incluída no estado corrente, devendo reconstruir qual seria o valor de estado se a linha de entrada fornecida nunca tivesse sido agregada, mas apenas as linhas que a seguem. Às vezes, isto requer que a função de transição para frente mantenha mais estados do que é necessário para o modo de agregação simples. Portanto, o modo de agregação móvel usa uma implementação completamente separada do modo simples: possui seu próprio tipo de dados de estado, sua própria função de transição para frente, e sua própria função final, caso seja necessário. Podem ser os mesmos tipos de dados e funções do modo simples, quando não há necessidade de um estado extra.
Como exemplo, podemos estender a agregação sum
mostrada acima para suportar o modo de agregação móvel dessa forma:
CREATE AGGREGATE sum (complex)
(
sfunc = complex_add,
stype = complex,
initcond = '(0,0)',
msfunc = complex_add,
minvfunc = complex_sub,
mstype = complex,
minitcond = '(0,0)'
);
Os parâmetros cujos nomes começam com m definem
a implementação da agregação móvel.
Exceto pela função de transição inversa minvfunc,
eles correspondem aos parâmetros da agregação simples sem o
m.
A função de transição para frente para o modo de agregação móvel não
pode retornar nulo como o novo valor de estado.
Se a função de transição inversa retornar nulo, isto é considerado
uma indicação de que a função inversa não pode reverter o cálculo do
estado para esta entrada específica, portanto, o cálculo da
agregação será refeito do zero para a posição inicial do quadro corrente.
Esta convenção permite que o modo de agregação móvel seja usado em
situações onde existem alguns casos pouco frequentes em que é
impraticável reverter o valor de estado corrente.
A função de transição inversa pode “não funcionar” nesses
casos, e ainda assim ser vantajosa, desde que funcione na maioria dos
casos.
Por exemplo, uma agregação que trabalha com números de ponto flutuante
pode optar por não funcionar quando uma entrada NaN
(não é um número) precisar ser removida do valor de estado corrente.
Ao escrever funções de suporte de agregação móvel, é importante ter
certeza de que a função de transição inversa pode reconstruir
exatamente o valor de estado correto.
Caso contrário, pode haver diferenças visíveis para o usuário nos
resultados, dependendo se o modo de agregação móvel for usado.
Um exemplo de agregação para a qual adicionar uma função de transição
inversa parece fácil inicialmente, mas onde este requisito não pode
ser atendido é sum sobre entradas
float4 ou float8.
Uma declaração ingênua de sum(
poderia ser
float8)
CREATE AGGREGATE unsafe_sum (float8)
(
stype = float8,
sfunc = float8pl,
mstype = float8,
msfunc = float8pl,
minvfunc = float8mi
);
Esta agregação, no entanto, pode fornecer resultados inteiramente diferentes do que forneceria sem a função de transição inversa. Por exemplo, considere
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 retorna 0 como seu segundo resultado,
em vez da resposta esperada que é 1.
A causa disso é a precisão limitada dos valores de ponto flutuante:
somar 1 a 1e20 resulta em
1e20 novamente, e assim subtrair
1e20 disso resulta em 0,
e não em 1.
Note que esta é uma limitação da aritmética de ponto flutuante em
geral, e não uma limitação do PostgreSQL.
As funções de agregação podem usar funções de transição de estado ou funções finais polimórficas, de modo que as mesmas funções possam ser usadas para implementar várias agregações. Veja Tipos de dados polimórficos para obter explicação sobre funções polimórficas. Indo um passo adiante, a própria função de agregação pode ser especificada com tipos de dados de entrada e de estado polimórficos, permitindo que uma única definição de agregação sirva para vários tipos de dados de entrada. A seguir está um exemplo de agregação polimórfica:
CREATE AGGREGATE array_accum (anycompatible)
(
sfunc = array_append,
stype = anycompatiblearray,
initcond = '{}'
);
Nesta agregação, o tipo de dados de estado real para qualquer chamada
de agregação é o tipo de dados matriz que possui o tipo de dados de
entrada real como elementos.
O comportamento da agregação é concatenar todas as entradas em uma
matriz desse tipo de dados
[117].
(Observação: a função integrada array_agg fornece
uma funcionalidade semelhante, com um desempenho melhor do que esta
definição teria.)
A seguir está mostrada a saída usando dois tipos de dados reais 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 linha)
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 linha)
Normalmente, uma função de agregação com tipo de dados de resultado
polimórfico tem um tipo de dados de estado polimórfico, como no
exemplo acima.
Isto é necessário porque, caso contrário, a função final não pode
ser declarada de uma forma que faça sentido: ela precisaria ter um
tipo de dados de resultado polimórfico, mas nenhum argumento polimórfico,
o que o comando CREATE FUNCTION não deixa, baseado
no fato de que o tipo de dados do resultado não pode ser inferido a
partir da chamada da função.
Mas às vezes é inconveniente usar um tipo de dados de estado polimórfico.
O caso mais comum é quando as funções de suporte da agregação devem
ser escritas em C, e o tipo de dados de estado deve
ser declarado como internal, porque não há equivalente ao
nível SQL para ele.
Para resolver este caso, é possível declarar a função final como
tendo argumentos “fictícios” extras, correspondendo aos
argumentos de entrada da agregação.
Estes argumentos fictícios são sempre passados como valores nulos,
porque nenhum valor específico está disponível quando a função final
é chamada.
Seu único uso é permitir que o tipo de dados de resultado de uma função
final polimórfica seja conectado ao(s) tipo(s) de dados de entrada
da agregação.
Por exemplo, a definição da agregação integrada
array_agg equivale 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
);
Aqui, a opção finalfunc_extra especifica que a
função final recebe, além do valor do estado, argumento(s) fictício(s)
extra(s) correspondente(s) ao(s) argumento(s) de entrada da agregação.
O argumento extra anynonarray permite que a declaração
de array_agg_finalfn seja válida.
Uma função de agregação pode aceitar um número variável de argumentos
declarando seu último argumento como uma matriz VARIADIC,
da mesma forma que para funções regulares;
veja Funções SQL com número variável de argumentos.
As funções de transição da agregação devem ter o mesmo tipo de dados
matriz de seu último argumento.
A(s) função(ões) de transição normalmente também seriam marcadas como
VARIADIC, mas isto não é estritamente necessário.
As agregações variádicas são facilmente mal utilizadas quando estão
juntas com a opção ORDER BY
(veja Expressões de agregação), uma vez que o
analisador não pode dizer se foi fornecido um número errado de
argumentos reais nesta combinação.
Lembre-se de que tudo à direita de ORDER BY
é chave de ordenação, e não argumento para a agregação.
Por exemplo, em
SELECT myaggregate(a ORDER BY a, b, c) FROM ...
o analisador verá isto como um único argumento de função de agregação e três chaves de ordenação. No entanto, o usuário pode ter pretendido
SELECT myaggregate(a, b, c ORDER BY a) FROM ...
Se myaggregate for variádica, as duas chamadas
podem ser perfeitamente válidas.
Pela mesma razão, é bom pensar duas vezes antes de criar funções de agregação com os mesmos nomes e diferentes números de argumentos regulares.
As agregações descritas até agora são agregações “normais”.
O PostgreSQL também oferece suporte a
agregações de conjunto ordenado, que diferem
das agregações normais de duas maneiras principais.
Primeiro, além dos argumentos usuais da agregação, avaliados
uma vez por linha de entrada, uma agregação de conjunto ordenado pode
ter argumentos “diretos”, avaliados apenas uma
vez por operação de agregação.
Depois, a sintaxe para os argumentos usuais da agregação especifica
uma ordem de classificação para eles explicitamente.
Uma agregação de conjunto ordenado é geralmente usada para implementar
um cálculo que depende de uma ordem de linha específica, por exemplo,
ordenação ou percentil, de modo que a ordem de classificação seja
um aspecto obrigatório de qualquer chamada.
Por exemplo, a definição interna de percentile_disc
equivale 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
);
Esta agregação usa um argumento direto float8
(a fração de percentil) e uma entrada de agregação que pode ser de
qualquer tipo de dados ordenável.
Poderia ser usada para obter a mediana da renda familiar dessa forma:
SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY income) FROM households;
percentile_disc
-----------------
50489
Aqui, 0.5 é um argumento direto; não faz sentido
a fração de percentil ser um valor variando entre as linhas.
Ao contrário do caso de agregações normais, a ordenação de linhas
de entrada para uma agregação de conjunto ordenado não
é feita nos bastidores, sendo responsabilidade das funções de suporte
da agregação.
A abordagem de implementação típica é manter a referência a um objeto
“tuplesort” no valor de estado da agregação, alimentar
as linhas recebidas neste objeto e, em seguida, concluir a
ordenação e ler os dados na função final.
Esta formulação permite que a função final execute operações especiais,
como injetar linhas “hipotéticas” adicionais nos dados
a serem classificados.
Enquanto as agregações normais podem ser frequentemente implementadas
com funções de suporte escritas em PL/pgSQL
ou outra linguagem procedural, agregações de conjunto ordenado geralmente
precisam ser escritas em C, já que seus valores de
estado não são definíveis como qualquer tipo de dados
SQL.
(No exemplo acima, deve-se observar que o valor de estado é declarado
como do tipo de dados internal — isto é típico.)
Além disso, como a função final executa a ordenação, não é
possível continuar adicionando linhas de entrada executando a
função de transição novamente mais tarde.
Isto significa que a função final não é READ_ONLY,
devendo ser declarada no comando
CREATE AGGREGATE
como READ_WRITE, ou como SHAREABLE
se for possível que chamadas da função final adicionais usem o
estado já classificado.
A função de transição de estado para uma agregação de conjunto
ordenado recebe o valor de estado corrente, mais os valores de entrada
da agregação para cada linha, e retorna o valor de estado atualizado.
Esta é a mesma definição para agregações normais, mas observe que os
argumentos diretos (se houver) não são fornecidos.
A função final recebe o valor do último estado, os valores dos
argumentos diretos, se houver, e
(se finalfunc_extra for especificado)
valores nulos correspondentes à(s) entrada(s) da agregação.
Assim como nas agregações normais, na verdade,
finalfunc_extra só é útil se a agregação for
polimórfica; então o(s) argumento(s) fictício(s) extra(s) são
necessários para conectar o tipo de dados do resultado da função
final ao(s) tipo(s) de dados de entrada da agregação.
No momento, as agregações de conjuntos ordenados não podem ser usadas como funções de janela, portanto, não há necessidade de suportarem o modo de agregação móvel.
Opcionalmente, uma função de agregação pode dar suporte a agregação parcial. A ideia da agregação parcial é executar a função de transição de estado da agregação em diferentes subconjuntos dos dados da entrada independentemente e, em seguida, combinar os valores de estado resultantes desses subconjuntos para produzir o mesmo valor de estado que resultaria da varredura de toda a entrada em uma única operação. Este modo pode ser usado para agregação paralela, fazendo com que diferentes processos de trabalho verifiquem diferentes partes de uma tabela. Cada processo trabalhador (background worker) produz um valor de estado parcial e, no final, estes valores de estado são combinados para produzir um valor de estado final. (No futuro, este modo também poderá ser usado para fins como combinar agregações em tabelas locais e remotas; mas isto ainda não foi implementado.)
Para dar suporte à agregação parcial, a definição da agregação deve fornecer uma função de combinação, recebendo dois valores do tipo de dados de estado da agregação (representando os resultados da agregação em dois subconjuntos das linhas de entrada), e produzindo um novo valor do tipo de dados de estado, representando o que o estado teria após agregar a combinação desses conjuntos de linhas. Não está especificado qual teria sido a ordem relativa das linhas de entrada dos dois conjuntos. Isto significa que é geralmente impossível definir uma função de combinação útil para agregações que são sensíveis à ordem da linha de entrada.
Como exemplos simples, as agregações MAX e
MIN podem ser construídas para dar suporte à
agregação parcial especificando a função de combinação como a mesma
função de comparação maior-de-dois ou menor-de-dois usada como
sua função de transição.
A agregação sum só precisa de uma função de adição
como função de combinação.
(Novamente, isto é o mesmo que sua função de transição, a menos que o
valor de estado seja mais amplo que o tipo de dados da entrada.)
A função de combinação é tratada como uma função de transição que
assume o valor do tipo de dados de estado, e não do tipo de dados da
entrada subjacente, como seu segundo argumento.
Em particular, as regras para lidar com valores nulos e funções
estritas são semelhantes.
Além disso, se a definição de agregação especificar um
initcond não nulo, lembre-se de que isto será
usado não apenas como o estado inicial para cada execução de agregação
parcial, mas também como o estado inicial para a função de combinação,
que será chamada para combinar cada resultado parcial neste estado.
Se o tipo de dados de estado da agregação for declarado como
internal, é responsabilidade da função de combinação
que seu resultado seja alocado no contexto de memória correto para
valores de estado da agregação.
Isto significa, em particular, que quando a primeira entrada for
NULL, é inválido apenas retornar a segunda
entrada, porque este valor estará no contexto errado, e não terá
tempo de vida suficiente.
Quando o tipo de dados de estado da agregação é declarado como
internal, geralmente também é apropriado para a definição
da agregação fornecer uma função de serialização
e uma função de desserialização,
que permitem que este valor de estado seja copiado de um processo
para outro.
Sem estas funções, a agregação paralela não pode ser realizada, e
aplicações futuras, como agregação local/remota, provavelmente
também não funcionarão.
Uma função de serialização deve receber um único argumento do tipo de dados
internal, e retornar um resultado do tipo de dados
bytea, representando o valor de estado armazenado em um simples
BLOB de bytes.
Inversamente, uma função de desserialização reverte esta conversão.
Deve receber dois argumentos dos tipos de dados bytea e
internal, e retornar um resultado do tipo de dados
internal.
(O segundo argumento não é usado sendo sempre zero, mas é necessário
por motivos de segurança de tipo de dados.)
O resultado da função de desserialização deve ser simplesmente alocado
no contexto de memória corrente, porque, ao contrário do resultado da
função de combinação, ele não dura muito tempo.
Também vale a pena notar que, para uma agregação ser executada
em paralelo, a própria agregação deve ser marcada como
PARALLEL SAFE.
As marcações de segurança de paralelismo nas funções de suporte não
são consultadas.
As funções escritas em C podem detectar que estão
sendo chamadas como função de suporte de agregação chamando a função
AggCheckCallContext. Por exemplo:
if (AggCheckCallContext(fcinfo, NULL))
Um motivo para verificar isto é que, quando for verdade, a primeira
entrada deve ser um valor de estado temporário, portanto, pode ser
modificado com segurança no próprio local, em vez de alocar uma nova cópia.
Veja um exemplo na função int8inc().
(Embora as funções de transição de agregação sempre tenham permissão
para modificar o valor de transição no local, as funções finais de
agregação são geralmente desencorajadas de fazê-lo; se o fizerem,
o comportamento deve ser declarado ao criar a agregação.
Veja CREATE AGGREGATE para obter mais detalhes.)
O segundo argumento da função AggCheckCallContext
pode ser usado para recuperar o contexto de memória onde os valores
de estado da agregação estão sendo mantidos.
Isto é útil para funções de transição que desejam usar objetos
“expandidos” (veja Considerações sobre TOAST)
como seus valores de estado.
Na primeira chamada, a função de transição deve retornar um objeto
expandido cujo contexto de memória é filho do contexto de estado
da agregação e, em seguida, continuar retornando o mesmo objeto
expandido nas chamadas subsequentes.
Veja um exemplo na função array_append().
(a função array_append() não é a função de
transição de nenhuma agregação integrada, mas foi escrita para se
comportar de forma eficiente quando usada como função de transição
de uma agregação personalizada.)
Outra rotina de suporte disponível para funções de agregação escritas
em C é AggGetAggref,
que retorna o nó de análise Aggref que define a
chamada da agregação.
Ela é útil principalmente para agregações de conjuntos ordenados,
que podem inspecionar a subestrutura do nó Aggref
para descobrir qual ordem de classificação devem implementar.
Podem ser encontrados exemplos no arquivo
orderedsetaggs.c no código-fonte do
PostgreSQL.