4

我想使用一个窗口函数来确定每一行满足特定条件的先前记录的总数。

一个具体的例子:

clone=# \d test
              Table "pg_temp_2.test"
 Column |            Type             | Modifiers 
--------+-----------------------------+-----------
 id     | bigint                      | 
 date   | timestamp without time zone | 

我想知道date“前 1 小时”内的每个行数date

我可以用窗口功能做到这一点吗?还是我需要调查 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 还不是我非常了解的东西。我有一种感觉,我很快就会去。:)

4

2 回答 2

6

我不认为您可以使用普通查询、CTE 和窗口函数廉价地做到这一点——它们的框架定义是静态的,但您需要一个动态框架(取决于列值)。

通常,您必须仔细定义窗口的下限和上限:以下查询排除当前行并包含下边框。
仍然有一个细微的区别:该函数包括当前行的先前对等点,而相关子查询将它们排除在外...

测试用例

使用ts而不是保留字date作为列名。

CREATE TABLE test (
  id  bigint
, ts  timestamp
);

ROM - 罗曼的查询

使用 CTE,将时间戳聚合到一个数组中,unnest,计数......
虽然正确,但性能会急剧下降,而不是一手满满的行。这里有几个性能杀手。见下文。

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

row_number()使用plpgsql 函数按时间顺序循环行,并将其与同一查询上的游标结合,跨越所需的时间范围。然后我们可以减去行号:

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$;

以一小时的默认间隔调用:

SELECT * FROM running_window_ct();

或任何间隔:

SELECT * FROM running_window_ct('2 hour - 3 second');

db<>fiddle here
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
ARR:DNF
COR:6760 毫秒
FNC:828 毫秒

功能是明显的胜利者。它是最快的一个数量级并且规模最好。
数组处理无法竞争。

于 2013-08-11T16:25:21.080 回答
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中自己测试

以前我还在学习 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 fiddle 演示

希望有帮助

于 2013-08-11T18:38:42.317 回答