使用 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 压缩时间段的主要内容,如果未能解决你的问题,请参考以下文章

使用 tar gzip 或 zip 压缩的文件大小略有相同

我可以使用 split linux 命令拆分压缩的 sql 文件吗?如果不是,那么还有啥其他方法可以做吗?

Oracle SQL 查询使用 case when,压缩空字段

可以压缩 SQL Server 网络流量吗?

如何在 Spark SQL 中使用 snappy 压缩将数据写入配置单元表

使用 RStudio 连接到本地存储的压缩 SQL 数据库并在其上运行查询