MySQL根据状态字段计算多行之间的时间差

bde*_*eta 4 mysql

我正在尝试计算单个用户的总登录时间。我有以下 MySQL 表:

user_id | timelog             | status
------- | ------------------- | ------
472     | 2017-07-18 08:00:00 | login
472     | 2017-07-18 09:00:00 | break start
472     | 2017-07-18 09:30:00 | break end
472     | 2017-07-18 10:00:00 | logout
472     | 2017-07-18 11:00:00 | login
472     | 2017-07-18 14:00:00 | logout
Run Code Online (Sandbox Code Playgroud)

客户端想要计算用户在选定日期内登录所花费的时间。在做一些案例研究时,我能够计算出第一次登录/注销之间的时间:

SELECT
  TIMEDIFF(
    (SELECT timelog FROM qc_user_status_logs WHERE status = 'logout' AND user_id = '472' AND timelog LIKE '2017-07-18%' LIMIT 0,1),
    (SELECT timelog FROM qc_user_status_logs WHERE status = 'login' AND user_id = '472' AND timelog LIKE '2017-07-18%' LIMIT 0,1)
) as loggedInTime
Run Code Online (Sandbox Code Playgroud)

但是,从示例数据中可以看出,用户在一天中可以有多次登录/注销,以及多次休息时间。我将如何仅使用 MySQL 聚合登录时间。我已经使用 PHP 实现了这一点,但是由于服务器性能问题(有很多记录),我必须找出在 MySQL 中计算总时间的方法。

joa*_*olo 8

这不是一个简单的查询,因此,让我们一步一步地进行:

设想

CREATE TABLE qc_user_status_logs
(
    user_id integer, 
    timelog datetime, 
    status  varchar(15)
) ;
INSERT INTO qc_user_status_logs
    (user_id, timelog, status)
VALUES
    -- Your example data
    (472, '2017-07-18 08:00:00', 'login'),
    (472, '2017-07-18 09:00:00', 'break start'),
    (472, '2017-07-18 09:30:00', 'break end'),
    (472, '2017-07-18 10:00:00', 'logout'),
    (472, '2017-07-18 11:00:00', 'login'),
    (472, '2017-07-18 14:00:00', 'logout'),
    -- An extra user
    (532, '2017-07-18 09:00:00', 'login'),
    (532, '2017-07-18 09:30:00', 'logout'),
    -- And another entry for a user that doesn't log out 
    -- (i.e.: it is *now* logged in)
    (654, now() - interval 33 minute, 'login');
Run Code Online (Sandbox Code Playgroud)

第1步

对于每次登录,通过子查询找到相应的注销(在 MariaDB 中,您将使用窗口函数)

SELECT
    user_id, 
    timelog AS login_time, 
    coalesce(
      (SELECT timelog 
         FROM qc_user_status_logs t_out 
        WHERE     t_out.user_id = t_in.user_id 
              AND t_out.timelog >= t_in.timelog 
              AND t_out.status = 'logout'
      ORDER BY timelog 
        LIMIT 1
      ), 
      now()
    ) AS logout_time
FROM
    qc_user_status_logs t_in
WHERE
    status = 'login'
ORDER BY
    user_id, timelog ;
Run Code Online (Sandbox Code Playgroud)
用户 ID | 登录时间 | 登出时间        
------: | :------------------ | :------------------
    第472话 2017-07-18 08:00:00 | 2017-07-18 10:00:00
    第472话 2017-07-18 11:00:00 | 2017-07-18 14:00:00
    第532话 2017-07-18 09:00:00 | 2017-07-18 09:30:00
    第654话 2017-07-21 23:38:53 | 2017-07-22 00:11:53

第2步

将“登录/注销”时间转换为“登录”间隔。最好的方法是将时间转换为 unix_times 并减去。结果将是登录和注销之间的秒数:

SELECT
    user_id, 
    login_time, 
    logout_time, 
    timediff(logout_time, login_time) AS logged_in_time,
    unix_timestamp(logout_time) - unix_timestamp(login_time) AS seconds_logged_in_time
FROM
    (SELECT
        user_id, 
        timelog AS login_time, 
        coalesce(
          (SELECT timelog 
             FROM qc_user_status_logs t_out 
            WHERE     t_out.user_id = t_in.user_id 
                  AND t_out.timelog >= t_in.timelog 
                  AND t_out.status = 'logout'
          ORDER BY timelog 
            LIMIT 1
          ), 
          now()
        ) AS logout_time
    FROM
        qc_user_status_logs t_in
    WHERE
        status = 'login'
    ) AS q1
ORDER BY
    user_id, login_time ;
Run Code Online (Sandbox Code Playgroud)
用户 ID | 登录时间 | 登出时间 | 登录时间 | seconds_logged_in_time
------: | :------------------ | :------------------ | :------------- | ---------------------:
    第472话 2017-07-18 08:00:00 | 2017-07-18 10:00:00 | 02:00:00 | 7200
    第472话 2017-07-18 11:00:00 | 2017-07-18 14:00:00 | 03:00:00 | 10800
    第532话 2017-07-18 09:00:00 | 2017-07-18 09:30:00 | 00:30:00 | 1800
    第654话 2017-07-21 23:38:53 | 2017-07-22 00:11:53 | 00:33:00 | 1980年

第 3 步

从上一个查询,聚合(添加)登录间隔,按用户分组

SELECT
    user_id, 
    sum(unix_timestamp(logout_time) - unix_timestamp(login_time)) AS total_seconds_logged_in_time
FROM
    (SELECT
        user_id, 
        timelog AS login_time, 
        coalesce(
          (SELECT timelog 
             FROM qc_user_status_logs t_out 
            WHERE     t_out.user_id = t_in.user_id 
                  AND t_out.timelog >= t_in.timelog 
                  AND t_out.status = 'logout'
          ORDER BY timelog 
            LIMIT 1
          ), 
          now()
        ) AS logout_time
    FROM
        qc_user_status_logs t_in
    WHERE
        status = 'login'
    ) AS q1
GROUP BY
    user_id
ORDER BY
    user_id ;
Run Code Online (Sandbox Code Playgroud)
用户 ID | total_seconds_logged_in_time
------: | ---------------------------:
    第472话 18000
    第532话 1800
    第654话 1980年

第四步

我们在休息时做同样的事情

SELECT
    user_id, 
    sum(unix_timestamp(logout_time) - unix_timestamp(login_time)) AS total_seconds_break_time
FROM
    (SELECT
        user_id, 
        timelog AS login_time, 
        coalesce(
          (SELECT timelog 
             FROM qc_user_status_logs t_out 
            WHERE     t_out.user_id = t_in.user_id 
                  AND t_out.timelog >= t_in.timelog 
                  AND t_out.status = 'break end'
          ORDER BY timelog 
            LIMIT 1
          ), 
          now()
        ) AS logout_time
    FROM
        qc_user_status_logs t_in
    WHERE
        status = 'break start'
    ) AS q1
GROUP BY
    user_id
ORDER BY
    user_id ;
Run Code Online (Sandbox Code Playgroud)
用户 ID | total_seconds_break_time
------: | -----------------------:
    第472话 1800

最后一步:

取 (step 3 query) 和LEFT JOINit with the (step 4 query) ON user_id,这样我们就可以将所有的信息对应user_id起来。

减去total_seconds_break_time(或 0,如果没有任何中断;使用coalesce)。

这会给你最终的结果:

SELECT
    q10.user_id, 
    q10.total_seconds_logged_in_time - 
        coalesce(q20.total_seconds_break_time, 0) AS net_total_seconds_logged_in_time
FROM    
    (SELECT
        user_id, 
        sum(unix_timestamp(logout_time) - unix_timestamp(login_time)) AS total_seconds_logged_in_time
    FROM
        (SELECT
            user_id, 
            timelog AS login_time, 
            coalesce(
              (SELECT timelog 
                 FROM qc_user_status_logs t_out 
                WHERE     t_out.user_id = t_in.user_id 
                      AND t_out.timelog >= t_in.timelog 
                      AND t_out.status = 'logout'
              ORDER BY timelog 
                LIMIT 1
              ), 
              now()
            ) AS logout_time
        FROM
            qc_user_status_logs t_in
        WHERE
            status = 'login'
        ) AS q1
    GROUP BY
        user_id
    ) AS q10
    LEFT JOIN
    (SELECT
        user_id, 
        sum(unix_timestamp(logout_time) - unix_timestamp(login_time)) AS total_seconds_break_time
    FROM
        (SELECT
            user_id, 
            timelog AS login_time, 
            coalesce(
              (SELECT timelog 
                 FROM qc_user_status_logs t_out 
                WHERE     t_out.user_id = t_in.user_id 
                      AND t_out.timelog >= t_in.timelog 
                      AND t_out.status = 'break end'
              ORDER BY timelog 
                LIMIT 1
              ), 
              now()
            ) AS logout_time
        FROM
            qc_user_status_logs t_in
        WHERE
            status = 'break start'
        ) AS q1
    GROUP BY
        user_id
    ) AS q20 ON q20.user_id = q10.user_id
ORDER BY
    q10.user_id ;
Run Code Online (Sandbox Code Playgroud)
用户 ID | net_total_seconds_logged_in_time
------: | -------------------------------:
    第472话 16200
    第532话 1800
    第654话 1980年

可以在这里找到dbfiddle 的所有内容