Soma / contagem / média rotativa ao longo do intervalo de datas

20

Em um banco de dados de transações que abrangem milhares de entidades ao longo de 18 meses, eu gostaria de executar uma consulta para agrupar todos os períodos de 30 dias possíveis entity_idcom uma SOMA de seus valores de transação e COUNT de suas transações nesse período de 30 dias, e retornar os dados de uma maneira que eu possa consultar. Após muitos testes, esse código realiza muito do que eu quero:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

E eu vou usar em uma consulta maior estruturada algo como:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

O caso que esta consulta não cobre é quando a contagem da transação se estende por vários meses, mas ainda fica dentro de 30 dias um do outro. Esse tipo de consulta é possível com o Postgres? Nesse caso, agradeço qualquer contribuição. Muitos dos outros tópicos discutem agregados "em execução ", não rolando .

Atualizar

O CREATE TABLEscript:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

Os dados de amostra podem ser encontrados aqui . Estou executando o PostgreSQL 9.1.16.

A produção ideal incluiria SUM(amount)e COUNT()de todas as transações em um período contínuo de 30 dias. Veja esta imagem, por exemplo:

Exemplo de linhas que idealmente seriam incluídas em um "conjunto", mas não porque o meu conjunto é estático por mês.

O destaque verde da data indica o que está sendo incluído na minha consulta. O destaque da linha amarela indica os registros que eu gostaria de fazer parte do conjunto.

Leitura anterior:

tufelkinder
fonte
11
Por every possible 30-day period by entity_idque você quer dizer o período pode começar a qualquer dia, para 365 possíveis períodos em um (não-bissexto) ano? Ou você deseja considerar apenas os dias com uma transação real como início de um período individualmente para algum entity_id ? De qualquer forma, forneça sua definição de tabela, versão do Postgres, alguns dados de amostra e o resultado esperado para a amostra.
Erwin Brandstetter
Em teoria, eu quis dizer qualquer dia, mas na prática não há necessidade de considerar os dias em que não há transações. Publiquei os dados de amostra e a definição da tabela.
tufelkinder
Portanto, você deseja acumular linhas iguais entity_idem uma janela de 30 dias a partir de cada transação real. Pode haver várias transações para o mesmo (trans_date, entity_id)ou essa combinação é definida como única? Sua definição da tabela não tem UNIQUEou restrição PK, mas as restrições parecem estar faltando ...
Erwin Brandstetter
A única restrição está na idchave primária. Pode haver várias transações por entidade por dia.
21715 Tillelkinder
Sobre a distribuição de dados: existem entradas (por entidade_id) na maioria dos dias?
Erwin Brandstetter

Respostas:

26

A consulta que você tem

Você pode simplificar sua consulta usando uma WINDOWcláusula, mas isso apenas reduz a sintaxe, não altera o plano de consulta.

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • Também usando o ligeiramente mais rápido count(*), uma vez que idcertamente é definido NOT NULL?
  • E você não precisa, ORDER BY entity_idjá que você jáPARTITION BY entity_id

Porém, você pode simplificar ainda mais:
não adicione ORDER BYa definição da janela, pois ela não é relevante para sua consulta. Então você não precisa definir um quadro de janela personalizado:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

Mais simples, mais rápida, mas ainda assim uma versão melhor do que você tem , com meses estáticos .

A consulta que você pode querer

... não está claramente definido, por isso vou construir sobre essas suposições:

Contar transações e valor para cada período de 30 dias na primeira e na última transação de qualquer entity_id. Exclua os períodos inicial e final sem atividade, mas inclua todos os períodos possíveis de 30 dias dentro desses limites externos.

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

Isso lista todos os períodos de 30 dias para cada um entity_idcom seus agregados e como trans_datesendo o primeiro dia (incl.) Do período. Para obter valores para cada linha individual, junte-se à tabela base mais uma vez ...

A dificuldade básica é a mesma discutida aqui:

A definição de quadro de uma janela não pode depender dos valores da linha atual.

E, em vez disso, ligue generate_series()com a timestampentrada:

A consulta que você realmente deseja

Após a atualização e discussão da pergunta:
Acumule linhas do mesmo entity_idem uma janela de 30 dias, iniciando em cada transação real.

Como seus dados são distribuídos esparsamente, deve ser mais eficiente executar uma auto-junção com uma condição de intervalo , ainda mais porque o Postgres 9.1 ainda não possui LATERALjunções:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

SQL Fiddle.

Uma janela rolante só poderia fazer sentido (com relação ao desempenho) com os dados da maioria dos dias.

Isso não agrega duplicatas (trans_date, entity_id)por dia, mas todas as linhas do mesmo dia são sempre incluídas na janela de 30 dias.

Para uma tabela grande, um índice de cobertura como este poderia ajudar bastante:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

A última coluna amounté útil apenas se você conseguir verificações somente de índice. Caso contrário, solte-o.

Mas não será usado enquanto você selecionar a tabela inteira de qualquer maneira. Ele suportaria consultas para um pequeno subconjunto.

Erwin Brandstetter
fonte
Isso parece realmente bom, testá-lo sobre os dados agora, e tentando entender tudo sua consulta está realmente fazendo ...
tufelkinder
@tufelkinder: adicionada uma solução para a pergunta atualizada.
Erwin Brandstetter
Revendo agora. Estou intrigado que ele seja executado no SQL violino ... Quando eu tentar executá-lo diretamente no meu transactiondb, ele erros fora comcolumn "t0.amount" must appear in the GROUP BY clause...
tufelkinder
@tufelkinder: cortei o caso de teste em 100 linhas. O sqlfiddle limita o tamanho dos dados de teste. Jake (o autor) reduziu o limite de limites há alguns meses, para que o site fique com menos facilidade parado.
Erwin Brandstetter
11
Desculpe pelo atraso, necessário para testá-lo no banco de dados completo. Sua resposta foi incrivelmente profunda e educacional, como sempre. Obrigado!
precisa saber é o seguinte