查找处理多个作业/订单的总时间,每个工人和作业/订单的重叠/重叠时间
Posted
技术标签:
【中文标题】查找处理多个作业/订单的总时间,每个工人和作业/订单的重叠/重叠时间【英文标题】:Find total time worked with multiple jobs / orders with overlap / overlapping times on each worker and job / order 【发布时间】:2013-03-27 23:24:32 【问题描述】:当我第一次进入 sql 世界时,我夜以继日地搜索这个问题的答案。找不到与我的需求类似的任何东西,所以我决定提出并回答我自己的问题,以防其他人像我一样需要帮助。
这是我拥有的数据的一个示例。为简单起见,这一切都来自 Job 表。每个 JobID 都有自己的开始和结束时间,基本上是随机的,可以重叠、有间隙、与其他作业同时开始和结束等。
--Available--
JobID WorkerID JobStart JobEnd
1 25 '2012-11-17 16:00' '2012-11-17 17:00'
2 25 '2012-11-18 16:00' '2012-11-18 16:50'
3 25 '2012-11-19 18:00' '2012-11-19 18:30'
4 25 '2012-11-19 17:30' '2012-11-19 18:10'
5 26 '2012-11-18 16:00' '2012-11-18 17:10'
6 26 '2012-11-19 16:00' '2012-11-19 16:50'
我希望查询结果显示的是:
WorkerID TotalTime(in Mins)
25 170
26 120
编辑:忘了提到重叠需要被忽略。基本上,这应该像对待小时工而不是承包商一样对待这些工人及其工作。就像我工作了两个 jobID 并从下午 12:00 到 12:30 开始和完成它们一样,作为员工,我只能获得 30 分钟的报酬,而承包商可能会获得 60 分钟的报酬,因为他们的工作是单独对待的,而且每份工作获得报酬。此查询的目的是分析数据库中与工人相关的工作,并且需要找出该工人是否被视为雇员,他在给定时间内的总工作时间是多少。
EDIT2:7 小时内不让我回答我自己的问题,稍后将其移到那里。
好的,现在回答问题。基本上,我使用临时表在我正在查找的作业的最小和最大日期时间之间构建每一分钟。
IF OBJECT_ID('tempdb..#time') IS NOT NULL
BEGIN
drop table #time
END
DECLARE @FromDate AS DATETIME,
@ToDate AS DATETIME,
@Current AS DATETIME
SET @FromDate = '2012-11-17 16:00'
SET @ToDate = '2012-11-19 18:30'
create table #time (cte_start_date datetime)
set @current = @FromDate
while (@current < @ToDate)
begin
insert into #time (cte_start_date)
values (@current)
set @current = DATEADD(n, 1, @current)
end
现在我的所有分钟都在临时表中。现在我需要将所有 Job 表信息加入其中,并一次性选择出我需要的内容。
SELECT J.WorkerID
,COUNT(DISTINCT t.cte_start_date) AS TotalTime
FROM #time AS t
INNER JOIN Job AS J ON t.cte_start_date >= J.JobStart AND t.cte_start_date < J.JobEnd --Thanks ErikE
GROUP BY J.WorkerID --Thanks Martin Parkin
drop table #time
这是一个非常简单的答案,很适合让别人开始。
【参考方案1】:此查询也可以完成这项工作。它的性能非常好(虽然执行计划看起来不太好,但实际的 CPU 和 IO 胜过许多其他查询)。
See it working in a Sql Fiddle.
WITH Times AS (
SELECT DISTINCT
H.WorkerID,
T.Boundary
FROM
dbo.JobHistory H
CROSS APPLY (VALUES (H.JobStart), (H.JobEnd)) T (Boundary)
), Groups AS (
SELECT
WorkerID,
T.Boundary,
Grp = Row_Number() OVER (PARTITION BY T.WorkerID ORDER BY T.Boundary) / 2
FROM
Times T
CROSS JOIN (VALUES (1), (1)) X (Dup)
), Boundaries AS (
SELECT
G.WorkerID,
TimeStart = Min(Boundary),
TimeEnd = Max(Boundary)
FROM
Groups G
GROUP BY
G.WorkerID,
G.Grp
HAVING
Count(*) = 2
)
SELECT
B.WorkerID,
WorkedMinutes = Sum(DateDiff(minute, 0, B.TimeEnd - B.TimeStart))
FROM
Boundaries B
WHERE
EXISTS (
SELECT *
FROM dbo.JobHistory H
WHERE
B.WorkerID = H.WorkerID
AND B.TimeStart < H.JobEnd
AND B.TimeEnd > H.JobStart
)
GROUP BY
WorkerID
;
使用WorkerID, JobStart, JobEnd, JobID
上的聚集索引,并使用上面的 7 行示例,为新的工作人员/作业数据提供模板,重复足够多的时间以生成包含 14,336 行的表,以下是性能结果。我已经在页面上包含了其他有效/正确的答案(到目前为止):
Author CPU Elapsed Reads Scans
------ --- ------- ------ -----
Erik 157 166 122 2
Gordon 375 378 106964 53251
我在另一台(速度较慢的)服务器上进行了更详尽的测试(每个查询运行 25 次,每个指标的最佳和最差值被丢弃,其余 23 个值被取平均值)并得到以下结果:
Query CPU Duration Reads Notes
-------- ---- -------- ------ ----------------------------------
Erik 1 215 231 122 query as above
Erik 2 326 379 116 alternate technique with no EXISTS
Gordon 1 578 682 106847 from j
Gordon 2 584 673 106847 from dbo.JobHistory
我认为可以确保改进的替代技术。好吧,它节省了 6 次读取,但消耗了更多的 CPU(这是有道理的)。与其将每个时间片的开始/结束统计信息一直执行到结束,不如仅根据原始数据重新计算要与EXISTS
保持哪些片。可能是少数工作人员的不同配置文件可能会更改不同查询的性能统计信息。
如果有人想尝试,请使用我的小提琴中的 CREATE TABLE
和 INSERT
语句,然后运行 11 次:
INSERT dbo.JobHistory
SELECT
H.JobID + A.MaxJobID,
H.WorkerID + A.WorkerCount,
DateAdd(minute, Elapsed + 45, JobStart),
DateAdd(minute, Elapsed + 45, JobEnd)
FROM
dbo.JobHistory H
CROSS JOIN (
SELECT
MaxJobID = Max(JobID),
WorkerCount = Max(WorkerID) - Min(WorkerID) + 1,
Elapsed = DateDiff(minute, Min(JobStart), Min(JobEnd))
FROM dbo.JobHistory
) A
;
我为此查询构建了另外两个解决方案,但性能大约翻倍的最佳解决方案存在致命缺陷(无法正确处理完全封闭的时间范围)。另一个有非常高/差的统计数据(我知道但必须尝试)。
说明
使用每行中的所有端点时间,通过复制每个端点时间,然后以每次与下一个可能时间配对的方式进行分组,构建所有可能感兴趣的时间范围的不同列表。将这些范围的经过分钟数求和,只要它们与任何实际工人的工作时间一致。
【讨论】:
。 .由于您正在测试性能,您能否对我的查询进行细微更改以查看它是如何工作的?主查询中最里面的from j
真的可以是from jobs
——它不需要计算重叠标志。这可能会在我的查询上节省一些时间,因为每次引用它时 SQL Server 都会运行 CTE。
今晚晚些时候我应该可以做到!
。 .我不相信你的方法可以推广。当组由开始和结束组成时,这是有道理的。但是当一个组是两个开始或两个结束时,我很怀疑。在我看来,您似乎需要保留有关某件事是整个过程的开始还是结束的信息。这只是一种唠叨的感觉;您的版本可能 100% 正确。
测试结果:将from j
改为from jobs
后,执行计划一致,统计数据无一致或显着差异。关于我的方法,您能否修改小提琴以呈现您能想到的最棘手的数据?我很高兴您能揭露我的查询中的一个缺陷,以便我更正它。至于我,我 100% 相信这是正确的。 :) 从逻辑上解决它:我关心的是每个间隔。我计算所有这些,剔除当时没有工作的那些,然后求和。 7:00 - 9:00, 8:00 - 9:00
变为 7:00 - 8:00, 8:00 - 9:00
。它有效。【参考方案2】:
如下查询应该提供您正在寻找的答案:
SELECT WorkerID,
SUM(DATEDIFF(minute, JobStart, JobEnd)) AS TotalTime
FROM Job
GROUP BY WorkerID
抱歉,它未经测试(我没有 SQL Server 在这里测试它),但它应该可以解决问题。
【讨论】:
抱歉,回复的有点快。我忘了提出我的问题,这应该被视为忽略重叠,只是寻找工作的总时间,就好像工作像员工一样完成。就像我做两份工作,他们都在同一天从中午 12:00 到下午 12:30 开始和结束,作为一名员工,我只得到 30 分钟的报酬,而不是 60 分钟。我现在正在编辑我的问题,否则你所拥有的将作为每项工作的总和。 啊,没问题,我再看看能不能回答你修改后的问题:)【参考方案3】:这是一个复杂的查询。解释如下。
with j as (
select j.*,
(select 1
from jobs j2
where j2.workerid = j.workerid and
j2.starttime < j.endtime and
j2.starttime > j.starttime
) as HasOverlap
from jobs j
)
select workerId,
sum(datediff(minute, periodStart, PeriodEnd)) as NumMinutes
from (select workerId, min(startTime) as periodStart, max(endTime) as PeriodEnd
from (select j.*,
(select min(starttime)
from j j2
where j2.workerid = j.workerid and
j2.starttime >= j.starttime and
j2.HasOverlap is null
) as thegroup
from j
) j
group by workerId, thegroup
) j
group by workerId;
理解这种方法的关键是理解“重叠”逻辑。当下一个开始时间在前一个结束时间之前时,一个时间段与下一个时间段重叠。通过为每条记录分配一个重叠标志,我们知道它是否与“下一个”记录重叠。上述逻辑为此使用了开始时间。使用 JobId 可能会更好,尤其是如果同一工作人员的两个作业可以同时开始。
重叠标志的计算使用相关子查询(这是with
子句中的j
)。
然后,对于每条记录,我们返回并找到之后overlap
值为NULL 的第一条记录。这为给定重叠集中的所有记录提供了一个分组键。
然后,剩下的只是聚合结果,首先在workerId
/group 级别,然后在workerId
级别以获得最终结果。
我没有运行这个 SQL,所以它可能有语法错误。
【讨论】:
以上是关于查找处理多个作业/订单的总时间,每个工人和作业/订单的重叠/重叠时间的主要内容,如果未能解决你的问题,请参考以下文章