【问题标题】:Flattening intersecting timespans by user in PostgreSQL在 PostgreSQL 中按用户展平相交时间跨度
【发布时间】:2014-02-21 08:14:11
【问题描述】:

我正在尝试将重叠的开始结束时间戳合并到单个时间跨度中。类似的问题在 SO 上可用 here。我想为我在数据中拥有的每个用户分别合并时间戳。

SQLFiddle

样本数据:

-- drop table if exists app_log;

create table app_log (
  user_id int,
  login_time timestamp,
  logout_time timestamp
);

insert into app_log values
  (1, '2014-01-01 08:00', '2014-01-01 10:00'), /* here we start */
  (1, '2014-01-01 09:10', '2014-01-01 09:59'), /* fully included in previous interval */
  (1, '2014-01-01 10:00', '2014-01-01 10:48'), /* continuing first interval */
  (1, '2014-01-01 10:40', '2014-01-01 10:49'), /* continuing previous interval */
  (1, '2014-01-01 10:55', '2014-01-01 11:00'), /* isolated interval */
  (2, '2014-01-01 09:00', '2014-01-01 11:00'), /* 2nd user is shifted by one hour */
  (2, '2014-01-01 10:10', '2014-01-01 10:59'), /* to simulate overlaps with 1st user */
  (2, '2014-01-01 11:00', '2014-01-01 11:48'), 
  (2, '2014-01-01 11:40', '2014-01-01 11:49'), 
  (2, '2014-01-01 11:55', '2014-01-01 12:00')  
;

要求的结果:

  used_id  login_time       logout_time
  1        2014-01-01 08:00 2014-01-01 10:49 /* Merging first 4 lines */
  1        2014-01-01 10:55 2014-01-01 11:00 /* 5 th line is isolated */
  2        2014-01-01 09:00 2014-01-01 11:49 /* Merging lines 6-9 */
  2        2014-01-01 11:55 2014-01-01 12:00 /* last line is isolated */

我尝试使用mentioned question 中提供的解决方案,但即使对于单个用户,它也不会返回正确答案:

with recursive

in_data as (select login_time as d1, logout_time as d2 from app_log where user_id = 1)

, dateRanges (ancestorD1, parentD1, d2, iter) as
(
--anchor is first level of collapse
    select
        d1 as ancestorD1,
        d1 as parentD1,
        d2,
        cast(0 as int) as iter
    from in_data

--recurse as long as there is another range to fold in
    union all

    select
        tLeft.ancestorD1,
        tRight.d1 as parentD1,
        tRight.d2,
        iter + 1  as iter
    from dateRanges as tLeft join in_data as tRight
        --join condition is that the t1 row can be consumed by the recursive row
        on tLeft.d2 between tRight.d1 and tRight.d2
            --exclude identical rows
            and not (tLeft.parentD1 = tRight.d1 and tLeft.d2 = tRight.d2)
)
select
    ranges1.*
from dateRanges as ranges1
where not exists (
    select 1
    from dateRanges as ranges2
    where ranges1.ancestorD1 between ranges2.ancestorD1 and ranges2.d2
        and ranges1.d2 between ranges2.ancestorD1 and ranges2.d2
        and ranges2.iter > ranges1.iter
);

结果:

ancestord1 parentd1 d2 iter
2014-01-01 10:55:00;2014-01-01 10:55:00;2014-01-01 11:00:00;0
2014-01-01 08:00:00;2014-01-01 10:40:00;2014-01-01 10:49:00;2
2014-01-01 09:10:00;2014-01-01 10:40:00;2014-01-01 10:49:00;3

上面的查询有什么问题,如何扩展它以获取用户的结果? PostgreSQL中有没有更好的解决方案?

【问题讨论】:

  • 作为旁注,don't use BETWEEN with date/time/timestamp types。 SQL Server 仍然可能存在特定风险(取决于您的 PostgreSQL 发行版是否使用双支持时间戳编译),但可能性较小;但是,我个人认为BETWEEN 实际上促进了对非整数类型范围的错误看法。也就是说,将其他所有内容视为逻辑上(如果不是实际上)是一个自然数,具有潜在的未定义精度。

标签: sql postgresql postgresql-9.1 date-range recursive-query


【解决方案1】:

我发现这个example of how to make a 'range aggregate' 使用窗口函数和许多嵌套子查询。我只是将它调整为按 user_id 进行分区和分组,它似乎可以满足您的要求:

SELECT user_id, min(login_time) as login_time, max(logout_time) as logout_time
FROM (
    SELECT user_id, login_time, logout_time,
        max(new_start) OVER (PARTITION BY user_id ORDER BY login_time, logout_time) AS left_edge
    FROM (
        SELECT user_id, login_time, logout_time,
            CASE 
                WHEN login_time <= max(lag_logout_time) OVER (
                    PARTITION BY user_id ORDER BY login_time, logout_time 
                ) THEN NULL 
                ELSE login_time 
            END AS new_start
        FROM (
            SELECT 
                user_id, 
                login_time, 
                logout_time,
                lag(logout_time) OVER (PARTITION BY user_id ORDER BY login_time, logout_time) AS lag_logout_time
            FROM app_log
        ) AS s1
    ) AS s2
) AS s3
GROUP BY user_id, left_edge
ORDER BY user_id, min(login_time)

结果:

 user_id |     login_time      |     logout_time
---------+---------------------+---------------------
       1 | 2014-01-01 08:00:00 | 2014-01-01 10:49:00
       1 | 2014-01-01 10:55:00 | 2014-01-01 11:00:00
       2 | 2014-01-01 09:00:00 | 2014-01-01 11:49:00
       2 | 2014-01-01 11:55:00 | 2014-01-01 12:00:00
(4 rows)

它的工作原理是首先检测每个新范围的开始(由 user_id 分区),然后根据检测到的范围进行扩展和分组。我发现我必须非常仔细地阅读那篇文章才能理解它!

文章建议可以使用 Postgresql>=9.0 通过删除最里面的子查询并更改窗口范围来简化它,但我无法让它工作。

【讨论】:

  • 看起来不错,需要花点时间才能真正理解它。我认为最里面的子查询可以替换为ROWS 子句,如下所示:SELECT user_id, login_time, logout_time, CASE WHEN login_time &lt;= max(logout_time) OVER (PARTITION BY user_id ORDER BY login_time, logout_time rows between unbounded preceding and 1 preceding) THEN NULL ELSE login_time END AS new_start FROM app_log。也许您可以将其添加到您的答案中,以便我们有最简单的陈述。
猜你喜欢
  • 2010-11-01
  • 2011-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多