检索连续行之间的最小时间间隔的ID

时间:2015-08-20 15:25:30

标签: sql postgresql time-series window-functions gaps-and-islands

我在Postgres 9.3中有以下event表:

CREATE TABLE event (
  event_id    integer PRIMARY KEY,
  user_id     integer,
  event_type  varchar,
  event_time  timestamptz
);

我的目标是检索所有user_id之间的任何事件之间(或最后一次事件与当前时间之间)至少30天的差距。另一个复杂因素是,我只希望具有其中一个差距的用户在执行某个event_type 'convert'时发生。如何轻松完成?

event表中的一些示例数据可能如下所示:

INSERT INTO event (event_id, user_id, event_type, event_time)
VALUES
(10, 1, 'signIn',  '2015-05-05 00:11'),
(11, 1, 'browse',  '2015-05-05 00:12'),  -- no 'convert' event

(20, 2, 'signIn',  '2015-06-07 02:35'),
(21, 2, 'browse',  '2015-06-07 02:35'),
(22, 2, 'convert', '2015-06-07 02:36'),  -- only 'convert' event
(23, 2, 'signIn',  '2015-08-10 11:00'),  -- gap of >= 30 days
(24, 2, 'signIn',  '2015-08-11 11:00'),

(30, 3, 'convert', '2015-08-07 02:36'),  -- starting with 1st 'convert' event
(31, 3, 'signIn',  '2015-08-07 02:36'),
(32, 3, 'convert', '2015-08-08 02:36'),
(33, 3, 'signIn',  '2015-08-12 11:00'),  -- all gaps below 30 days
(33, 3, 'browse',  '2015-08-12 11:00'),  -- gap until today (2015-08-20) too small

(40, 4, 'convert', '2015-05-07 02:36'),
(41, 4, 'signIn',  '2015-05-12 11:00');  -- gap until today (2015-08-20) >= 30 days

预期结果:

user_id
--------
2
4

2 个答案:

答案 0 :(得分:2)

一种方法:

SELECT user_id
FROM  (
   SELECT user_id
        , lead(e.event_time, 1, now()) OVER (PARTITION BY e.user_id ORDER BY e.event_time)
          - event_time AS gap
   FROM  (  -- only users with 'convert' event
      SELECT user_id, min(event_time) AS first_time
      FROM   event
      WHERE  event_type = 'convert'
      GROUP  BY 1
      ) e1
   JOIN   event e USING (user_id)
   WHERE  e.event_time >= e1.first_time
   ) sub
WHERE  gap >= interval '30 days'
GROUP  BY 1;

如果没有“下一行”,window function lead()允许包含默认值,这样可以方便地满足您的额外要求“或者在他们的最后一次事件和当前时间之间”。

索引

如果你的桌子很大,你至少应该在(user_id, event_time)上有一个索引:

CREATE INDEX event_user_time_idx ON event(user_id, event_time);

如果您经常这样做并且event_type'转换'很少,请添加另一个部分索引:

CREATE INDEX event_user_time_convert_idx ON event(user_id, event_time)
WHERE  event_type = 'convert';

每个用户许多事件

并且只有30天的差距普通(不是罕见的情况) 指数变得更加重要 试试这个recursive CTE可以获得更好的效果:

WITH RECURSIVE cte AS (
   (  -- parentheses required
   SELECT DISTINCT ON (user_id)
          user_id, event_time, interval '0 days' AS gap
   FROM   event
   WHERE  event_type = 'convert'
   ORDER  BY user_id, event_time
   )

   UNION ALL
   SELECT c.user_id, e.event_time, COALESCE(e.event_time, now()) - c.event_time
   FROM   cte c
   LEFT   JOIN LATERAL (
      SELECT e.event_time
      FROM   event e
      WHERE  e.user_id = c.user_id
      AND    e.event_time > c.event_time
      ORDER  BY e.event_time
      LIMIT  1     -- the next later event
      ) e ON true  -- add 1 row after last to consider gap till "now"
   WHERE  c.event_time IS NOT NULL
   AND    c.gap < interval '30 days'
   )
SELECT * FROM cte
WHERE  gap >= interval '30 days';

它有更多的开销,但可以停止 - 每个用户 - 在第一个足够大的差距。如果那应该是最后一个事件 now 之间的差距,那么结果中的event_time为NULL。

新的SQL Fiddle,其中有更多显示测试数据,显示两个查询。

这些相关答案中的详细解释:

答案 1 :(得分:0)

SQL Fiddle

这是另一种方式,可能不像@Erwin那样整洁,但所有步骤都分开,因此很容易适应。

  • include_today:添加虚拟事件以指示当前日期。
  • event_convert:计算每个#'的事件{ begin = '(^[ \t]+)?(?=#'' )'; end = '(?!\G)'; beginCaptures = { 1 = { name = 'punctuation.whitespace.comment.leading.r'; }; }; patterns = ( { name = 'comment.line.number-sign-tick.r'; begin = "#' "; end = '\n'; beginCaptures = { 0 = { name = 'punctuation.definition.comment.r'; }; }; }, ); }, 第一次出现(在这种情况下只有convert
  • event_row:为每个事件指定唯一的连续ID。每个user_id
  • 从1开始
  • 最后一部分加在一起并使用user_id = 2222因此可以计算日期差异。
  • 此外,结果显示两个事件都涉及user_id范围,因此您可以查看这是否是您想要的结果。

rnum = rnum + 1