我想确定每一行在给定时间范围内先前记录的总数。
具体例子:
clone=# \d test
Table "pg_temp_2.test"
Column | Type | Modifiers
--------+-----------------------------+-----------
id | bigint |
date | timestamp without time zone |
我想知道每个
date
在“之前 1 小时”内的行数 date
。SELECT id, date
, count(*) OVER (HAVING previous_rows.date >= (date - '1 hour'::interval)) -- ?
FROM test;
我可以通过加入针对自身的测试来编写此内容,如下所示 - 但这不会随着大表而扩展。
SELECT a.id, a.date, count(b.*)-1
FROM test a, test b
WHERE (b.date >= a.date - '1 hour'::interval AND b.date < a.date)
GROUP BY 1,2
ORDER BY 2;
我可以用递归查询来做这件事吗?或者使用常规的通用表表达式 (CTE)?
引用 Postgres 11 的 发行说明:
- 添加 SQL:2011 指定的所有窗口函数框架选项(Oliver Ford、Tom Lane)
具体来说,允许
模式使用RANGE
和PRECEDING
来 选择分组值在指定值的正负范围内的行 抵消。 [...]FOLLOWING
所以现在就很简单了。并且比旧的解决方法更快:
SELECT id, ts
, count(*) OVER (ORDER BY ts RANGE '1 hour' PRECEDING EXCLUDE CURRENT ROW)
FROM test
ORDER BY ts;
如果
ts
中可能存在重复项,您必须定义如何计算这些重复项,并且可能需要执行更多操作。
您无法通过简单的查询、CTE 和窗口函数廉价地完成此操作 - 它们的框架定义是静态的,但您需要一个动态框架(取决于列值)。
通常,您必须仔细定义窗口的下边界和上边界:以下查询排除当前行和包括下边框。
仍然有一个细微的差别:该函数包括当前行的前一个同级,而相关子查询则排除它们......
使用
ts
代替保留字 date
作为列名。
CREATE TABLE test (
id bigint
, ts timestamp
);
使用 CTE,将时间戳聚合到数组中,取消嵌套,计数...
虽然正确,但当行数超过一手时,性能会“急剧恶化”。这里有几个性能杀手。见下文。
ARR - 计算数组元素
删除不必要的第二个 CTE。
count()
array_length()
计数。
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;
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;
row_number()
按时间顺序循环行,并将其与游标 组合在同一查询上,跨越所需的时间范围。然后我们可以减去行数:
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 ROWS UNBOUNDED PRECEDING) 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 ROWS UNBOUNDED PRECEDING)
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$;
为什么ROWS UNBOUNDED PRECEDING
?参见:查找每个客户的前 3 个订单
SELECT * FROM running_window_ct();
或任意间隔:
SELECT * FROM running_window_ct('2 hour - 3 second');
小提琴
旧sqlfiddle
基准
-- 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
我为每次运行改变了
粗体部分,并用
EXPLAIN ANALYZE
选择了5个中最好的。
ROM:27.656 毫秒
ARR:7.834 毫秒
COR:5.488 毫秒
FNC:
1.115 毫秒
1000行
ROM:2116.029 毫秒
ARR:189.679 毫秒
COR:65.802 毫秒
FNC:
8.466 毫秒
5000行
ROM:51347 毫秒!!
ARR:3167 毫秒
COR:333 毫秒
FNC:
42 毫秒
100000 行
ROM:地下城与勇士
到达:DNF
COR:6760 毫秒
FNC:
828 毫秒
函数是明显的胜利者。它速度最快一个数量级,并且扩展性最好。
数组处理无法竞争。
CREATE OR REPLACE FUNCTION agg_array_range_func
(
accum anyarray,
el_cur anyelement,
el_start anyelement,
el_end anyelement
)
returns anyarray
as
$func$
declare
i int;
N int;
begin
N := array_length(accum, 1);
i := 1;
if N = 0 then
return array[el_cur];
end if;
while i <= N loop
if accum[i] between el_start and el_end then
exit;
end if;
i := i + 1;
end loop;
return accum[i:N] || el_cur;
end;
$func$
LANGUAGE plpgsql;
CREATE AGGREGATE agg_array_range
(
anyelement,
anyelement,
anyelement
)
(
SFUNC=agg_array_range_func,
STYPE=anyarray
);
select
id, ts,
array_length(
agg_array_range(ts, ts - interval '1 hour', ts) over (order by ts)
, 1) - 1
from test;
中自行测试 previous
我仍在学习 PostgreSQL,但我非常喜欢所有的可能性。如果是 SQL Server,我会使用 select for xml 和 select from xml。我不知道如何在 PostreSQL 中做到这一点,但是对于该任务有更好的东西 - 数组!所以这是我的带有窗口函数的 CTE(我认为如果表中存在重复的日期,它会无法正常工作,而且我也不知道它是否会比自连接表现更好):
with cte1 as (
select
id, ts,
array_agg(ts) over(order by ts asc) as dates
from test
), cte2 as (
select
c.id, c.ts,
array(
select arr
from (select unnest(dates) as arr) as x
where x.arr >= c.ts - '1 hour'::interval
) as dates
from cte1 as c
)
select c.id, c.ts, array_length(c.dates, 1) - 1 as cnt
from cte2 as c