【问题标题】:Finding complement date ranges?寻找补充日期范围?
【发布时间】:2010-12-30 14:28:02
【问题描述】:

我有两个表,它们都有列 StartDate 和 EndDate。

我正在尝试返回一个结果集,其中包含一个表 (TableA) 中的所有日期范围,以及另一个 (TableB) 中的所有补充日期范围。

CREATE TABLE [dbo].[TableA](
    [ID] [int] NOT NULL,
    [StartDate] [datetime] NOT NULL,
    [EndDate] [datetime] NOT NULL
)

CREATE TABLE [dbo].[TableB](
    [ID] [int] NOT NULL,
    [StartDate] [datetime] NOT NULL,
    [EndDate] [datetime] NOT NULL
)

INSERT INTO TableA (ID, StartDate, EndDate) VALUES(1, '4/1/2009', '8/1/2009')
INSERT INTO TableA (ID, StartDate, EndDate) VALUES(1, '10/1/2009', '12/1/2009')
INSERT INTO TableB (ID, StartDate, EndDate) VALUES(1, '1/1/2009', '2/1/2010')

INSERT INTO TableA (ID, StartDate, EndDate) VALUES(2, '4/1/2009', '8/1/2009')
INSERT INTO TableB (ID, StartDate, EndDate) VALUES(2, '1/1/2009', '5/1/2009')
INSERT INTO TableB (ID, StartDate, EndDate) VALUES(2, '7/1/2009', '12/1/2009')

三个数据集的预期结果应该是:

(ID = 1)
1/1/2009 - 4/1/2009 (from TableB)
4/1/2009 - 8/1/2009 (from TableA)
8/1/2009 - 10/1/2009 (from TableB)
10/1/2009 - 12/1/2009 (from TableA)
12/1/2009 - 2/1/2010 (from TableB)

(ID = 2)
1/1/2009 - 4/1/2009 (from TableB)
4/1/2009 - 8/1/2009 (from TableA)
8/1/2009 - 12/1/2009 (from TableB)

不能保证日期范围是连续的,我无法对它们在表格之间的重叠方式做出任何假设......在每个表格中,它们可以被假设为不重叠。

我在思考如何将 TableB 中的单个日期范围拆分为多个部分以在 SQL 中找到其中的所有补充“区域”时遇到了问题。

大家有什么建议吗?

【问题讨论】:

  • 在此上下文中定义“补充”。
  • 你有多少行?性能是个问题吗?
  • PS +1 关于在创建脚本中包含测试数据的问题。如果您没有制作创建脚本,我可能不会为这个问题而烦恼,因为自己制作它们太无聊了(即使它不会花费那么长时间)。我希望更多的人花时间来做这件事,以节省其他人的努力。
  • 好的,根据实际情况调整解决方案并将生产数据复制到我的开发环境中。 TableB 中有 77K 条记录,TableA 中有 5K 条记录(我完成项目后会少很多)。视图上的总运行时间:17 秒,产生 96K 行。这还不错,但绝对可以通过相关表中的更好索引来改进。当 TableA 有 100 条记录时,它在不到一秒的时间内运行。
  • 通过适当的索引,运行时间减少到 1 秒。非常好!

标签: sql sql-server sql-server-2005 date-range complement


【解决方案1】:

如果您将其创建为视图,我认为它可以满足您的需求。它使用 CTE,SQL Server 2005 应该支持,但更早版本不支持。

WITH Timestamps AS (
    SELECT Id, StartDate AS Date FROM TableA
    UNION
    SELECT Id, EndDate AS Date FROM TableA
    UNION
    SELECT Id, StartDate AS Date FROM TableB
    UNION
    SELECT Id, EndDate AS Date FROM TableB
), Timestamps2 AS (
    SELECT ROW_NUMBER() OVER (ORDER BY Id, Date) AS RowNumber, * FROM Timestamps
), Timestamps3 AS (
    SELECT T1.ID, T1.Date AS StartDate, T2.Date AS EndDate
    FROM Timestamps2 AS T1 JOIN Timestamps2 AS T2
    ON T1.RowNumber + 1 = T2.RowNumber AND T1.ID = T2.ID
), IntervalsFromB AS (
    SELECT T.ID, T.StartDate, T.EndDate FROM Timestamps3 AS T
    LEFT JOIN TableA AS A
    ON T.StartDate >= A.StartDate AND T.EndDate <= A.EndDate
    WHERE A.StartDate IS NULL)
SELECT * FROM TableA
UNION ALL
SELECT * FROM IntervalsFromB

完整输出(为便于阅读,按 Id、StartDate 排序):

Id  StartDate               EndDate
1   2009-01-01 00:00:00.000 2009-04-01 00:00:00.000
1   2009-04-01 00:00:00.000 2009-08-01 00:00:00.000
1   2009-08-01 00:00:00.000 2009-10-01 00:00:00.000
1   2009-10-01 00:00:00.000 2009-12-01 00:00:00.000
1   2009-12-01 00:00:00.000 2010-02-01 00:00:00.000
2   2009-01-01 00:00:00.000 2009-04-01 00:00:00.000
2   2009-04-01 00:00:00.000 2009-08-01 00:00:00.000
2   2009-08-01 00:00:00.000 2009-12-01 00:00:00.000

实现这一点对我来说非常复杂,所以我想知道是否有人可以看到更简单的方法。我可能错过了一些让这变得更简单的技巧。如果是这样,请告诉我!此外,如果您有很多行,您几乎肯定需要在您的表上使用一些索引才能使其表现良好。其他一些优化可能是可能的 - 我没有尝试尽可能快的性能,只是为了获得正确的结果。

【讨论】:

  • 可以将最后的UNION替换为FULL JOIN,否则查询正确。见这里:explainextended.com/2009/11/09/inverting-date-ranges
  • 不错的链接 - 它几乎解释了我刚刚写的查询。我永远不会通过谷歌搜索找到它。
  • PS,我认为我的最终 UNION ALL 是正确的——这只是我将 TableA 和 (TableB-TableA) 的结果结合起来的部分。我认为您在我的查询中使用 FULL JOIN 所指的部分在 Timestamps3 中(是的,名字不好,对不起),而我在其中执行了“INNER JOIN”。这会用 NULL 杀死两行,但我认为这就是他想要的,所以我认为不需要任何更改。
  • 您也可以在第一个 CTE 中将一些“UNIONs”更改为“UNION ALLs”以获得更好的性能。我认为这是一个小问题,所以我忽略了这一点。
  • 好东西。看起来与示例数据集配合得非常好。我会在几秒钟内尝试一下我的真实数据集。非常感谢马克,我自己永远不会想到这个。
猜你喜欢
  • 2014-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-05
  • 2023-02-07
  • 2014-01-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多