Como obtenho o agregado de uma função de janela no Postgres?

11

Eu tenho uma tabela que contém duas colunas de permutações / combinações de matrizes inteiras e uma terceira coluna que contém um valor, assim:

CREATE TABLE foo
(
  perm integer[] NOT NULL,
  combo integer[] NOT NULL,
  value numeric NOT NULL DEFAULT 0
);
INSERT INTO foo
VALUES
( '{3,1,2}', '{1,2,3}', '1.1400' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0.9280' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,2,1}', '{1,2,3}', '0' ),
( '{3,2,1}', '{1,2,3}', '0.8000' )

Quero descobrir a média e o desvio padrão para cada permutação, bem como para cada combinação. Eu posso fazer isso com esta consulta:

SELECT
  f1.perm,
  f2.combo,
  f1.perm_average_value,
  f2.combo_average_value,
  f1.perm_stddev,
  f2.combo_stddev,
  f1.perm_count,
  f2.combo_count
FROM
(
  SELECT
    perm,
    combo,
    avg( value ) AS perm_average_value,
    stddev_pop( value ) AS perm_stddev,
    count( * ) AS perm_count
  FROM foo
  GROUP BY perm, combo
) AS f1
JOIN
(
  SELECT
    combo,
    avg( value ) AS combo_average_value,
    stddev_pop( value ) AS combo_stddev,
    count( * ) AS combo_count
  FROM foo
  GROUP BY combo
) AS f2 ON ( f1.combo = f2.combo );

No entanto, essa consulta pode ficar muito lenta quando tenho muitos dados, porque a tabela "foo" (que na realidade consiste em 14 partições cada uma com aproximadamente 4 milhões de linhas) precisa ser examinada duas vezes.

Recentemente, eu aprendi que o Postgres suporta "Funções de Janela", que é basicamente como um GROUP BY para uma coluna específica. Modifiquei minha consulta para usá-las da seguinte maneira:

SELECT
  perm,
  combo,
  avg( value ) as perm_average_value,
  avg( avg( value ) ) over w_combo AS combo_average_value,
  stddev_pop( value ) as perm_stddev,
  stddev_pop( avg( value ) ) over w_combo as combo_stddev,
  count( * ) as perm_count,
  sum( count( * ) ) over w_combo AS combo_count
FROM foo
GROUP BY perm, combo
WINDOW w_combo AS ( PARTITION BY combo );

Embora isso funcione para a coluna "combo_count", as colunas "combo_average_value" e "combo_stddev" não são mais precisas. Parece que a média está sendo calculada para cada permutação e, em seguida, calculada a média uma segunda vez para cada combinação, o que está incorreto.

Como posso consertar isso? As funções da janela podem ser usadas como uma otimização aqui?

Scott Small
fonte
Assumindo a versão atual do Postgres 9.2? As funções da janela vieram com 8.4.
Erwin Brandstetter
Desculpe, esqueci de especificar. Sim, eu estou usando o Postgres 9.2.4 mais recente.
21713 Scott Scott pequeno

Respostas:

9

Você pode ter funções de janela no resultado de funções agregadas em um único nível de consulta.

Tudo isso funcionaria bem depois de algumas modificações - exceto que falha no desvio padrão do princípio matemático . Os cálculos envolvidos não são lineares; portanto, você não pode simplesmente combinar desvios padrão de subpopulações.

SELECT perm
      ,combo
      ,avg(value)                 AS perm_average_value
      ,sum(avg(value) * count(*)) OVER w_combo /
       sum(count(*)) OVER w_combo AS combo_average_value
      ,stddev_pop(value)          AS perm_stddev
      ,0                          AS combo_stddev  -- doesn't work!
      ,count(*)                   AS perm_count
      ,sum(count(*)) OVER w_combo AS combo_count
FROM   foo
GROUP  BY perm, combo
WINDOW w_combo  AS (PARTITION BY combo);

Para combo_average_valuevocê precisaria dessa expressão

sum(avg(value) * count(*)) OVER w_combo / sum(count(*)) OVER w_combo

Como você precisa de uma média ponderada . (A média de um grupo com 10 membros pesa mais que a média de um grupo com apenas 2 membros!)

Isso funciona :

SELECT DISTINCT ON (perm, combo)
       perm
      ,combo
      ,avg(value)        OVER wpc AS perm_average_value
      ,avg(value)        OVER wc  AS combo_average_value
      ,stddev_pop(value) OVER wpc AS perm_stddev
      ,stddev_pop(value) OVER wc  AS combo_stddev
      ,count(*)          OVER wpc AS perm_count
      ,count(*)          OVER wc  AS combo_count
FROM   foo
WINDOW wc  AS (PARTITION BY combo)
      ,wpc AS (PARTITION BY perm, combo);

Estou usando duas janelas diferentes aqui e reduzo as linhas com as DISTINCTquais é aplicada mesmo depois das funções da janela.

Mas duvido seriamente que seja mais rápido que sua consulta original. Tenho certeza de que não é.

Melhor desempenho com layout de tabela alterado

As matrizes têm uma sobrecarga de 24 bytes (pequenas variações dependendo do tipo). Além disso, você parece ter alguns itens por matriz e muitas repetições. Para uma mesa enorme como a sua, pagaria para normalizar o esquema. Layout de exemplo:

CREATE TABLE combo ( 
  combo_id serial PRIMARY KEY
 ,combo    int[] NOT NULL
);

CREATE TABLE perm ( 
  perm_id  serial PRIMARY KEY
 ,perm     int[] NOT NULL
);

CREATE TABLE value (
  perm_id  int REFERENCES perm(perm_id)
 ,combo_id int REFERENCES combo(combo_id)
 ,value numeric NOT NULL DEFAULT 0
);

Se você não precisar de integridade referencial, poderá omitir as restrições de chave estrangeira.

A conexão com combo_idtambém pode ser colocada na tabela perm, mas nesse cenário eu a armazenaria (ligeiramente desnormalizada) valuepara obter um melhor desempenho.

Isso resultaria em um tamanho de linha de 32 bytes (cabeçalho de tupla + preenchimento: 24 bytes, 2 x int (8 bytes), sem preenchimento), além do tamanho desconhecido da sua numericcoluna. (Se você não precisar de extrema precisão, uma double precisionou mesmo uma realcoluna também será necessária.)

Mais sobre armazenamento físico nesta resposta relacionada ao SO ou aqui:
Configurando o PostgreSQL para desempenho de leitura

De qualquer forma, isso é apenas uma fração do que você tem agora e tornaria sua consulta muito mais rápida apenas por tamanho. Agrupar e classificar números inteiros simples também é muito mais rápido.

Você primeiro agregaria uma subconsulta e depois ingressaria no perme combopara obter o melhor desempenho.

Erwin Brandstetter
fonte
Obrigado pela resposta clara e concisa. Você está correto, parece que não há como obter o desvio padrão de uma população de subconjuntos dessa maneira. Dito isto, gosto da simplicidade da sua solução. Eliminar o GROUP BY torna a consulta resultante muito mais legível. Infelizmente, como você suspeitava, o desempenho é insignificante. Eu tive que matar a consulta depois de executar por mais de 30 minutos.
21713 Scott Scott pequeno
@ ScottSmall: Você poderia fazer algo pelo desempenho ... consulte a atualização para responder.
Erwin Brandstetter
Para simplificar minha pergunta, removi as colunas da footabela que não eram relevantes. Na realidade, existem várias outras colunas que não são usadas por essa consulta, portanto, não estou convencido de que a normalização das permutações e combinações forneça um aumento de velocidade significativo para esse caso de uso específico.
22613 Scott Scott pequeno
Além disso, os valores inteiros que compõem cada permutação e combinação vêm de outra tabela no DB. A pré-geração desses dados é computacionalmente cara. O comprimento máximo de um perm / combo é 5, no entanto 5Pn e 5Cn crescem bastante para grandes valores de n (atualmente em torno de 1000, mas crescem diariamente) ... de qualquer maneira, otimizando essa é a pergunta de outro dia. Mais uma vez obrigado por toda a sua ajuda Erwin.
21813 Scott Scott pequeno