【问题标题】:Retrieve IDs with a minimum time gap between consecutive rows检索连续行之间时间间隔最小的 ID
【发布时间】:2015-11-14 08:01:22
【问题描述】:

我在 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

【问题讨论】:

  • 我想我有。但是您能否也包括您的期望输出?
  • 另外请下次尝试加入一个小提琴,帮助我们解决您的问题SQL Fiddle
  • 所以不要列出根本没有'convert'事件的用户?
  • 我的猜测是事件 convert --- 事件 A(30 分钟)事件 B
  • 因此,对于您的示例数据,实际上只有 event_id 0005 可以满足这两个要求,因此您的结果集将是 user_id 2222。对吗?

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


【解决方案1】:

一种方法:

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 'convert' 很少见,请添加另一个部分索引:

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

它有相当多的开销,但可以在第一个足够大的间隙处停止 - 每个用户。如果这应该是上一个事件现在之间的间隔,那么结果中的event_time 为NULL。

新的SQL Fiddle 包含更多展示这两个查询的测试数据。

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

【讨论】:

  • 我认为您没有将当前日期作为 OP 提及
  • @JuanCarlosOropeza:没错。添加了 now()::timestamp 作为默认 lead() 来解决这个问题。
  • 假设每个用户有 100 行。更快的查询会发挥作用吗?我可以看看这个答案下面的那些吗?
  • @tscizzle:我添加了另一个答案。顺便说一句,now() 返回timestamptz,不需要添加演员表。
【解决方案2】:

SQL Fiddle

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

  • include_today:添加一个虚拟事件来指示当前日期。
  • event_convert:计算每个user_id事件convert第一次出现的时间(在这种情况下只有user_id = 2222
  • event_row:为每个事件分配一个唯一的连续ID。每个user_id 从 1 开始
  • 最后一部分连接在一起并使用rnum = rnum + 1,因此可以计算日期差异。
  • 结果还显示两个事件都在 30 days 范围内,因此您可以查看这是否是您想要的结果。

.

WITH include_today as (
     (SELECT 'xxxx' event_id, user_id, 'today' event_type, current_date as event_time
     FROM users)
     UNION
     (SELECT *
     FROM event)
),
event_convert as (
     SELECT user_id, MIN(event_time) min_time
     FROM event
     WHERE event_type = 'convert'
     GROUP BY user_id
),
event_row as (
    SELECT *, row_number() OVER (PARTITION BY user_id ORDER BY event_time desc) as rnum
    FROM 
        include_today 
 )
SELECT 
    A.user_id,
    A.event_id eventA, 
    A.event_type typeA,
    A.event_time timeA,
    B.event_id eventB, 
    B.event_type typeB,
    B.event_time timeB,
    (B.event_time - A.event_time) days
FROM
    event_convert e 
    Inner Join event_row A 
        ON e.user_id = A.user_id and e.min_time <= a. event_time 
    Inner Join event_row B 
        ON A.rnum = B.rnum + 1
       AND A.user_id = B.user_id
WHERE 
   (B.event_time - A.event_time) > interval '30 days'
ORDER BY 1,4

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-07-22
    • 1970-01-01
    • 1970-01-01
    • 2018-01-10
    • 1970-01-01
    • 1970-01-01
    • 2010-10-28
    相关资源
    最近更新 更多