PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Funções de janela ou expressões de tabela comuns:conte as linhas anteriores dentro do intervalo


Eu não acho que você possa fazer isso de forma barata com uma consulta simples, CTEs e funções de janela - sua definição de quadro é estática, mas você precisa de um quadro dinâmico (dependendo dos valores da coluna).

Geralmente, você terá que definir os limites inferior e superior de sua janela com cuidado:As seguintes consultas excluem a linha atual e incluir a borda inferior.
Ainda há uma pequena diferença:a função inclui os pares anteriores da linha atual, enquanto a subconsulta correlacionada os exclui ...

Caso de teste


Usando ts em vez da palavra reservada date como nome da coluna.
CREATE TABLE test (
  id  bigint
, ts  timestamp
);

ROM - consulta de Roman


Use CTEs, agregue carimbos de data/hora em uma matriz, desagrupe, conte ...
Embora esteja correto, o desempenho deteriora-se drasticamente com mais de uma mão cheia de linhas. Existem alguns assassinos de desempenho aqui. Ver abaixo.

ARR - contagem de elementos da matriz


Peguei a consulta de Roman e tentei simplificá-la um pouco:
  • Remova o 2º CTE que não é necessário.
  • Transforme o 1º CTE em subconsulta, o que é mais rápido.
  • Direto count() em vez de reagregar em um array e contar com array_length() .

Mas o manuseio de arrays é caro e o desempenho ainda se deteriora muito com mais linhas.
SELECT id, ts
     , (SELECT count(*)::int - 1
        FROM   unnest(dates) x
        WHERE  x >= sub.ts - interval '1h') AS ct
FROM (
   SELECT id, ts
        , array_agg(ts) OVER(ORDER BY ts) AS dates
   FROM   test
   ) sub;

COR - subconsulta correlacionada


Você poderia resolva-o com uma simples subconsulta correlacionada. Muito mais rápido, mas ainda assim...
SELECT id, ts
     , (SELECT count(*)
        FROM   test t1
        WHERE  t1.ts >= t.ts - interval '1h'
        AND    t1.ts < t.ts) AS ct
FROM   test t
ORDER  BY ts;

FNC - Função


Faça um loop sobre as linhas em ordem cronológica com um row_number() na função plpgsql e combine isso com um cursor sobre a mesma consulta, abrangendo o período de tempo desejado. Então podemos apenas subtrair os números das linhas:
CREATE OR REPLACE FUNCTION running_window_ct(_intv interval = '1 hour')
  RETURNS TABLE (id bigint, ts timestamp, ct int)
  LANGUAGE plpgsql AS
$func$
DECLARE
   cur   CURSOR FOR
         SELECT t.ts + _intv AS ts1, row_number() OVER (ORDER BY t.ts) AS rn
         FROM   test t ORDER BY t.ts;
   rec   record;
   rn    int;

BEGIN
   OPEN cur;
   FETCH cur INTO rec;
   ct := -1;  -- init

   FOR id, ts, rn IN
      SELECT t.id, t.ts, row_number() OVER (ORDER BY t.ts)
      FROM   test t ORDER BY t.ts
   LOOP
      IF rec.ts1 >= ts THEN
         ct := ct + 1;
      ELSE
         LOOP
            FETCH cur INTO rec;
            EXIT WHEN rec.ts1 >= ts;
         END LOOP;
         ct := rn - rec.rn;
      END IF;

      RETURN NEXT;
   END LOOP;
END
$func$;

Chamada com intervalo padrão de uma hora:
SELECT * FROM running_window_ct();

Ou com qualquer intervalo:
SELECT * FROM running_window_ct('2 hour - 3 second');

db<>mexa aqui
antigo sqlfiddle

Referência


Com a tabela acima, executei um benchmark rápido no meu antigo servidor de teste:(PostgreSQL 9.1.9 no Debian).
-- TRUNCATE test;
INSERT INTO test
SELECT g, '2013-08-08'::timestamp
         + g * interval '5 min'
         + random() * 300 * interval '1 min' -- halfway realistic values
FROM   generate_series(1, 10000) g;

CREATE INDEX test_ts_idx ON test (ts);
ANALYZE test;  -- temp table needs manual analyze

Eu variei o negrito parte para cada execução e tirou a melhor de 5 com EXPLAIN ANALYZE .

100 linhas
ROM:27,656 ms
ARR:7,834 ms
COR:5,488 ms
FNC:1,115 ms

1.000 linhas
ROM:2116,029 ms
ARR:189,679 ms
COR:65,802 ms
FNC:8,466 ms

5.000 linhas
ROM:51347 ms !!
ARR:3167 ms
COR:333 ms
FNC:42 ms

100.000 linhas
ROM:DNF
ARR:DNF
COR:6760 ms
FNC:828 ms

A função é o vencedor claro. É mais rápido por uma ordem de magnitude e escala melhor.
O manuseio de array não pode competir.