当我第一次在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
这是非常简单的答案,很高兴有人开始。
答案 0 :(得分:1)
如下所示的查询应提供您正在寻找的答案:
SELECT WorkerID,
SUM(DATEDIFF(minute, JobStart, JobEnd)) AS TotalTime
FROM Job
GROUP BY WorkerID
道歉它没有经过测试(我没有SQL Server在这里测试它),但它应该可以解决这个问题。
答案 1 :(得分: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
;
我为这个查询构建了另外两个解决方案,但最好的一个具有大约两倍的性能有一个致命的缺陷(没有正确处理完全封闭的时间范围)。另一个有非常高/坏的统计数据(我知道但必须尝试)。
<强>解释强>
使用每行的所有端点时间,通过复制每个端点时间,然后以每次与下一个可能时间配对的方式进行分组,建立所有可能感兴趣的时间范围的不同列表。将这些范围的经过分钟数与任何实际工人的工作时间相吻合。
答案 2 :(得分:0)
这是一个复杂的查询。说明如下。
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可能更好,特别是如果同一工作人员的两个工作可以同时开始。
重叠标记的计算使用相关子查询(j
子句中的with
。)
然后,对于每条记录,我们返回并找到overlap
值为NULL后的第一条记录。这为给定重叠集中的所有记录提供了分组键。
其余的只是汇总结果,首先是workerId
/组级别,然后是workerId
级别,以获得最终结果。
我没有运行此SQL,因此可能存在语法错误。