我正在寻找能够平均时间戳之间的差异,不包括周末和不包括营业时间(仅在08:00:00 - 17:00:00之间)。
我正在尝试使用一个查询来解决这个问题,但是如果不能使用MySQL就可以回退到PHP函数
以下是我用来获取平均时间戳差异的当前函数。
EG。以下查询将返回星期五上午8点至星期一下午5点之间的差异为81小时,它需要返回18小时,因为它应该排除周末和工作日不在办公时间。
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 ;
答案 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,
我有额外的列,因此您可以看到记录的逻辑流程。现在我们有了每个票证(没有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
答案 2 :(得分:0)
构建并填充表
CREATE TABLE BusinessDays (
day DATE NOT NULL,
PRIMARY KEY (day)
) ENGINE=InnoDB;
它将包含所有未来工作日的日期。您可以根据需要删除任何国家法定假日等。 (这可能是此解决方案的一个额外功能。)
您的表格start_dt
和end_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时溢出。