36.12. Agregações definidas pelo usuário #

36.12.1. Modo de agregação móvel
36.12.2. Agregações polimórficas e variádicas
36.12.3. Agregações de conjunto ordenado
36.12.4. Agregação parcial
36.12.5. Funções de suporte para agregação

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}'
);

Nota

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.

36.12.1. Modo de agregação móvel #

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(float8) poderia ser

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.

36.12.2. Agregações polimórficas e variádicas #

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.

Nota

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.

36.12.3. Agregações de conjunto ordenado #

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.

36.12.4. Agregação parcial #

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.

36.12.5. Funções de suporte para agregação #

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.