如何计算工作时间以外的时间

sup*_*nic 5 mysql

这最初看起来很简单,但事实证明这是一个真正令人头痛的问题.下面是我的表,数据,预期输出和SQL Fiddle,我在哪里解决我的问题.

架构和数据:

CREATE TABLE IF NOT EXISTS `meetings` (
  `id` int(6) unsigned NOT NULL,
  `user_id` int(6) NOT NULL,
  `start_time` DATETIME,
  `end_time` DATETIME,
  PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `meetings` (`id`, `user_id`, `start_time`, `end_time`) VALUES
  ('0', '1', '2018-05-09 04:30:00', '2018-05-09 17:30:00'),
  ('1', '1', '2018-05-10 06:30:00', '2018-05-10 17:30:00'),
  ('2', '1', '2018-05-10 12:30:00', '2018-05-10 16:00:00'),
  ('3', '1', '2018-05-11 17:00:00', '2018-05-12 11:00:00'),
  ('4', '2', '2018-05-11 07:00:00', '2018-05-12 11:00:00'),
  ('5', '2', '2018-05-11 04:30:00', '2018-05-11 15:00:00');
Run Code Online (Sandbox Code Playgroud)

我想从上面得到的是在09:00到17:00之外工作的总时间,按天和user_id分组.所以上述数据的结果如下:

  date        | user_id | overtime_hours
  ---------------------------------------
  2018-05-09  | 1       | 05:00:00
  2018-05-10  | 1       | 03:00:00
  2018-05-11  | 1       | 07:00:00
  2018-05-12  | 1       | 09:00:00
  2018-05-11  | 2       | 13:30:00
  2018-05-12  | 2       | 09:00:00
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,预期结果仅为每天的加班时间和9至5之外的那些时间的用户加总.

下面是我所在的查询和SQL小提琴.当开始和结束跨越午夜(或多个午夜)时,主要问题出现了

SELECT
    SEC_TO_TIME(SUM(TIME_TO_SEC(TIME(end_time)) - TIME_TO_SEC(TIME(start_time)))), user_id, DATE(start_time)
FROM
(SELECT 
    start_time, CASE WHEN TIME(end_time) > '09:00:00' THEN DATE_ADD(DATE(end_time), INTERVAL 9 HOUR) ELSE end_time END AS end_time, user_id
FROM
    meetings
WHERE
    TIME(start_time) < '09:00:00'

UNION

SELECT 
    CASE WHEN TIME(start_time) < '17:00:00' THEN DATE_ADD(DATE(start_time), INTERVAL 17 HOUR) ELSE start_time END AS start_time, end_time, user_id
FROM
    meetings
WHERE
    TIME(end_time) > '17:00:00') AS clamped_times
GROUP BY user_id, DATE(start_time)
Run Code Online (Sandbox Code Playgroud)

http://sqlfiddle.com/#!9/77bc85/1

当小提琴决定剥落时的粘贴:https://pastebin.com/1YvLaKbT

正如您所看到的那样,查询可以在开始时获取简单的加班,并在同一天结束,但不适用于多天的加班.

Mad*_*iya 2

如果会议将持续n天,并且您希望计算特定会议中每天的“工作时间”;它敲响了警钟,我们可以使用数字生成表。

(SELECT 0 AS gap UNION ALL SELECT 1 UNION ALL SELECT 2) AS ngen
Run Code Online (Sandbox Code Playgroud)

我们将使用数字生成器表来考虑从 到 的各个日期的单独start_timeend_time。对于本例,我假设会议时间不太可能超过 2 天。如果它碰巧跨越更多天数,您可以通过UNION ALL SELECT 3 ..ngen 派生表添加更多内容来轻松扩展范围。

在此基础上,我们将确定“开始时间”和“结束时间”,以考虑正在进行的会议中的特定“工作日期”。user_id此计算是在派生表中针对“工作日期”分组进行的。

之后,我们可以SUM()使用一些数学方法来增加用户每天的“工作时间”。请查找下面的查询。我已经添加了大量的评论;如果还有什么不清楚的地方请告诉我。


DB Fiddle 上的演示

查询#1

SELECT 
  dt.user_id, 
  dt.wd AS date, 

  SEC_TO_TIME(SUM(

      CASE 
        /*When both start & end times are less than 9am OR more than 5pm*/
        WHEN (st < TIME_TO_SEC('09:00:00') AND et < TIME_TO_SEC('09:00:00')) OR 
             (st > TIME_TO_SEC('17:00:00') AND et > TIME_TO_SEC('17:00:00'))
        THEN et - st  /* straightforward difference between the two times */

        /* atleast one of the times is in 9am-5pm block, OR, 
           start < 9 am and end > 5pm.
           Math of this can be worked out based on signum function */
        ELSE GREATEST(0, TIME_TO_SEC('09:00:00') - st) + 
             GREATEST(0, et - TIME_TO_SEC('17:00:00'))

      END
  )) AS working_hours  

FROM 
(

 SELECT 
   m.user_id, 

   /* Specific work date */
   DATE(m.start_time) + INTERVAL ngen.gap DAY AS wd, 

   /* Start time to consider for this work date */
   /* If the work date is on the same date as the actual start time
      we consider this time */
   CASE WHEN DATE(m.start_time) + INTERVAL ngen.gap DAY = DATE(m.start_time) 
             THEN TIME_TO_SEC(TIME(m.start_time))

        /* We are on the days after the start day */
        ELSE 0  /* 0 seconds (start of the day) */
   END AS st, 

   /* End time to consider for this work date */
   /* If the work date is on the same date as the actual end time
      we consider this time */
   CASE WHEN DATE(m.start_time) + INTERVAL ngen.gap DAY = DATE(m.end_time) 
             THEN TIME_TO_SEC(TIME(m.end_time)) 

        /* More days to come still for this meeting, 
           we consider the end of this day as end time */
        ELSE 86400  /* 24 hours * 3600 seconds (end of the day) */
   END AS et

 FROM meetings AS m 
 JOIN (SELECT 0 AS gap UNION ALL SELECT 1 UNION ALL SELECT 2) AS ngen
   ON DATE(start_time) + INTERVAL ngen.gap DAY <= DATE(end_time)

) AS dt 
GROUP BY dt.user_id, dt.wd;
Run Code Online (Sandbox Code Playgroud)

结果

| user_id | date       | working_hours |
| ------- | ---------- | ------------- |
| 1       | 2018-05-09 | 05:00:00      |
| 1       | 2018-05-10 | 03:00:00      |
| 1       | 2018-05-11 | 07:00:00      |
| 1       | 2018-05-12 | 09:00:00      |
| 2       | 2018-05-11 | 13:30:00      |
| 2       | 2018-05-12 | 09:00:00      |
Run Code Online (Sandbox Code Playgroud)

进一步优化的可能性:

  1. 这个查询可以很容易地摆脱子查询(派生表)的使用。我只是这样写,以便以可理解的方式传达数学和过程。但是,您可以轻松地将这两个SELECT块合并到一个查询中。
  2. 也许,可以在日期/时间函数的使用方面进行更多优化,并进一步简化其中的数学。函数详细信息可参见:https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html
  3. 某些日期计算需要多次执行,例如, DATE(m.start_time) + INTERVAL ngen.gap DAY。为了避免重新计算,我们可以利用用户定义的变量,这也将使查询变得不那么冗长。
  4. 使这个JOIN条件可控制JOIN .. ON DATE(start_time) + INTERVAL ngen.gap DAY <= DATE(end_time)