计算范围内的前几行

问题描述 投票:0回答:2

我想确定每一行在给定时间范围内先前记录的总数。

具体例子:

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)?

sql postgresql plpgsql common-table-expression window-functions
2个回答
9
投票

Postgres 11 或更高版本

引用 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
中可能存在重复项,您必须定义如何计算这些重复项,并且可能需要执行更多操作。

Postgres 10 或以上

您无法通过简单的查询、CTE 和窗口函数廉价地完成此操作 - 它们的框架定义是静态的,但您需要一个动态框架(取决于列值)。

通常,您必须仔细定义窗口的下边界和上边界:以下查询排除当前行和包括下边框。
仍然有一个细微的差别:该函数包括当前行的前一个同级,而相关子查询则排除它们......

测试用例

使用

ts
代替保留字
date
作为列名。

CREATE TABLE test (
  id  bigint
, ts  timestamp
);

ROM - 罗曼的查询

使用 CTE,将时间戳聚合到数组中,取消嵌套,计数...
虽然正确,但当行数超过一手时,性能会“急剧恶化”。这里有几个性能杀手。见下文。 ARR - 计算数组元素

我接受了 Roman 的查询并尝试简化它:

删除不必要的第二个 CTE。
  • 将第一个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;

COR - 相关子查询

可以

用一个简单的相关子查询来解决它。快了很多,但仍然...

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 - 功能
使用 

plpgsql 函数

中的

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
基准

根据上面的表格,我在我的旧测试服务器上运行了一个快速基准测试:(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 ANALYZE选择了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 COR:6760 毫秒
FNC:
828 毫秒

函数是明显的胜利者。它速度最快一个数量级,并且扩展性最好。 数组处理无法竞争。



2
投票
我之前的尝试表现不佳,因为它将所有元素组合成数组,而这不是我想要做的。所以这是一个更新版本 - 它的性能不如自连接或游标功能,但它并不像我之前的版本那么糟糕:

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;

我在我的本地机器和 sqlfiddle 中进行了测试,实际上自连接表现最好(我很惊讶,我的结果与 Erwin 不一样),然后是 Erwin 函数,然后是这个聚合。您可以在
sqlfiddle

中自行测试 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

参见
sql 小提琴演示

希望有帮助

© www.soinside.com 2019 - 2024. All rights reserved.