0

当我第一次开始在 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

现在我有一个临时表中的所有分钟。现在我需要将所有作业表信息加入其中,并一次性选择我需要的内容。

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

这是一个非常简单的答案,很适合让某人开始。

4

3 回答 3

1

如下查询应提供您正在寻找的答案:

SELECT  WorkerID,
    SUM(DATEDIFF(minute, JobStart, JobEnd)) AS TotalTime
  FROM  Job
  GROUP BY WorkerID

抱歉,它未经测试(我没有 SQL Server 在这里测试它),但它应该可以解决问题。

于 2013-03-27T23:29:18.540 回答
1

此查询也可以完成这项工作。它的性能非常好(虽然执行计划看起来不太好,但实际的 CPU 和 IO 击败了许多其他查询)。

看到它在 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 TABLEandINSERT语句,然后运行 ​​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
;

我为此查询构建了另外两个解决方案,但性能大约翻倍的最佳解决方案有一个致命缺陷(无法正确处理完全封闭的时间范围)。另一个有非常高/差的统计数据(我知道但必须尝试)。

解释

使用每行中的所有端点时间,通过复制每个端点时间,然后以每次与下一个可能时间配对的方式进行分组,构建所有可能感兴趣的时间范围的不同列表。将这些范围的经过分钟数与任何实际工人的工作时间重合。

于 2013-03-28T01:45:38.463 回答
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 可能会更好,特别是如果同一工人的两个作业可以同时开始。

重叠标志的计算使用相关子查询(jwith子句中)。

然后,对于每条记录,我们返回并找到第一个overlap值为 NULL 的记录。这为给定重叠集中的所有记录提供了分组键。

然后,剩下的只是聚合结果,首先在workerId/group 级别,然后在workerId级别以获得最终结果。

我没有运行这个 SQL,所以它可能有语法错误。

于 2013-03-27T23:49:06.367 回答