Search code examples
sqlsql-serversql-server-2014

Divide rows with date in SQL Server 2014


I have a problem with SQL. I have the following table:

declare @t table (START_DATE datetime,
                  END_DATE datetime, 
                  GROSS_SALES_PRICE decimal(10,2)
                 );

insert into @t 
values ('2014-08-06 00:00:00.000', '2014-10-06 23:59:59.000', 29.99),
       ('2014-09-06 00:00:00.000', '2014-09-09 23:59:59.000', 32.99),
       ('2014-09-10 00:00:00.000', '2014-09-30 23:59:59.000', 32.99),
       ('2014-10-07 00:00:00.000', '2049-12-31 23:59:59.000', 34.99)

I would like to separate the dates which overlaps. For example I have in the first row START_DATE 2014-08-06 and END_DATE 2014-10-06. We can see that the dates from the second and the third row are inside this period of time from first row.

So I would like to separate them as follows:

declare @t2 table (START_DATE datetime,
                   END_DATE datetime, 
                   GROSS_SALES_PRICE decimal(10,2)
                  );

insert into @t2 
values ('2014-08-06 00:00:00.000', '2014-09-05 23:59:59.000', 29.99),
       ('2014-09-06 00:00:00.000', '2014-09-09 23:59:59.000', 32.99),
       ('2014-09-10 00:00:00.000', '2014-09-30 23:59:59.000', 32.99),
       ('2014-10-01 00:00:00.000', '2014-10-06 23:59:59.000', 29.99),
       ('2014-10-07 00:00:00.000', '2049-12-31 23:59:59.000', 34.99)

So the second and the third rows remained unchanged. The first row should have new END_DATE. We also have new row. The GROSS_SALES_PRICE should remain as it is in internal period. Thanks for help. I am using SQL Server 2014


Solution

  • A calendar/dates table can simplify this, but we can also use a query to generate a temporary dates table using a common table expression.

    From there, we can solve this as a gaps and islands style problem. Using the dates table and using outer apply() to get the latest values for start_date and gross_sales_price we can identify the groups we want to re-aggregate by using two row_number()s. The first just ordered by date, less the other that is partitioned by the value we have as the latest start_date and ordered by date.

    Then you can dump the results of the common table expression src to a temporary table and do your inserts/deletes using that or you can use merge using src.

    /* -- dates --*/
    declare @fromdate datetime, @thrudate datetime;
    select  @fromdate = min(start_date), @thrudate = max(end_date) from #t;
    ;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
    , dates as (
      select top (datediff(day, @fromdate, @thrudate)+1) 
          [Date]=convert(datetime,dateadd(day,row_number() over(order by (select 1))-1,@fromdate))
        , [End_Date]=convert(datetime,dateadd(millisecond,-3,dateadd(day,row_number() over(order by (select 1)),@fromdate)))
      from n as deka cross join n as hecto cross join n as kilo
                     cross join n as tenK cross join n as hundredK
       order by [Date]
    )
    /* -- islands -- */
    , cte as (
    select 
        start_date = d.date
      , end_date   = d.end_date
      , x.gross_sales_price
      , grp = row_number() over (order by d.date)
            - row_number() over (partition by x.start_date order by d.date)
    from dates d
      outer apply (
        select top 1 l.start_date, l.gross_sales_price
        from #t l
        where d.date >= l.start_date
          and d.date <= l.end_date
        order by l.start_date desc
        ) x
    )
    /* -- aggregated islands -- */
    , src as (
    select 
        start_date = min(start_date)
      , end_date   = max(end_date)  
      , gross_sales_price
    from cte
    group by gross_sales_price, grp
    )
    /* -- merge -- */
    merge #t with (holdlock) as target
    using src as source
      on target.start_date = source.start_date
     and target.end_date   = source.end_date
     and target.gross_sales_price = source.gross_sales_price
    when not matched by target 
      then insert (start_date, end_date, gross_sales_price)
        values (start_date, end_date, gross_sales_price)
    when not matched by source 
      then delete
    output $action, inserted.*, deleted.*;
    /* -- results -- */
    select 
        start_date
      , end_date  
      , gross_sales_price
    from #t 
    order by start_date
    

    rextester demo: http://rextester.com/MFXCQQ90933

    merge output (you do not need to output this, just showing for the demo):

    +---------+---------------------+---------------------+-------------------+---------------------+---------------------+-------------------+
    | $action |     START_DATE      |      END_DATE       | GROSS_SALES_PRICE |     START_DATE      |      END_DATE       | GROSS_SALES_PRICE |
    +---------+---------------------+---------------------+-------------------+---------------------+---------------------+-------------------+
    | INSERT  | 2014-10-01 00:00:00 | 2014-10-06 23:59:59 | 29.99             | NULL                | NULL                | NULL              |
    | INSERT  | 2014-08-06 00:00:00 | 2014-09-05 23:59:59 | 29.99             | NULL                | NULL                | NULL              |
    | DELETE  | NULL                | NULL                | NULL              | 2014-08-06 00:00:00 | 2014-10-06 23:59:59 | 29.99             |
    +---------+---------------------+---------------------+-------------------+---------------------+---------------------+-------------------+
    

    results:

    +-------------------------+-------------------------+-------------------+
    |       start_date        |        end_date         | gross_sales_price |
    +-------------------------+-------------------------+-------------------+
    | 2014-08-06 00:00:00.000 | 2014-09-05 23:59:59.997 | 29.99             |
    | 2014-09-06 00:00:00.000 | 2014-09-09 23:59:59.997 | 32.99             |
    | 2014-09-10 00:00:00.000 | 2014-09-30 23:59:59.997 | 32.99             |
    | 2014-10-01 00:00:00.000 | 2014-10-06 23:59:59.997 | 29.99             |
    | 2014-10-07 00:00:00.000 | 2049-12-31 23:59:59.997 | 34.99             |
    +-------------------------+-------------------------+-------------------+
    

    calendar and numbers tables reference:

    merge reference: