【问题标题】:Flatten/merge overlapping time intervals展平/合并重叠时间间隔
【发布时间】:2014-02-23 11:21:37
【问题描述】:

我有一个包含数百万行的“服务”表。每一行对应于工作人员在给定日期和时间间隔内提供的服务(每一行都有一个唯一的 ID)。在某些情况下,员工可能会在重叠的时间范围内提供服务。我需要编写一个合并重叠时间间隔的查询,并以如下所示的格式返回数据。

我尝试按 StaffID 和 Date 字段进行分组并获取 BeginTime 的最小值和 EndTime 的最大值,但这并没有考虑到不重叠的时间范围。我怎样才能做到这一点?同样,该表包含数百万条记录,因此递归 CTE 方法可能存在性能问题。提前致谢。

服务表

ID    StaffID  Date        BeginTime EndTime
1     101      2014-01-01  08:00     09:00
2     101      2014-01-01  08:30     09:30
3     101      2014-01-01  18:00     20:30
4     101      2014-01-01  19:00     21:00

输出

StaffID Date        BeginTime EndTime
101     2014-01-01  08:00     09:30
101     2014-01-01  18:00     21:00

这是另一个示例数据集,其中包含贡献者提出的查询。 http://sqlfiddle.com/#!6/bfbdc/3

结果集中的前两行应合并为一行 (06:00-08:45) 但它会生成两行 (06:00-08:30 & 06:00-08:45)

【问题讨论】:

  • 只有一天内的次数吗?有没有跨越午夜的时间?
  • 是的,一天内只有几次。时间不会超过午夜。

标签: sql-server sql-server-2008 group-by


【解决方案1】:

我只提出了一个 CTE 查询,因为问题是可能存在一系列重叠时间,例如记录 1 与记录 2 重叠,记录 2 与记录 3 重叠,依此类推。如果没有 CTE 或其他类型的循环等,这很难解决。无论如何,请试一试。

CTE 查询的第一部分获取启动新组的服务,并且与其他一些服务的启动时间不同(我只需要一个启动组的记录)。第二部分获取那些开始一个小组的人,但还有不止一个具有相同的开始时间 - 再次,我只需要其中一个。最后一部分以递归方式建立在起始组之上,采用所有重叠的服务。

这里是SQLFiddle,添加了更多记录以展示不同类型的重叠和重复时间。

我不能使用ServiceID,因为它必须以与BeginTime 相同的方式订购。

;with flat as
(
 select StaffID, ServiceDate, BeginTime, EndTime, BeginTime as groupid 
 from services S1
 where not exists (select * from services S2 
 where S1.StaffID = S2.StaffID 
 and S1.ServiceDate = S2.ServiceDate 
 and S2.BeginTime <= S1.BeginTime and S2.EndTime <> S1.EndTime
 and S2.EndTime > S1.BeginTime)

  union all

  select StaffID, ServiceDate, BeginTime, EndTime, BeginTime as groupid 
  from services S1
 where exists (select * from services S2 
 where S1.StaffID = S2.StaffID 
 and S1.ServiceDate = S2.ServiceDate 
 and S2.BeginTime = S1.BeginTime and S2.EndTime > S1.EndTime)
   and not exists (select * from services S2 
 where S1.StaffID = S2.StaffID 
 and S1.ServiceDate = S2.ServiceDate 
 and S2.BeginTime < S1.BeginTime
 and S2.EndTime > S1.BeginTime)

 union all

 select S.StaffID, S.ServiceDate, S.BeginTime, S.EndTime, flat.groupid 
 from flat
 inner join services S 
 on flat.StaffID = S.StaffID
 and flat.ServiceDate = S.ServiceDate
 and flat.EndTime > S.BeginTime
 and flat.BeginTime < S.BeginTime and flat.EndTime < S.EndTime
)

select StaffID, ServiceDate, MIN(BeginTime) as begintime, MAX(EndTime) as endtime 
from flat
group by StaffID, ServiceDate, groupid
order by StaffID, ServiceDate, begintime, endtime

【讨论】:

  • 感谢 Szymon 提出这个解决方案。 'BeginTime' 值不会在每一天都是唯一的,但是对于“服务”表中的每条记录,还有另一个字段 (ServiceID) 是唯一的。我们可以使用该字段来改进查询。我将测试此解决方案并尽快提供反馈。再次感谢您的宝贵时间!
  • Szymon,这是您对示例数据的查询。结果集中的前两行应合并为一行 (06:00-08:45) 但它会生成两行 (06:00-08:30 & 06:00-08:45) sqlfiddle.com/#!6/bfbdc/3跨度>
  • @Thracian 感谢您准备好您的问题。我更新了我的查询以适应更多情况 - 我希望现在已经涵盖了所有内容。我认为现在应该涵盖各种重叠和重复时间。我在 SQL fiddle 中加入了更多记录来证明这一点。
  • 如何为 MySQL 5.6 或 5.7 重写?
【解决方案2】:

Elsewhere 我已经回答了一个类似的日期打包问题 几何策略。即,我解释了日期范围 作为一条线,并利用geometry::UnionAggregate 合并 范围。

不过,您的问题有两个特点。首先,它调用 对于 sql-server-2008。 geometry::UnionAggregate 不是那么 可用。但是,请在以下位置下载 microsoft 库 https://github.com/microsoft/SQLServerSpatialTools 并加载 它作为 clr 程序集添加到您的实例中,并且您拥有它 可用dbo.GeometryUnionAggregate

但真正让我感兴趣的是 你有几百万行可以使用。所以我认为 我会在这里重复这个策略,但增加了一个技巧 提高它的性能。这种技术会很好用,如果 您有很多相同的 StaffID/日期子集。


首先,让我们建立一个数字表。把这个换成你最喜欢的 方法来做到这一点。

select  i = row_number() over (order by (select null)) 
into    #numbers
from    @services; -- where i put your data

然后将日期转换为浮点数并使用这些浮点数创建 几何点。

这些点然后可以通过 STUnion 和 STEnvelope 变成线。

您的范围现在表示为几何线,通过以下方式合并它们 UnionAggregate。生成的几何对象“线条”可能包含 多行。但是任何重叠的线都会变成一条线。

select      s.StaffID, 
            s.Date,
            linesWKT = geometry::UnionAggregate(line).ToString() 

            -- If you have SQLSpatialTools installed then:
            -- linesWKT = dbo.GeometryUnionAggregate(line).ToString() 

into        #aggregateRangesToGeo
from        @services s
cross apply (select 
                beginTimeF = convert(float, convert(datetime,beginTime)),
                endTimeF = convert(float, convert(datetime,endTime))
            ) prepare
cross apply (select
                beginPt = geometry::Point(beginTimeF, 0, 0),
                endPt = geometry::Point(endTimeF, 0, 0)
            ) pointify
cross apply (select 
                line = beginPt.STUnion(endPt).STEnvelope()
            ) lineify
group by    s.StaffID,
            s.Date;

每个人员 ID/日期组合都有一个“行”对象。但取决于 在您的数据集上,可能有许多相同的“线”对象 在这些组合之间。如果需要工作人员,这很可能是真的 遵循例行程序,并将数据记录到最近的任何地方。

所以得到一个独特的“线条”对象列表。这应该改进 性能。

从中提取“行”内的各个行。将线条包裹起来, 这确保了这些线仅作为它们的端点存储。阅读 端点 x 值并将它们转换回它们的时间表示。

保留 WKT 表示,以便稍后将其重新加入组合。

select      lns.linesWKT,
            beginTime = convert(time, convert(datetime, ap.beginTime)),
            endTime = convert(time, convert(datetime, ap.endTime))
into        #parsedLines
from        (select distinct linesWKT from #aggregateRangesToGeo) lns
cross apply (select 
                lines = geometry::STGeomFromText(linesWKT, 0)
            ) geo
join        #numbers n on n.i between 1 and geo.lines.STNumGeometries()
cross apply (select 
                line = geo.lines.STGeometryN(n.i).STEnvelope()
            ) ln
cross apply (select 
                beginTime = ln.line.STPointN(1).STX,
                endTime = ln.line.STPointN(3).STX
            ) ap;

现在只需将解析后的数据加入到 StaffId/Date 组合中。

select      ar.StaffID,
            ar.Date,
            pl.beginTime, 
            pl.endTime
from        #aggregateRangesToGeo ar
join        #parsedLines pl on ar.linesWKT = pl.linesWKT
order by    ar.StaffID, 
            ar.Date,
            pl.beginTime;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-04-12
    • 2015-05-10
    • 2016-11-22
    • 2013-10-16
    • 1970-01-01
    • 2016-01-24
    • 2011-02-03
    相关资源
    最近更新 更多