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