【问题标题】:Date Range Intersection Splitting in SQLSQL中的日期范围交叉分割
【发布时间】:2010-11-26 17:46:21
【问题描述】:

我有一个 SQL Server 2005 数据库,其中包含一个名为 Memberships 的表。

表架构为:

PersonID int, Surname nvarchar(30), FirstName nvarchar(30), Description nvarchar(100), StartDate datetime, EndDate datetime

我目前正在开发一个网格功能,该功能按人显示会员资格的细分。其中一项要求是在日期范围相交的地方拆分成员资格行。交集必须以姓氏和名字为界,即分裂只发生在具有相同姓氏和名字的成员记录中。

示例表数据:

18 Smith John 扑克俱乐部 01/01/2009 NULL
18 史密斯约翰图书馆 05/01/2009 18/01/2009
18 史密斯约翰健身房 2009 年 10 月 1 日 2009 年 1 月 28 日
26 亚当斯简普拉提 03/01/2009 16/02/2009

预期结果集:

18 史密斯约翰扑克俱乐部 01/01/2009 04/01/2009
18 史密斯约翰扑克俱乐部/图书馆 05/01/2009 09/01/2009
18 史密斯约翰扑克俱乐部/图书馆/健身房 10/01/2009 18/01/2009
18 史密斯约翰扑克俱乐部/健身房 19/01/2009 28/01/2009
18 史密斯约翰扑克俱乐部 29/01/2009 无效
26 亚当斯简普拉提 03/01/2009 16/02/2009

有谁知道我如何编写一个存储过程来返回一个具有上述分解的结果集。

【问题讨论】:

  • 您的设计如何处理具有相同名字/姓氏的多个成员?您提供的样本数据指的是三个不同的人,他们叫 John Smith,这并非不可能。
  • 这是一个有效的观点,我已经编辑了我的问题以反映这种可能性。我确实为每个人存储了一个 ID,但是在我写这个问题的时候,我并没有想到重复的名字。为反馈干杯。
  • 有一个 PersonID - 我会完全忽略名称位,直到最终输出 Select
  • 很公平 - 为了便于说明,我将其保留在问题中。

标签: sql-server sql-server-2005 split intersection date-range


【解决方案1】:

您将遇到的问题是随着数据集的增长,使用 TSQL 解决它的解决方案将无法很好地扩展。下面使用一系列动态构建的临时表来解决问题。它使用数字表将每个日期范围条目拆分为相应的日期。这是它无法扩展的地方,主要是因为您的开放范围 NULL 值似乎是无穷大,因此您必须在很远的将来交换一个固定日期,将转换范围限制为可行的时间长度。通过构建包含适当索引的日历表或日历表以优化每一天的呈现,您可能会看到更好的性能。

一旦范围被拆分,描述就会使用 XML PATH 合并,以便范围系列中的每一天都有为其列出的所有描述。按 PersonID 和 Date 的行编号允许使用两个 NOT EXISTS 检查找到每个范围的第一行和最后一行,以查找匹配的 PersonID 和描述集的前一行不存在或下一行不存在的实例t 存在匹配的 PersonID 和 Description 集。

然后使用 ROW_NUMBER 对该结果集重新编号,以便将它们配对以构建最终结果。

/*
SET DATEFORMAT dmy
USE tempdb;
GO
CREATE TABLE Schedule
( PersonID int, 
 Surname nvarchar(30), 
 FirstName nvarchar(30), 
 Description nvarchar(100), 
 StartDate datetime, 
 EndDate datetime)
GO
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Poker Club', '01/01/2009', NULL)
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Library', '05/01/2009', '18/01/2009')
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Gym', '10/01/2009', '28/01/2009')
INSERT INTO Schedule VALUES (26, 'Adams', 'Jane', 'Pilates', '03/01/2009', '16/02/2009')
GO

*/

SELECT 
 PersonID, 
 Description, 
 theDate
INTO #SplitRanges
FROM Schedule, (SELECT DATEADD(dd, number, '01/01/2008') AS theDate
    FROM master..spt_values
    WHERE type = N'P') AS DayTab
WHERE theDate >= StartDate 
  AND theDate <= isnull(EndDate, '31/12/2012')

SELECT 
 ROW_NUMBER() OVER (ORDER BY PersonID, theDate) AS rowid,
 PersonID, 
 theDate, 
 STUFF((
  SELECT '/' + Description
  FROM #SplitRanges AS s
  WHERE s.PersonID = sr.PersonID 
    AND s.theDate = sr.theDate
  FOR XML PATH('')
  ), 1, 1,'') AS Descriptions
INTO #MergedDescriptions
FROM #SplitRanges AS sr
GROUP BY PersonID, theDate


SELECT 
 ROW_NUMBER() OVER (ORDER BY PersonID, theDate) AS ID, 
 *
INTO #InterimResults
FROM
(
 SELECT * 
 FROM #MergedDescriptions AS t1
 WHERE NOT EXISTS 
  (SELECT 1 
   FROM #MergedDescriptions AS t2 
   WHERE t1.PersonID = t2.PersonID 
     AND t1.RowID - 1 = t2.RowID 
     AND t1.Descriptions = t2.Descriptions)
UNION ALL
 SELECT * 
 FROM #MergedDescriptions AS t1
 WHERE NOT EXISTS 
  (SELECT 1 
   FROM #MergedDescriptions AS t2 
   WHERE t1.PersonID = t2.PersonID 
     AND t1.RowID = t2.RowID - 1
     AND t1.Descriptions = t2.Descriptions)
) AS t

SELECT DISTINCT 
 PersonID, 
 Surname, 
 FirstName
INTO #DistinctPerson
FROM Schedule

SELECT 
 t1.PersonID, 
 dp.Surname, 
 dp.FirstName, 
 t1.Descriptions, 
 t1.theDate AS StartDate, 
 CASE 
  WHEN t2.theDate = '31/12/2012' THEN NULL 
  ELSE t2.theDate 
 END AS EndDate
FROM #DistinctPerson AS dp
JOIN #InterimResults AS t1 
 ON t1.PersonID = dp.PersonID
JOIN #InterimResults AS t2 
 ON t2.PersonID = t1.PersonID 
  AND t1.ID + 1 = t2.ID 
  AND t1.Descriptions = t2.Descriptions

DROP TABLE #SplitRanges
DROP TABLE #MergedDescriptions
DROP TABLE #DistinctPerson
DROP TABLE #InterimResults

/*

DROP TABLE Schedule

*/

上述解决方案还将处理附加描述之间的间隙,因此,如果您要为 PersonID 18 添加另一个描述留下间隙:

INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Gym', '10/02/2009', '28/02/2009')

它将适当地填补空白。正如 cmets 中所指出的,您不应在此表中包含名称信息,应将其规范化为可以在最终结果中加入的 Persons 表。我通过使用 SELECT DISTINCT 来模拟另一个表来构建一个临时表来创建该 JOIN。

【讨论】:

    【解决方案2】:

    试试这个

    SET DATEFORMAT dmy
    DECLARE @Membership TABLE( 
        PersonID    int, 
        Surname     nvarchar(16), 
        FirstName   nvarchar(16), 
        Description nvarchar(16), 
        StartDate   datetime, 
        EndDate     datetime)   
    INSERT INTO @Membership VALUES (18, 'Smith', 'John', 'Poker Club', '01/01/2009', NULL)
    INSERT INTO @Membership VALUES (18, 'Smith', 'John','Library', '05/01/2009', '18/01/2009')
    INSERT INTO @Membership VALUES (18, 'Smith', 'John','Gym', '10/01/2009', '28/01/2009')
    INSERT INTO @Membership VALUES (26, 'Adams', 'Jane','Pilates', '03/01/2009', '16/02/2009')
    
    --Program Starts
    declare @enddate datetime
    --Measuring extreme condition when all the enddates are null(i.e. all the memberships for all members are in progress)
    -- in such a case taking any arbitary date e.g. '31/12/2009' here else add 1 more day to the highest enddate
    select @enddate =  case when max(enddate) is null then '31/12/2009' else max(enddate) + 1 end from @Membership
    
    --Fill the null enddates
    ; with fillNullEndDates_cte as
    (
        select
                row_number() over(partition by PersonId order by PersonId) RowNum
                ,PersonId
                ,Surname
                ,FirstName
                ,Description
                ,StartDate
                ,isnull(EndDate,@enddate) EndDate
        from @Membership
    )
    --Generate a date calender
    , generateCalender_cte as
    (
        select 
            1 as CalenderRows
            ,min(startdate) DateValue
        from @Membership
           union all
            select 
                CalenderRows+1
                ,DateValue + 1
            from    generateCalender_cte   
            where   DateValue + 1 <= @enddate
    )
    --Generate Missing Dates based on Membership
    ,datesBasedOnMemberships_cte as
     (
        select 
                t.RowNum
                ,t.PersonId
                ,t.Surname
                ,t.FirstName
                ,t.Description          
                , d.DateValue
                ,d.CalenderRows
        from generateCalender_cte d 
        join fillNullEndDates_cte t ON d.DateValue between t.startdate and t.enddate
    )
    --Generate Dscription Based On Membership Dates
    , descriptionBasedOnMembershipDates_cte as
    (
        select    
            PersonID
            ,Surname
            ,FirstName
            ,stuff((
                select '/' + Description
                from datesBasedOnMemberships_cte d1
                where d1.PersonID = d2.PersonID 
                and d1.DateValue = d2.DateValue
                for xml path('')
            ), 1, 1,'') as Description
            , DateValue
            ,CalenderRows
        from datesBasedOnMemberships_cte d2
        group by PersonID, Surname,FirstName,DateValue,CalenderRows
    )
    --Grouping based on membership dates
    ,groupByMembershipDates_cte as
    (
        select d.*,
        CalenderRows - row_number() over(partition by Description order by PersonID, DateValue) AS  [Group]
        from descriptionBasedOnMembershipDates_cte d
    )
    select PersonId
    ,Surname
    ,FirstName
    ,Description
    ,convert(varchar(10), convert(datetime, min(DateValue)), 103) as StartDate
    ,case when max(DateValue)= @enddate then null else convert(varchar(10), convert(datetime, max(DateValue)), 103) end as EndDate
    from groupByMembershipDates_cte 
    group by [Group],PersonId,Surname,FirstName,Description
    order by PersonId,StartDate
    option(maxrecursion 0)
    

    【讨论】:

      【解决方案3】:

      [只有很多很多年之后。]

      我创建了一个存储过程,它将按单个表中的分区对齐和中断段,然后您可以使用这些对齐的中断将描述转换为使用子查询和 XML PATH 的参差不齐的列。

      查看以下是否有帮助:

      1. 文档:https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.md

      2. 存储过程:https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.sql

      例如,您的调用可能如下所示:

      EXEC dbo.DateSegments_AlignWithinTable
      @tableName = 'tableName',
      @keyFieldList = 'PersonID',
      @nonKeyFieldList = 'Description',
      @effectivveDateFieldName = 'StartDate',
      @terminationDateFieldName = 'EndDate'
      

      您将希望将结果(这是一个表)捕获到另一个表或临时表中(假设它在下面的示例中称为“AlignedDataTable”)。然后,您可以使用子查询进行透视。

      SELECT 
          PersonID, StartDate, EndDate,
      
          SUBSTRING ((SELECT ',' + [Description] FROM AlignedDataTable AS innerTable 
              WHERE 
                  innerTable.PersonID = AlignedDataTable.PersonID
                  AND (innerTable.StartDate = AlignedDataTable.StartDate) 
                  AND (innerTable.EndDate = AlignedDataTable.EndDate)
              ORDER BY id
              FOR XML PATH ('')), 2, 999999999999999) AS IdList
      
       FROM AlignedDataTable
       GROUP BY PersonID, StartDate, EndDate 
       ORDER BY PersonID, StartDate
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-03-29
        • 1970-01-01
        • 1970-01-01
        • 2016-08-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多