MySQL - 时间戳之间的平均差异,不包括周末和非营业时间

时间:2016-03-03 00:05:26

标签: php mysql

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

我正在尝试使用一个查询来解决这个问题,但是如果不能使用MySQL就可以回退到PHP函数

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

EG。以下查询将返回星期五上午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 ;

3 个答案:

答案 0 :(得分:4)

它可能,但非常非常难看使用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;

答案 1 :(得分:0)

好吧,使用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

这构建了一个子表别名&#34; jdays&#34;表示0到10天的一天序列(如果任何单个活动需要超过10天,只需延长限制)。我用-1开始@dayLoop,所以当你加入你的jobs表时(假设实际上它将有超过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

除了创建一些仅代表相关日期的变量,商店营业时间开放/结束的日期/时间(上午8点/下午5点),特定的票证ID开始/结束时间(如果在几天之间延续),还有更新时间(响应时间),如果有关日期是一周工作日,则为标志栏。这只是声明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 ALONE,对于这个SINGLE ID,jDays表的连接基于从创建到关闭的总天数比jDays值(例如0,1,2,3,...)更大。 ...)。为什么要创建多行?因为每一天都需要根据自己的优点进行评估。因此,一次获取一个数据元素,我只计算ON_的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&#39;。现在,我分别为商店开启和关闭时加上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日重复),我们现在想知道票证时间STARTS,更新和结束(关闭)的时间。因此,开始时间是创建时间或日期开始的大部分时间。在我的示例的MODIFIED开始时间中,故障单的START时间实际上是下午1:15时间而不是原始数据的8am,只是为了给出上下文。更新和结束时间基于最短时间。因此,由于更新和关闭是在周末之后的星期一,我们希望在一天(3月4日)的下午5点停止时钟。类似于关闭时间。

现在对于正在处理的每行,我可以将TIMESTAMPDIFF()的START,UPDATE和END时间用于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条记录,如下所示 Sample 4 records for single ticket ID of original data.

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

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

为了调整相应的星期几开始/结束时间,只需根据给定的星期几而不是固定的8和17更新#hours的date_add组件。这也考虑跨越多天,包括周末 所以现在整个事情都被客户的票证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

enter image description here

答案 2 :(得分:0)

构建并填充表

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

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

您的表格start_dtend_dtDATETIME,您希望根据规则计算它们之间的时间。

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

-- 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时溢出。