2015-05-18 9 views
4

У меня есть следующие структуры таблицы:Определения смежных интервалов дат

id int -- more like a group id, not unique in the table 
AddedOn datetime -- when the record was added 

Для конкретного id есть самого один рекорд каждый день. Я должен написать запрос, который возвращает смежные (на уровне дня) интервалы даты для каждого id. Ожидаемый результат структура:

id int 
StartDate datetime 
EndDate datetime 

Обратите внимание, что время часть AddedOn доступна, но это не важно.

Чтобы было понятнее, вот некоторые входные данные:

with data as 
(
    select * from 
    (
    values 
    (0, getdate()), --dummy record used to infer column types 

    (1, '20150101'), 
    (1, '20150102'), 
    (1, '20150104'), 
    (1, '20150105'), 
    (1, '20150106'), 

    (2, '20150101'), 
    (2, '20150102'), 
    (2, '20150103'), 
    (2, '20150104'), 
    (2, '20150106'), 
    (2, '20150107'), 

    (3, '20150101'), 
    (3, '20150103'), 
    (3, '20150105'), 
    (3, '20150106'), 
    (3, '20150108'), 
    (3, '20150109'), 
    (3, '20150110') 
) as d(id, AddedOn) 
    where id > 0 -- exclude dummy record 
) 
select * from data 

И ожидаемый результат:

id  StartDate  EndDate 
1  2015-01-01  2015-01-02 
1  2015-01-04  2015-01-06 

2  2015-01-01  2015-01-04 
2  2015-01-06  2015-01-07 

3  2015-01-01  2015-01-01 
3  2015-01-03  2015-01-03 
3  2015-01-05  2015-01-06 
3  2015-01-08  2015-01-10 

Хотя это выглядит как обычная проблема, которую я не мог найти достаточно подобный вопрос , Также я приближаюсь к решению, и я опубликую его, когда (и если) это сработает, но я чувствую, что должен быть более элегантный.

+2

Пробелы и островов: https://www.simple-talk.com/sql/t-sql-programming/the-sql-of-gaps и-острова-в-последовательности/https://technet.microsoft.com/en-us/library/aa175780%28v=sql.80%29.aspx http://blogs.msdn.com/b/samlester/ архив/2012/09/04/tsql-solve-it-your-way-gaps-and-island-with-a-twist.aspx В SQL Server 2012 я бы использовал 'LAG' или' LEAD', но в 2008 году было бы самосоединение. –

ответ

5

Вот ответ без какого-либо причудливого объединения, но просто используя группу by и row_number, которая не только проста, но и более эффективна.

WITH CTE_dayOfYear 
AS 
(
    SELECT id, 
      AddedOn, 
      DATEDIFF(DAY,'20000101',AddedOn) dyID, 
      ROW_NUMBER() OVER (ORDER BY ID,AddedOn) row_num 
    FROM data 
) 

SELECT ID, 
     MIN(AddedOn) StartDate, 
     MAX(AddedOn) EndDate, 
     dyID-row_num AS groupID 
FROM CTE_dayOfYear 
GROUP BY ID,dyID - row_num 
ORDER BY ID,2,3 

Логика в том, что dyID основывается на дате, когда имеются пробелы в то время как row_num не имеет пробелов. Поэтому каждый раз, когда в dyID есть пробел, он меняет разницу между row_num и dyID. Тогда я просто использую эту разницу как мой идентификатор groupID.

+1

Это хорошо .. –

+1

Все решения правильные, и они заслуживают достойного внимания. Однако я принимаю это как ответ, потому что он компактный. – B0Andrew

3

В Sql Server 2008 это немного боли без LEAD и LAG функции:

WITH data 
      AS (SELECT * , 
         ROW_NUMBER() OVER (ORDER BY id, AddedOn) AS rn 
       FROM  (VALUES (0, GETDATE()), --dummy record used to infer column types 
         (1, '20150101'), (1, '20150102'), (1, '20150104'), 
         (1, '20150105'), (1, '20150106'), (2, '20150101'), 
         (2, '20150102'), (2, '20150103'), (2, '20150104'), 
         (2, '20150106'), (2, '20150107'), (3, '20150101'), 
         (3, '20150103'), (3, '20150105'), (3, '20150106'), 
         (3, '20150108'), (3, '20150109'), (3, '20150110')) 
         AS d (id, AddedOn) 
       WHERE id > 0 -- exclude dummy record 
      ), 
     diff 
      AS (SELECT d1.* , 
         CASE WHEN ISNULL(DATEDIFF(dd, d2.AddedOn, d1.AddedOn), 
             1) = 1 THEN 0 
          ELSE 1 
         END AS diff 
       FROM  data d1 
         LEFT JOIN data d2 ON d1.id = d2.id 
              AND d1.rn = d2.rn + 1 
      ), 
     parts 
      AS (SELECT * , 
         (SELECT SUM(diff) 
          FROM  diff d2 
          WHERE  d2.rn <= d1.rn 
         ) AS p 
       FROM  diff d1 
      ) 
    SELECT id , 
      MIN(AddedOn) AS StartDate , 
      MAX(AddedOn) AS EndDate 
    FROM parts 
    GROUP BY id , 
      p 

Выход:

id StartDate    EndDate 
1 2015-01-01 00:00:00.000 2015-01-02 00:00:00.000 
1 2015-01-04 00:00:00.000 2015-01-06 00:00:00.000 
2 2015-01-01 00:00:00.000 2015-01-04 00:00:00.000 
2 2015-01-06 00:00:00.000 2015-01-07 00:00:00.000 
3 2015-01-01 00:00:00.000 2015-01-01 00:00:00.000 
3 2015-01-03 00:00:00.000 2015-01-03 00:00:00.000 
3 2015-01-05 00:00:00.000 2015-01-06 00:00:00.000 
3 2015-01-08 00:00:00.000 2015-01-10 00:00:00.000 

Пошаговое руководство:

дифф Это CTE возвращает данные:

1 2015-01-01 00:00:00.000 1 0 
1 2015-01-02 00:00:00.000 2 0 
1 2015-01-04 00:00:00.000 3 1 
1 2015-01-05 00:00:00.000 4 0 
1 2015-01-06 00:00:00.000 5 0 

Вы присоединяетесь к той же самой таблице, чтобы получить предыдущую строку. Затем вычислить разницу в днях между текущей строкой и предыдущей строки и если результат равен 1 день, то выбрать 0 еще выбрать 1.

части Это CTE выбирает результат из предыдущего шага и суммирует новый столбец (это совокупная сумма сумма всех значений нового столбца от начала до текущей строки), так что вы получаете разделы в группе:.

1 2015-01-01 00:00:00.000 1 0 0 
1 2015-01-02 00:00:00.000 2 0 0 
1 2015-01-04 00:00:00.000 3 1 1 
1 2015-01-05 00:00:00.000 4 0 1 
1 2015-01-06 00:00:00.000 5 0 1 
2 2015-01-01 00:00:00.000 6 0 1 
2 2015-01-02 00:00:00.000 7 0 1 
2 2015-01-03 00:00:00.000 8 0 1 
2 2015-01-04 00:00:00.000 9 0 1 
2 2015-01-06 00:00:00.000 10 1 2 
2 2015-01-07 00:00:00.000 11 0 2 
3 2015-01-01 00:00:00.000 12 0 2 
3 2015-01-03 00:00:00.000 13 1 3 

последний шаг это просто группировка по ID и new column и собирание min и max значения для дат.

2

Я взял "остров Решение # 3 из SQL MVP Deep Див" решения от https://www.simple-talk.com/sql/t-sql-programming/the-sql-of-gaps-and-islands-in-sequences/ и применяюсь к тестовым данным: набор

with 
data as 
(
    select * from 
    (
    values 
    (0, getdate()), --dummy record used to infer column types 

    (1, '20150101'), 
    (1, '20150102'), 
    (1, '20150104'), 
    (1, '20150105'), 
    (1, '20150106'), 

    (2, '20150101'), 
    (2, '20150102'), 
    (2, '20150103'), 
    (2, '20150104'), 
    (2, '20150106'), 
    (2, '20150107'), 

    (3, '20150101'), 
    (3, '20150103'), 
    (3, '20150105'), 
    (3, '20150106'), 
    (3, '20150108'), 
    (3, '20150109'), 
    (3, '20150110') 
    ) as d(id, AddedOn) 
    where id > 0 -- exclude dummy record 
) 
,CTE_Seq 
AS 
(
    SELECT 
     ID 
     ,SeqNo 
     ,SeqNo - ROW_NUMBER() OVER (PARTITION BY ID ORDER BY SeqNo) AS rn 
    FROM 
     data 
     CROSS APPLY 
     (
      SELECT DATEDIFF(day, '20150101', AddedOn) AS SeqNo 
     ) AS CA 
) 
SELECT 
    ID 
    ,DATEADD(day, MIN(SeqNo), '20150101') AS StartDate 
    ,DATEADD(day, MAX(SeqNo), '20150101') AS EndDate 
FROM CTE_Seq 
GROUP BY ID, rn 
ORDER BY ID, StartDate; 

Результата

ID StartDate    EndDate 
1 2015-01-01 00:00:00.000 2015-01-02 00:00:00.000 
1 2015-01-04 00:00:00.000 2015-01-06 00:00:00.000 
2 2015-01-01 00:00:00.000 2015-01-04 00:00:00.000 
2 2015-01-06 00:00:00.000 2015-01-07 00:00:00.000 
3 2015-01-01 00:00:00.000 2015-01-01 00:00:00.000 
3 2015-01-03 00:00:00.000 2015-01-03 00:00:00.000 
3 2015-01-05 00:00:00.000 2015-01-06 00:00:00.000 
3 2015-01-08 00:00:00.000 2015-01-10 00:00:00.000 

Я бы рекомендовал вы должны изучить промежуточные результаты CTE_Seq, чтобы понять, как это работает.Просто разместите

select * from CTE_Seq 

вместо окончательного SELECT ... GROUP BY .... Вы получите этот набор результатов:

ID SeqNo rn 
1 0 -1 
1 1 -1 
1 3 0 
1 4 0 
1 5 0 
2 0 -1 
2 1 -1 
2 2 -1 
2 3 -1 
2 5 0 
2 6 0 
3 0 -1 
3 2 0 
3 4 1 
3 5 1 
3 7 2 
3 8 2 
3 9 2 

Каждой дата преобразуется в порядковом номер по DATEDIFF(day, '20150101', AddedOn). ROW_NUMBER() генерирует набор последовательных чисел без пробелов, поэтому, когда эти числа вычитаются из последовательности с зазорами, разница переходит/изменяется. Разница остается неизменной до следующего зазора, поэтому в финале SELECTGROUP BY ID, rn объединяет все строки с одного и того же острова.

1

Я хотел бы опубликовать свое собственное решение тоже, потому что это еще один подход:

with data as 
(
    ... 
), 
temp as 
(
    select  d.id 
      ,d.AddedOn 
      ,dprev.AddedOn as PrevAddedOn 
      ,dnext.AddedOn as NextAddedOn 
    FROM  data d 
      left JOIN 
      data dprev on dprev.id = d.id 
         and dprev.AddedOn = dateadd(d, -1, d.AddedOn) 
      left JOIN 
      data dnext on dnext.id = d.id 
         and dnext.AddedOn = dateadd(d, 1, d.AddedOn) 
), 
starts AS 
(
    select  id 
      ,AddedOn 
    from  temp 
    where  PrevAddedOn is NULL 
), 
ends as 
(
    select  id 
      ,AddedOn 
    from  temp 
    where  NextAddedon is NULL 
) 
SELECT s.id as id 
     ,s.AddedOn as StartDate 
     ,(select min(e.AddedOn) from ends e where e.id = s.id and e.AddedOn >= s.AddedOn) as EndDate 
from starts s 
2

Вот простое решение, которое не использует аналитику. Я склонен не использовать аналитику, потому что я работаю со многими различными СУБД, и многие из них еще не внедряют их и даже те, у кого есть разные синтаксисы. Я просто привык писать родовой код, когда это возможно.

with 
Data(ID, AddedOn)as(
    select 1, convert(date, '20150101') union all 
    select 1, '20150102' union all 
    select 1, '20150104' union all 
    select 1, '20150105' union all 
    select 1, '20150106' union all 
    select 2, '20150101' union all 
    select 2, '20150102' union all 
    select 2, '20150103' union all 
    select 2, '20150104' union all 
    select 2, '20150106' union all 
    select 2, '20150107' union all 
    select 3, '20150101' union all 
    select 3, '20150103' union all 
    select 3, '20150105' union all 
    select 3, '20150106' union all 
    select 3, '20150108' union all 
    select 3, '20150109' union all 
    select 3, '20150110' 
) 
select d.ID, d.AddedOn StartDate, IsNull(d1.AddedOn, '99991231') EndDate 
from Data d 
left join Data d1 
    on d1.ID = d.ID 
    and d1.AddedOn =(
     select Min(AddedOn) 
     from data 
     where ID = d.ID 
     and AddedOn > d.AddedOn); 

В вашей ситуации я предполагаю, что ID и AddedOn образуют составной PK и поэтому индексируются. Таким образом, запрос будет работать впечатляюще быстро даже на очень больших таблицах.

Кроме того, я использовал внешнее соединение, потому что казалось, что последняя дата AddOn каждого ID должна быть видна в столбце StartDate. Вместо NULL я использовал общее значение MaxDate. NULL может работать так же, как и флаг «это самый последний флаг StartDate».

Вот выход для ID = 1:

ID   StartDate EndDate 
----------- ---------- ---------- 
1   2015-01-01 2015-01-02 
1   2015-01-02 2015-01-04 
1   2015-01-04 2015-01-05 
1   2015-01-05 2015-01-06 
1   2015-01-06 9999-12-31 
Смежные вопросы