【问题标题】:Finding total session time of a user in postgres在 postgres 中查找用户的总会话时间
【发布时间】:2022-01-17 01:08:15
【问题描述】:

我正在尝试创建一个查询,该查询将为我提供每个用户每个月的总登录时间列。

username | auth_event_type |         time        | credential_id 

Joe      |       1         | 2021-11-01 09:00:00 | 44
Joe      |       2         | 2021-11-01 10:00:00 | 44
Jeff     |       1         | 2021-11-01 11:00:00 | 45
Jeff     |       2         | 2021-11-01 12:00:00 | 45
Joe      |       1         | 2021-11-01 12:00:00 | 46    
Joe      |       2         | 2021-11-01 12:30:00 | 46
Joe      |       1         | 2021-12-06 14:30:00 | 47
Joe      |       2         | 2021-12-06 15:30:00 | 47

auth_event_type 列指定事件是登录 (1) 还是注销 (2),而 credential_id 指示会话。

我正在尝试创建一个输出如下的查询:

username | year_month | total_time
Joe      | 2021-11    | 1:30
Jeff     | 2021-11    | 1:00
Joe      | 2021-12    | 1:00

我将如何在 postgres 中执行此操作?我在想它会涉及一个窗​​口功能?如果有人能指出我正确的方向,那就太好了。谢谢。

【问题讨论】:

    标签: sql postgresql window-functions


    【解决方案1】:

    解决方案 1 部分工作

    不确定窗口函数是否会对您有所帮助,但聚合函数会:

    WITH list AS
    (
    SELECT username
         , date_trunc('month', time) AS year_month
         , max(time ORDER BY time) - min(time ORDER BY time) AS session_duration
      FROM your_table
     GROUP BY username, date_trunc('month', time), credential_id
     )
    SELECT username
         , to_char (year_month, 'YYYY-MM') AS year_month
         , sum(session_duration) AS total_time
      FROM list
     GROUP BY username, year_month
    

    查询的第一部分汇总了相同用户名 credential_id 的登录/注销时间,第二部分汇总了每年登录/注销时间之间的差异。在登录时间和注销时间在同一个月之前,此查询运行良好,但如果不是,则失败。

    解决方案 2 完全有效

    为了计算每个用户名和每月的total_time,无论登录时间和注销时间是什么,我们可以使用时间范围方法,将会话范围[login_time, logout_time) 与每月范围[monthly_start_time, monthly_end_time) 相交:

    WITH monthly_range AS
    (
    SELECT to_char(m.month_start_date, 'YYYY-MM') AS month
         , tsrange(m.month_start_date, m.month_start_date+ interval '1 month' ) AS monthly_range
      FROM
         ( SELECT generate_series(min(date_trunc('month', time)), max(date_trunc('month', time)), '1 month') AS month_start_date
             FROM your_table
         ) AS m
    ), session_range AS
    (
    SELECT username
         , tsrange(min(time ORDER BY auth_event_type), max(time ORDER BY auth_event_type)) AS session_range
      FROM your_table
     GROUP BY username, credential_id
    )
    SELECT s.username
         , m.month
         , sum(upper(p.period) - lower(p.period)) AS total_time
      FROM monthly_range AS m
     INNER JOIN session_range AS s
        ON s.session_range && m.monthly_range
     CROSS JOIN LATERAL (SELECT s.session_range * m.monthly_range AS period) AS p
     GROUP BY s.username, m.month
    

    dbfiddle查看结果

    【讨论】:

      【解决方案2】:

      使用窗口函数lag() 并按credential_id 对它进行分区,按time 排序,例如

      WITH j AS (
        SELECT username, time, age(time, LAG(time) OVER w)
        FROM t
        WINDOW w AS (PARTITION BY credential_id ORDER BY time
                     ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)
      ) 
      SELECT username, to_char(time,'yyyy-mm'),sum(age) FROM j
      GROUP BY 1,2;
      

      注意:在这种情况下,框架 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW 几乎是可选的,但将窗口函数保持尽可能明确被认为是一种好习惯,这样以后您就不需要必须阅读文档才能弄清楚您的查询在做什么。

      演示:db<>fiddle

      【讨论】:

      • username 应该被添加到第一个窗口的 PARTITION BY 子句中。此解决方案可以,直到同一credential_id的登录时间和注销时间不共享同一个月。
      • @Edouard 虽然 OP 没有提到这种行为,但你是完全正确的。我只是考虑了提供的数据。感谢您的观察 +1
      猜你喜欢
      • 2014-01-24
      • 2019-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-06-17
      • 1970-01-01
      相关资源
      最近更新 更多