Jag tror inte att du kan göra detta billigt med en vanlig fråga, CTE:er och fönsterfunktioner - deras ramdefinition är statisk, men du behöver en dynamisk ram (beroende på kolumnvärden).
I allmänhet måste du definiera den nedre och övre gränsen för ditt fönster noggrant:Följande frågor utesluter den aktuella raden och inkludera den nedre gränsen.
Det finns fortfarande en mindre skillnad:funktionen inkluderar tidigare peers i den aktuella raden, medan den korrelerade underfrågan utesluter dem ...
Testfall
Använder ts
istället för reserverat ord date
som kolumnnamn.
CREATE TABLE test (
id bigint
, ts timestamp
);
ROM - Romans fråga
Använd CTE:er, samla tidsstämplar till en array, unnest, räkna ...
Även om det är korrekt, försämras prestanda drastiskt med mer än en hand full av rader. Det finns ett par prestationsdödare här. Se nedan.
ARR - räkna arrayelement
Jag tog Romans fråga och försökte effektivisera den lite:
- Ta bort 2:a CTE som inte är nödvändigt.
- Omvandla 1:a CTE till underfråga, vilket är snabbare.
- Direkt
count()
istället för att återaggregera till en array och räkna medarray_length()
.
Men arrayhantering är dyrt, och prestandan försämras fortfarande dåligt med fler rader.
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 - korrelerad underfråga
Du kunde lös det med en enkel korrelerad underfråga. Mycket snabbare, men ändå ...
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 - Funktion
Slinga över rader i kronologisk ordning med en row_number()
i plpgsql-funktionen och kombinera det med en markör över samma fråga och spänner över den önskade tidsramen. Sedan kan vi bara subtrahera radnummer:
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$;
Ring med standardintervall på en timme:
SELECT * FROM running_window_ct();
Eller med valfritt intervall:
SELECT * FROM running_window_ct('2 hour - 3 second');
db<>spela här
Gammal sqlfiddle
Benchmark
Med tabellen ovanifrån körde jag en snabb benchmark på min gamla testserver:(PostgreSQL 9.1.9 på 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
Jag varierade fet del för varje körning och tog det bästa av 5 med EXPLAIN ANALYZE
.
100 rader
ROM:27,656 ms
ARR:7,834 ms
COR:5,488 ms
FNC:1,115 ms
1 000 rader
ROM:2116,029 ms
ARR:189,679 ms
COR:65,802 ms
FNC:8,466 ms
5 000 rader
ROM:51347 ms !!
ARR:3167 ms
COR:333 ms
FNC:42 ms
100 000 rader
ROM:DNF
ARR:DNF
COR:6760 ms
FNC:828 ms
Funktionen är den klara segraren. Den är snabbast i en storleksordning och skalar bäst.
Arrayhantering kan inte konkurrera.