【问题标题】:MySQL - Average difference between timestamps, excluding weekends and out of business hoursMySQL - 时间戳之间的平均差异,不包括周末和非工作时间
【发布时间】:2016-03-03 00:05:26
【问题描述】:

我正在寻找平均时间戳之间差异的能力,不包括周末和非工作时间(仅在 08:00:00 - 17:00:00 之间)。

我正在尝试仅使用查询来使其正常工作,但如果无法使用 MySQL 则可以回退到 PHP 函数

下面是我用来获取平均时间戳差异的当前函数。

例如。下面的查询将返回星期五上午 8 点到星期一下午 5 点之间的差为 81 小时,它需要返回 18 小时,因为它应该排除周末和工作日的非办公时间。

SQLFIDDLE LINK

SELECT 
    clients.name, 
    avg(TIMESTAMPDIFF(HOUR, jobs.time_created, jobs.time_updated)) AS average_response, 
    avg(TIMESTAMPDIFF(HOUR, jobs.time_created, jobs.time_closed)) AS average_closure, 
    count(jobs.id) AS ticket_count, 
    SUM(time_total) AS time_spent 
FROM 
    jobs
LEFT JOIN 
    clients ON jobs.client = clients.id 
WHERE 
    jobs.status = 'closed' 
GROUP BY 
    jobs.client

我查看了at other questions,但它们似乎不适用于时间戳,只有日期。

结果

我现在使用下面的存储函数来实现我想要的结果。它将忽略工作时间以外的时间(08:00:00 - 17:00:00)并忽略周末。它基本上只会计算两个时间戳之间的营业时间差。

DROP FUNCTION IF EXISTS BUSINESSHOURSDIFF;
DELIMITER $$
CREATE FUNCTION BUSINESSHOURSDIFF(start_time TIMESTAMP, end_time TIMESTAMP)
RETURNS INT UNSIGNED
BEGIN
IF HOUR(start_time) > 17 THEN SET start_time = CONCAT_WS(' ', DATE(start_time), '17:00:00');
END IF;
IF HOUR(start_time) < 8 THEN SET start_time = CONCAT_WS(' ', DATE(start_time), '08:00:00');
END IF;
IF HOUR(end_time) > 17 THEN SET end_time = CONCAT_WS(' ', DATE(end_time), '17:00:00');
END IF;
IF HOUR(end_time) < 8 THEN SET end_time = CONCAT_WS(' ', DATE(end_time), '08:00:00');
END IF;
RETURN 45 * (DATEDIFF(end_time, start_time) DIV 7) + 
          9 * MID('0123455501234445012333450122234501101234000123450', 
                  7 * WEEKDAY(start_time) + WEEKDAY(end_time) + 1, 1) + 
          TIMESTAMPDIFF(HOUR, DATE(end_time), end_time) - 
          TIMESTAMPDIFF(HOUR, DATE(start_time), start_time);
END $$
DELIMITER ;

【问题讨论】:

  • WHERE HOUR(jobs.time_created) &gt;8 AND HOUR(jobs.time_created) &lt;17 等等 ??
  • @Dagon 这不起作用,它只会查找大于上午 8 点和下午 5 点之前的时间戳。我已经用当前结果和期望结果的示例编辑了问题。
  • 限制你想要的时间。
  • @Strawberry 我添加了一个带有模式、查询和当前结果的 sqlfiddle。预期结果在我的问题中以粗体显示。
  • @daremachine 怎么样?

标签: php mysql


【解决方案1】:

这是可能的,但仅使用 sql 非常难看。但是,如果您可以使用存储函数,那么它看起来也很漂亮。

根据您在问题中链接的 SO 问题,我们知道以下表达式计算两个日期之间的工作日数:

5 * (DATEDIFF(@E, @S) DIV 7) + 
    MID('0123455501234445012333450122234501101234000123450', 
        7 * WEEKDAY(@S) + WEEKDAY(@E) + 1, 1)

如果我们将此表达式乘以 9,即 # 每个工作日的工作时间,我们会得到 business hours diff。添加两个时间戳之间的小时调整给我们最终的表达式,然后我们可以平均

45 * (DATEDIFF(@E, @S) DIV 7) + 
      9 * MID('0123455501234445012333450122234501101234000123450', 
              7 * WEEKDAY(@S) + WEEKDAY(@E) + 1, 1) + 
      TIMESTAMPDIFF(HOUR, DATE(@E), @E) - 
      TIMESTAMPDIFF(HOUR, DATE(@S), @S)

所以,丑陋但有效的查询是:

SELECT 
  clients.name
, AVG(45 * (DATEDIFF(jobs.time_updated, jobs.time_created) DIV 7) + 
          9 * MID('0123455501234445012333450122234501101234000123450', 
                  7 * WEEKDAY(jobs.time_created) + WEEKDAY(jobs.time_updated) + 1, 1) + 
          TIMESTAMPDIFF(HOUR, DATE(jobs.time_updated), jobs.time_updated) - 
          TIMESTAMPDIFF(HOUR, DATE(jobs.time_created), jobs.time_created)) AS average_response
, AVG(45 * (DATEDIFF(jobs.time_closed, jobs.time_created) DIV 7) + 
          9 * MID('0123455501234445012333450122234501101234000123450', 
                  7 * WEEKDAY(jobs.time_created) + WEEKDAY(jobs.time_closed) + 1, 1) + 
          TIMESTAMPDIFF(HOUR, DATE(jobs.time_closed), jobs.time_closed) - 
          TIMESTAMPDIFF(HOUR, DATE(jobs.time_created), jobs.time_created)) AS average_closure
, COUNT(jobs.id) AS ticket_count 
, SUM(time_total) AS time_spent 
FROM jobs
LEFT JOIN clients ON jobs.client = clients.id 
WHERE jobs.status = 'closed' 
GROUP BY jobs.client

更好的选择是创建一个处理business hours diff 逻辑的存储函数。

DROP FUNCTION IF EXISTS BUSINESSHOURSDIFF;
DELIMITER $$    
CREATE FUNCTION BUSINESSHOURSDIFF(start_time TIMESTAMP, end_time TIMESTAMP) 
RETURNS INT UNSIGNED
BEGIN
RETURN 45 * (DATEDIFF(end_time, start_time) DIV 7) + 
          9 * MID('0123455501234445012333450122234501101234000123450', 
                  7 * WEEKDAY(start_time) + WEEKDAY(end_time) + 1, 1) + 
          TIMESTAMPDIFF(HOUR, DATE(end_time), end_time) - 
          TIMESTAMPDIFF(HOUR, DATE(start_time), start_time);
END $$
DELIMITER ;

然后根据需要调用它。

SELECT 
    clients.name
  , avg(BUSINESSHOURSDIFF(jobs.time_created, jobs.time_updated)) AS average_response
  , avg(BUSINESSHOURSDIFF(jobs.time_created, jobs.time_closed)) AS average_closure
  , count(jobs.id) AS ticket_count
  , SUM(time_total) AS time_spent 
FROM jobs
LEFT JOIN clients ON jobs.client = clients.id 
WHERE jobs.status = 'closed' 
GROUP BY jobs.client;

【讨论】:

  • 您省略了第一天和最后一天的营业时间部分(提示:最大(),最小())我认为正确的字符串是:0123455401234434012332340122123401101234000123450
  • 对不起,我没有理解@Strawberry 的意思,请您详细说明。谢谢。您是否暗示time_createdtime_updatedtime_closed 可以在营业时间之外,在这些情况下只需要营业时间差吗?
  • 我是这么理解的——但我可能搞错了
  • @HaleemurAli 很棒的工作,我正在使用 MySQL 中的存储函数。 time_created,time_updated&time_closed确实有可能不在营业时间,难道查询只能计算营业时间差?
  • @HaleemurAli 我刚刚测试了这些示例,一切都按预期工作。它们将导致 0 小时被退回。
【解决方案2】:

好的,使用 MySQL @variables 可能真的会伤到你的脑袋。它们的工作方式类似于内联程序语句,当您通过 := 进行分配时,它们可以用于下一个要查询的 sql 列中,从而简化您的逻辑,而无需一直进行繁重的日期数学运算。

首先,这是整个查询。那我就分解一下……

select
        pq.id,
        pq.client,
        c.name,
        sum( pq.UpdHours ) as ResponseHours,
        sum( pq.dayHours ) as TotHours,
        sum( pq.TimeOnlyOnce ) as TotalTime
    from
(select
        j.id,
        j.client,
        j.time_created, 
        j.time_updated,
        if( jdays.DaySeq = 0, time_total, 0 ) as TimeOnlyOnce,
        @justDay := date_add( date( j.time_created ), interval jdays.DaySeq day ) as JustTheDay,
        @dtS := date_add( @justDay, interval 8 hour ) as StoreOpen,
        @dtE := date_add( @justDay, interval 17 hour ) as StoreClosed,
        @isWkDay := IF( DAYOFWEEK(@justDay) in ( 1, 7 ), 0, 1 ) as IsWeekDay,
        @dtST := greatest( j.time_created, @dtS ) as StartTime,
        @dtUpd := least( j.time_updated, @dtE ) as TimeUpdate,
        @dtET := least( j.time_closed, @dtE ) as EndTime,
        if( @isWkDay, TIMESTAMPDIFF( HOUR, @dtST, @dtUpd ), null ) as UpdHours,
        if( @isWkDay, TIMESTAMPDIFF( HOUR, @dtST, @dtET ), null ) as dayHours,
        jdays.DaySeq
    from
        jobs j
          JOIN ( select @dayLoop := @dayLoop +1 as DaySeq
                   from jobs js,
                      ( select @dayLoop := -1 ) sqlvars
                   limit 10 ) jdays
            ON jdays.DaySeq <= TIMESTAMPDIFF( DAY, j.time_created, j.time_closed),
        ( select 
                @justDay := '2016-01-01',
                @dtS := @justDay,
                @dtE := @justDay,
                @dtST := @justDay,
                @dtET := @justDay,
                @dtUpd := @justDay,
                @isWkDay := 0) sqlvars2
    order by
        j.id,
        j.client,
        jdays.DaySeq) pq
           LEFT JOIN clients c 
                  ON pq.client = c.id 
    group by
        pq.id

首先,我从最里面的查询开始

JOIN ( select @dayLoop := @dayLoop +1 as DaySeq
          from jobs js,
               ( select @dayLoop := -1 ) sqlvars
          limit 10 ) jdays

这会构建一个子表别名“jdays”来表示从 0 到 10 天的日期序列(如果您需要任何单个活动超过 10 天,只需扩展限制)。我以-1 开始@dayLoop,因此当加入您的工作表时(假设实际上它将有超过10 条记录),它将获取10 行,其值分别为0、1、2、--- 9。这防止需要一些关于多少记录来表示给定作业可能运行的总时间的虚假表格,即时执行。

接下来是JOBS表之间的连接,上面的子查询表示多天,是为了创建笛卡尔结果,以及下一部分

( select 
        @justDay := '2016-01-01',
        @dtS := @justDay,
        @dtE := @justDay,
        @dtST := @justDay,
        @dtET := @justDay,
        @dtUpd := @justDay,
        @isWkDay := 0) sqlvars2

除了创建一些变量之外什么也不做(响应时间),如果所讨论的日期是否为工作日,则为标志列。这只是在 sql 语句中声明变量,无需外部声明。

现在,我正在使用所有@variables 的下一级查询。将其视为一次分析每一行,并根据 jDays 别名结果获取笛卡尔结果。

我只想查看您的第二张票证 ID

ID    Client  time_created         time_updated         time_closed          time_total 
6412  106     2016-03-04 08:00:00  2016-03-07 08:00:00  2016-03-07 17:00:00   .25

如果您单独运行此 INNER QUERY,对于此 SINGLE ID,与 jDays 表的连接基于从创建到关闭的总天数大于 jDays 值(例如 0、1、2、3、. ...)。为什么要创建多行?因为每一天都需要根据自己的优点来评估。因此,一次获取一个数据元素,我只计算一次 total_time 记录,以便基于 daySeq = 0 的 IF(),因此在针对不同行拆分时不会多次计算。 (3月4日、5日、6日、7日)

if( jdays.DaySeq = 0, time_total, 0 ) as TimeOnlyOnce,

现在日期。只是为了笑,让我们假设我们的 time_created 实际上是一些中午的值,例如 2016-03-04 13:15:00(下午 1:15)。我想要只是一天剥夺时间部分。 Date(j.time_created) 只返回日期部分。

@justDay := date_add( date( j.time_created ), interval jdays.DaySeq day ) as JustTheDay,

结果为“2016-03-04”。现在,我分别添加 8 小时和 17 小时来表示商店的开店和关店时间,结果如下,以及是否是周末的标志。

@dtS := date_add( @justDay, interval 8 hour ) as StoreOpen,
@dtE := date_add( @justDay, interval 17 hour ) as StoreClosed,
@isWkDay := IF( DAYOFWEEK(@justDay) in ( 1, 7 ), 0, 1 ) as IsWeekDay,

JustTheDay  StoreOpen (8am)     StoreClosed  (5pm)
2016-03-04  2016-03-04 8:00:00  2016-03-04 17:00:00

根据给定日期的这些基线值(并将在 3 月 5 日、6 日和 7 日重复),我们现在想知道票证时间何时开始、何时更新和结束(关闭)。因此,开始时间是创建时间或当天开始时间中的较大者。在根据我的示例修改的开始时间中,票的开始时间实际上是下午 1:15 时间,而不是原始数据的上午 8 点,只是为了提供上下文。更新和结束时间基于 LEAST 时间。因此,由于更新和关闭是在周末之后的星期一,我们希望在一天的下午 5 点(3 月 4 日)停止时钟。关闭时间类似。

所以现在对于正在处理的每行,我可以将这些 START、UPDATE 和 END 时间用于 TIMESTAMPDIFF() 的 THE SINGLE DAY。但如果是周末,请使用 Null,因为没有时间适用于计算。

@dtST := greatest( j.time_created, @dtS ) as StartTime,
@dtUpd := least( j.time_updated, @dtE ) as TimeUpdate,
@dtET := least( j.time_closed, @dtE ) as EndTime,
if( @isWkDay, TIMESTAMPDIFF( HOUR, @dtST, @dtUpd ), null ) as UpdHours,
if( @isWkDay, TIMESTAMPDIFF( HOUR, @dtST, @dtET ), null ) as dayHours,

现在,由于一张票跨越多个日期,我将有 4 条记录如下

我有额外的列,因此您可以查看记录的逻辑流程。现在我们有了每张票(没有 where 子句),内部查询创建了多行,每行代表票的一天。现在您只需将总小时数、通知客户之前的小时数和总时间(每个故障只存在第一个条目)和按工单分组。所以这给出了每张票的总响应、关闭、时间。

我知道您已经检查了一个有效的答案,但希望您也喜欢这个选项 :) 可能更容易理解和剖析。

要调整相应的星期几开始/结束时间,只需根据给定的星期几更新 date_add 组件 # 小时,而不是分别固定的 8 和 17。这也考虑了跨越包括周末在内的多天。
所以现在整个事情都根据客户的票证 ID 完成了

select
      QryPerID.client,
      QryPerID.name,
      avg( QryPerID.ResponseHours ) as AvgResponseHours,
      avg( QryPerID.TotHours ) as AvgTotHours,
      sum( QryPerID.TotalTime ) as TotalTime,
      count(*) as ClientTickets
   from
      ( entire previous query ) QryPerID
   group by
      QryPerID.client,
      QryPerID.name

【讨论】:

    【解决方案3】:

    构建和填充表格

    CREATE TABLE BusinessDays (
        day DATE NOT NULL,
        PRIMARY KEY (day)
    ) ENGINE=InnoDB;
    

    它将包含所有未来工作日的日期。您可以根据需要删除任何国定假日等。 (这可能是此解决方案的一个额外功能。)

    您的表有start_dtend_dt 作为DATETIME,并且您想根据您的规则计算它们之间的时间量。

    以下是为了便于阅读;它可以组合成一个查询速度/紧凑性:

    -- Worry about intervening days:
    SELECT @days := COUNT(*) - 2
        FROM YourTable yt
        JOIN BusinessDays a  ON a.day >= DATE(yt.start_dt)
        JOIN BusinessDays z  ON z.day <= DATE(yt.end_dt);
    
    -- Get hours in first and last days:
    SELECT @secs := TIME_TO_SEC(TIMEDIFF(TIME(start_dt), '08:00:00')) +
                    TIME_TO_SEC(TIMEDIFF('17:00:00', TIME(end_dt)))
        FROM YourTable;
    
    -- Generate answer:
    SELECT @days * 9 + @secs/3600 AS 'Hours';
    

    够简单吗?

    我不想生成诸如 123:30:00 这样的时间类型输出,因为它会在 840 小时时溢出。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-10-16
      • 1970-01-01
      • 2016-07-13
      • 2019-07-14
      • 2020-12-08
      • 1970-01-01
      • 1970-01-01
      • 2019-12-10
      相关资源
      最近更新 更多