在Rails + Postgres中按任意时间间隔计算记录的最佳方法

jpw*_*ynn 21 sql postgresql ruby-on-rails aggregate-functions generate-series

我的应用程序有一个Events带有时间戳事件的表.

我需要在每个最近的N时间间隔内报告事件的数量.对于不同的报告,间隔可以是"每周"或"每天"或"每小时"或"每15分钟间隔".

例如,用户可以显示他们每周,每天,每小时或每季度收到的订单数量.

1)我的偏好是动态地执行单个SQL查询(我正在使用Postgres)按任意时间间隔进行分组.有没有办法做到这一点?

2)一种简单但丑陋的暴力方法是对按时间戳排序的开始/结束时间帧内的所有记录执行单个查询,然后使用方法按任意间隔手动构建计数.

3)另一种方法是在事件表中为每个区间添加单独的字段并静态存储一个the_week the_day,the_hourthe_quarter_hour字段,这样我就可以在创建记录时(一次)进行"点击",而不是每次报告该字段时.

这里有什么最好的做法,因为我可以根据需要修改模型和预先存储间隔数据(尽管只需要将表格宽度加倍);

Erw*_*ter 38

幸运的是,你正在使用PostgreSQL.窗口功能generate_series()是你的朋友.

测试用例

给出以下测试表(应该提供):

CREATE TABLE event(event_id serial, ts timestamp);
INSERT INTO event (ts)
SELECT generate_series(timestamp '2018-05-01'
                     , timestamp '2018-05-08'
                     , interval '7 min') + random() * interval '7 min';
Run Code Online (Sandbox Code Playgroud)

每7分钟一次(加0到7分钟,随机).

基本解决方案

此查询计算任意时间间隔的事件.示例中的17分钟:

WITH grid AS (
   SELECT start_time
        , lead(start_time, 1, 'infinity') OVER (ORDER BY start_time) AS end_time
   FROM  (
      SELECT generate_series(min(ts), max(ts), interval '17 min') AS start_time
      FROM   event
      ) sub
   )
SELECT start_time, count(e.ts) AS events
FROM   grid       g
LEFT   JOIN event e ON e.ts >= g.start_time
                   AND e.ts <  g.end_time
GROUP  BY start_time
ORDER  BY start_time;
Run Code Online (Sandbox Code Playgroud)
  • 查询ts从基表中检索最小值和最大值以覆盖整个时间范围.您可以使用任意时间范围.

  • 根据需要提供任何时间间隔.

  • 每个时隙生成一行.如果在该间隔期间没有发生任何事件,则计数为0.

  • 确保正确处理上限和下限:

  • 窗口函数lead()有一个经常被忽略的特性:它可以在没有前导行时提供默认值.'infinity'在示例中提供.否则最后一个间隔将被上限切断NULL.

最小的等价物

以上查询使用CTE lead()和详细语法.优雅,也许更容易理解,但有点贵.这是一个更短,更快,最小的版本:

SELECT start_time, count(e.ts) AS events
FROM  (SELECT generate_series(min(ts), max(ts), interval '17 min') FROM event) g(start_time)
LEFT   JOIN event e ON e.ts >= g.start_time
                   AND e.ts <  g.start_time + interval '17 min'
GROUP  BY 1
ORDER  BY 1;
Run Code Online (Sandbox Code Playgroud)

"过去一周每15分钟"的例子

和格式化to_char().

SELECT to_char(start_time, 'YYYY-MM-DD HH24:MI'), count(e.ts) AS events
FROM   generate_series(date_trunc('day', localtimestamp - interval '7 days')
                     , localtimestamp
                     , interval '15 min') g(start_time)
LEFT   JOIN event e ON e.ts >= g.start_time
                   AND e.ts <  g.start_time + interval '15 min'
GROUP  BY start_time
ORDER  BY start_time;
Run Code Online (Sandbox Code Playgroud)

仍然ORDER BYGROUP BY基础时间戳,而不是格式化的字符串.这更快,更可靠.

db <> 在这里小提琴

相关答案在时间范围内产生运行计数:

  • 说真的,在SO的历史上是最多的.惊人.回答.永远. (9认同)