Sql 窗口函数或公共表表达式:统计范围内的上一行
我想使用一个窗口函数来确定,对于每一行,满足特定条件的前面记录的总数 一个具体的例子:Sql 窗口函数或公共表表达式:统计范围内的上一行,sql,postgresql,plpgsql,common-table-expression,window-functions,Sql,Postgresql,Plpgsql,Common Table Expression,Window Functions,我想使用一个窗口函数来确定,对于每一行,满足特定条件的前面记录的总数 一个具体的例子: clone=# \d test Table "pg_temp_2.test" Column | Type | Modifiers --------+-----------------------------+----------- id | bigint | date |
clone=# \d test
Table "pg_temp_2.test"
Column | Type | Modifiers
--------+-----------------------------+-----------
id | bigint |
date | timestamp without time zone |
我想知道每个日期前“1小时”内的行数
我可以用一个窗口函数来做这个吗?还是我需要调查CTE
我真的很想能够写一些像不工作这样的东西:
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?
CTE还不是我非常了解的东西。我有一种感觉,我很快就要走了 我不认为使用普通查询、CTE和窗口函数可以便宜地做到这一点——它们的帧定义是静态的,但您需要一个取决于列值的动态帧 通常,您必须仔细定义窗口的下限和上限:以下查询排除当前行并包括下边框。 还有一个细微的区别:该函数包含当前行的先前对等点,而相关子查询将它们排除在外 测试用例 使用ts而不是保留字date作为列名
CREATE TABLE test (
id bigint
, ts timestamp
);
使用CTE,将时间戳聚合到一个数组中,unest,count。。。
虽然正确,但性能急剧恶化,超过一手的行。这里有几个性能杀手。见下文
ARR-计数数组元素
我接受了Roman的提问,并试图简化一下:
拆下不必要的第二个CTE。
将第一个CTE转换为子查询,这会更快。
直接计数,而不是重新聚合到数组中并使用数组长度进行计数。
但阵列处理成本很高,而且行数越多,性能仍会严重恶化
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;
默认间隔为一小时的通话:
SELECT * FROM running_window_ct();
或以任何间隔:
SELECT * FROM running_window_ct('2 hour - 3 second');
小提琴
旧的
基准
使用上面的表,我在旧的测试服务器上运行了一个快速基准测试:Debian上的PostgreSQL 9.1.9
-- 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 ANALYSE从5个部分中选择最好的部分
100行
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
ARR: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;
看
希望对您有所帮助注意:您的第二次查询不会找到在前一个1小时时间窗口中没有任何匹配行的行注意2:尽量不要使用日期作为标识符。这是一个保留字类型名称。虽然它可以正常工作,但性能会因超过几行而急剧下降。在我更新的答案中考虑解释、备选和基准。谢谢@ ErWrBrand Stutter + 1,我有另一个想法,但是没有时间去尝试它,欧文可能有点聪明的分析。我感谢你为说明差异所做的努力。同时,我使用了plpgsql方法,因为我需要实现围绕这个想法的其他逻辑。正如您所建议的,性能明显好于我所希望的。这是否仍然适用于框架_子句选项上的窗口函数?前一个值和当前值之间的相似范围row@Davos:是的,仍然适用于13级研究生。窗口框架不能依赖于列值。
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