使用 SQL 压缩时间段
Posted
技术标签:
【中文标题】使用 SQL 压缩时间段【英文标题】:Condense Time Periods with SQL 【发布时间】:2016-02-19 05:56:12 【问题描述】:我有一个大型数据集,出于这个问题的目的,它有 3 个字段:
组标识符 从日期 迄今为止在任何给定的行上,From Date
将始终小于 To Date
,但在每个组中,由日期对表示的时间段(无特定顺序)可能重叠、相互包含在另一个中,或者甚至是相同的。
我想最终得到一个查询,该查询将每个组的结果压缩为连续的时间段。例如一个看起来像这样的组:
| Group ID | From Date | To Date |
--------------------------------------
| A | 01/01/2012 | 12/31/2012 |
| A | 12/01/2013 | 11/30/2014 |
| A | 01/01/2015 | 12/31/2015 |
| A | 01/01/2015 | 12/31/2015 |
| A | 02/01/2015 | 03/31/2015 |
| A | 01/01/2013 | 12/31/2013 |
会导致:
| Group ID | From Date | To Date |
--------------------------------------
| A | 01/01/2012 | 11/30/2014 |
| A | 01/01/2015 | 12/31/2015 |
我已经阅读了许多关于日期打包的文章,但我不太清楚如何将其应用于我的数据集。
如何构造一个可以给我这些结果的查询?
【问题讨论】:
您使用的是哪个版本的 SQL Server? 为什么要重新发明***? Itzik Ben-Gan 写了多篇关于包装间隔的文章,例如blogs.solidq.com/en/sqlserver/packing-intervals 【参考方案1】:《Microsoft® SQL Server® 2012 High-Performance T-SQL Using Window Functions》一书中的解决方案
;with C1 as(
select GroupID, FromDate as ts, +1 as type, 1 as sub
from dbo.table_name
union all
select GroupID, dateadd(day, +1, ToDate) as ts, -1 as type, 0 as sub
from dbo.table_name),
C2 as(
select C1.*
, sum(type) over(partition by GroupID order by ts, type desc
rows between unbounded preceding and current row) - sub as cnt
from C1),
C3 as(
select GroupID, ts, floor((row_number() over(partition by GroupID order by ts) - 1) / 2 + 1) as grpnum
from C2
where cnt = 0)
select GroupID, min(ts) as FromDate, dateadd(day, -1, max(ts)) as ToDate
from C3
group by GroupID, grpnum;
创建表:
if object_id('table_name') is not null
drop table table_name
create table table_name(GroupID varchar(100), FromDate datetime,ToDate datetime)
insert into table_name
select 'A', '01/01/2012', '12/31/2012' union all
select 'A', '12/01/2013', '11/30/2014' union all
select 'A', '01/01/2015', '12/31/2015' union all
select 'A', '01/01/2015', '12/31/2015' union all
select 'A', '02/01/2015', '03/31/2015' union all
select 'A', '01/01/2013', '12/31/2013'
【讨论】:
我将对其进行更多测试,但到目前为止,这个答案似乎给出了最好的结果并具有最好的性能。 在提到的书中可以找到非常详细的解释(Chapter 5, Packing Intervals)【参考方案2】:我会使用Calendar
表。这个表只是有几十年的日期列表。
CREATE TABLE [dbo].[Calendar](
[dt] [date] NOT NULL,
CONSTRAINT [PK_Calendar] PRIMARY KEY CLUSTERED
(
[dt] ASC
))
populate such table 有很多方法。
例如,从 1900 年 1 月 1 日开始的 10 万行(约 270 年):
INSERT INTO dbo.Calendar (dt)
SELECT TOP (100000)
DATEADD(day, ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1, '19000101') AS dt
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
OPTION (MAXDOP 1);
一旦你有一个Calendar
表,这里是如何使用它。
每个原始行都与 Calendar
表连接,以返回与 From 和 To 之间的日期一样多的行。
然后删除可能的重复项。
然后通过对两个序列中的行进行编号来进行经典的间隙和岛屿。
然后将找到的岛屿组合在一起以获得新的 From 和 To。
样本数据
我添加了第二组。
DECLARE @T TABLE (GroupID int, FromDate date, ToDate date);
INSERT INTO @T (GroupID, FromDate, ToDate) VALUES
(1, '2012-01-01', '2012-12-31'),
(1, '2013-12-01', '2014-11-30'),
(1, '2015-01-01', '2015-12-31'),
(1, '2015-01-01', '2015-12-31'),
(1, '2015-02-01', '2015-03-31'),
(1, '2013-01-01', '2013-12-31'),
(2, '2012-01-01', '2012-12-31'),
(2, '2013-01-01', '2013-12-31');
查询
WITH
CTE_AllDates
AS
(
SELECT DISTINCT
T.GroupID
,CA.dt
FROM
@T AS T
CROSS APPLY
(
SELECT dbo.Calendar.dt
FROM dbo.Calendar
WHERE
dbo.Calendar.dt >= T.FromDate
AND dbo.Calendar.dt <= T.ToDate
) AS CA
)
,CTE_Sequences
AS
(
SELECT
GroupID
,dt
,ROW_NUMBER() OVER(PARTITION BY GroupID ORDER BY dt) AS Seq1
,DATEDIFF(day, '2001-01-01', dt) AS Seq2
,DATEDIFF(day, '2001-01-01', dt) -
ROW_NUMBER() OVER(PARTITION BY GroupID ORDER BY dt) AS IslandNumber
FROM CTE_AllDates
)
SELECT
GroupID
,MIN(dt) AS NewFromDate
,MAX(dt) AS NewToDate
FROM CTE_Sequences
GROUP BY GroupID, IslandNumber
ORDER BY GroupID, NewFromDate;
结果
+---------+-------------+------------+
| GroupID | NewFromDate | NewToDate |
+---------+-------------+------------+
| 1 | 2012-01-01 | 2014-11-30 |
| 1 | 2015-01-01 | 2015-12-31 |
| 2 | 2012-01-01 | 2013-12-31 |
+---------+-------------+------------+
【讨论】:
【参考方案3】:; with
cte as
(
select *, rn = row_number() over (partition by [Group ID] order by [From Date])
from tbl
),
rcte as
(
select rn, [Group ID], [From Date], [To Date], GrpNo = 1, GrpFrom = [From Date], GrpTo = [To Date]
from cte
where rn = 1
union all
select c.rn, c.[Group ID], c.[From Date], c.[To Date],
GrpNo = case when c.[From Date] between r.GrpFrom and dateadd(day, 1, r.GrpTo)
or c.[To Date] between r.GrpFrom and r.GrpTo
then r.GrpNo
else r.GrpNo + 1
end,
GrpFrom= case when c.[From Date] between r.GrpFrom and dateadd(day, 1, r.GrpTo)
or c.[To Date] between r.GrpFrom and r.GrpTo
then case when c.[From Date] > r.GrpFrom then c.[From Date] else r.GrpFrom end
else c.[From Date]
end,
GrpTo = case when c.[From Date] between r.GrpFrom and dateadd(day, 1, r.GrpTo)
or c.[To Date] between r.GrpFrom and dateadd(day, 1, r.GrpTo)
then case when c.[To Date] > r.GrpTo then c.[To Date] else r.GrpTo end
else c.[To Date]
end
from rcte r
inner join cte c on r.[Group ID] = c.[Group ID]
and r.rn = c.rn - 1
)
select [Group ID], min(GrpFrom), max(GrpTo)
from rcte
group by [Group ID], GrpNo
【讨论】:
现在即使对于给定的样本数据,它也不会返回正确的结果。当Group ID
超过一个时,它会无法正常工作。
尝试将另一个组 ID 添加到表中。例如,这两个额外的行结果不正确:(2, '2012-01-01', '2012-12-31')
和 (2, '2013-01-01', '2013-12-31')
。
修改了查询。错过了分区
这些结果似乎还可以,但它似乎也让除了非常小的数据集之外的所有数据集都窒息。【参考方案4】:
几何方法
这里和elsewhere 我注意到日期打包问题
不要为这个问题提供几何方法。毕竟,
任何范围,包括日期范围,都可以解释为一条线。
那么为什么不将它们转换为 sql 几何类型并利用
geometry::UnionAggregate
合并范围。所以我给了一个刺
在你的帖子中。
代码说明
在“数字”中:
我构建了一个表示序列的表 换成您最喜欢的方式来制作数字表格。 对于联合操作,您永远不需要比 in 你原来的桌子,所以我只是用它作为基础来构建它。在“合并线”中:
我将日期转换为浮点数并使用这些浮点数 创建几何点。 在这个问题中,我们正在 “整数空间”,意味着没有时间考虑,所以 一个范围内与结束日期相隔一天的开始日期 在另一个应该与另一个合并。为了要做 发生合并,我们需要转换为“真实空间”,所以我们 将 1 添加到所有范围的尾部(我们稍后会撤消此操作)。 然后我通过 STUnion 和 STEnvelope 连接这些点。 最后,我通过 UnionAggregate 合并所有这些行。所结果的 'lines' 几何对象可能包含多条线,但如果它们 重叠,它们变成一条线。在外部查询中:
我使用数字 CTE 来提取“行”内的各个行。 我将这些行包起来,确保这些行被存储 仅作为其两个端点。 我读取端点 x 值并将它们转换回它们的时间 表示,确保将它们放回“整数空间”。代码
with
numbers as (
select row_number() over (order by (select null)) i
from @spans -- Where I put your data
),
mergeLines as (
select groupId,
lines = geometry::UnionAggregate(line)
from @spans
cross apply (select
startP = geometry::Point(convert(float,fromDate), 0, 0),
stopP = geometry::Point(convert(float,toDate) + 1, 0, 0)
) pointify
cross apply (select line = startP.STUnion(stopP).STEnvelope()) lineify
group by groupId
)
select groupId, fromDate, toDate
from mergeLines ml
join numbers n on n.i between 1 and ml.lines.STNumGeometries()
cross apply (select line = ml.lines.STGeometryN(i).STEnvelope()) l
cross apply (select
fromDate = convert(datetime, l.line.STPointN(1).STX),
toDate = convert(datetime, l.line.STPointN(3).STX) - 1
) unprepare
order by groupId, fromDate;
【讨论】:
以上是关于使用 SQL 压缩时间段的主要内容,如果未能解决你的问题,请参考以下文章
我可以使用 split linux 命令拆分压缩的 sql 文件吗?如果不是,那么还有啥其他方法可以做吗?
Oracle SQL 查询使用 case when,压缩空字段