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 comarray_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.