Search code examples
sqlsql-servert-sqlwindow-functionsgaps-and-islands

Need split the data when condition is getting changed without loop


Below is my table

if exists(select top 1 1 from sys.tables where name='ObjInfo')
drop table ObjInfo

  create table ObjInfo(id int identity
                     ,ObjNumber int
,ObjDate datetime
,ObjConditionId int)
 
  insert into ObjInfo(ObjNumber,ObjDate,ObjConditionId)
  values(1,'2014-01-03',1)
  ,(1,'2014-01-05',1)
  ,(1,'2014-01-06',1)
  ,(1,'2014-01-08',2)
  ,(1,'2014-01-13',1)
  ,(1,'2014-01-15',1)
  ,(1,'2014-01-25',4)
  ,(2,'2014-01-01',1)
  ,(2,'2014-01-05',1)
  ,(2,'2014-01-07',2)
  ,(2,'2014-01-08',2)
  ,(2,'2014-01-12',2)
  ,(2,'2014-01-14',3)
  ,(2,'2014-01-15',4)

My Job is to display ObjectNumbers wise when condition was changed and for which time period. My expected output is as below

ObjNumber ObjConditionId ConditionBeg ConditionEnd
1           1           2014-01-03      2014-01-08
1           2           2014-01-08      2014-01-13
1           1           2014-01-13      2014-01-25
1           4           2014-01-25      getdate()
2           1           2014-01-01      2014-01-07
2           2           2014-01-07      2014-01-14
2           3           2014-01-14      2014-01-15
2           4           2014-01-15      getdate()

I am trying below code but not getting how can I achieve this.

WITH ConditionCTE AS (
  SELECT ObjNumber, ObjConditionId, ObjDate AS ConditionBeg,
         LEAD(ObjDate, 1, GETDATE()) OVER (PARTITION BY ObjNumber ORDER BY ObjDate) AS ConditionEnd
  FROM ObjInfo
)
SELECT ObjNumber, ObjConditionId, ConditionBeg,
       CASE WHEN CAST(ConditionEnd as date)= GETDATE() THEN 'getdate()' ELSE ConditionEnd END AS ConditionEnd
FROM ConditionCTE
ORDER BY ObjNumber, ConditionBeg;

Solution

  • This is a classic gaps-and-island problem; you can use the difference between row numbers to identify groups.

    This gives you for each island, with its start and end dates:

    select ObjNumber, ObjConditionId, 
        min(ObjDate) MinObjDate, 
        max(ObjDate) MaxObjDate
    from (
        select o.*,
            row_number() over(partition by ObjNumber                 order by ObjDate) rn1,
            row_number() over(partition by ObjNumber, ObjConditionId order by ObjDate) rn2
        from ObjInfo o
    ) o
    group by ObjNumber, ObjConditionId, rn1 - rn2
    order by ObjNumber, MinObjDate
    

    If you want to the start of the next island as MaxObjDate instead, as shown in your expected results, we can use lead() on top of this (the three-arguments form allows us to provide a default value):

    select ObjNumber, ObjConditionId, 
        min(ObjDate) MinObjDate, 
        lead(min(ObjDate), 1, getdate()) over(partition by ObjNumber order by min(ObjDate)) MaxObjDate
    from (
        select o.*,
            row_number() over(partition by ObjNumber                 order by ObjDate) rn1,
            row_number() over(partition by ObjNumber, ObjConditionId order by ObjDate) rn2
        from ObjInfo o
    ) o
    group by ObjNumber, ObjConditionId, rn1 - rn2
    order by ObjNumber, MinObjDate
    

    fiddle

    ObjNumber ObjConditionId MinObjDate MaxObjDate
    1 1 2014-01-03 00:00:00.000 2014-01-08 00:00:00.000
    1 2 2014-01-08 00:00:00.000 2014-01-13 00:00:00.000
    1 1 2014-01-13 00:00:00.000 2014-01-25 00:00:00.000
    1 4 2014-01-25 00:00:00.000 2023-06-06 08:46:17.283
    2 1 2014-01-01 00:00:00.000 2014-01-07 00:00:00.000
    2 2 2014-01-07 00:00:00.000 2014-01-14 00:00:00.000
    2 3 2014-01-14 00:00:00.000 2014-01-15 00:00:00.000
    2 4 2014-01-15 00:00:00.000 2023-06-06 08:46:17.283