2010-04-01 2 views
6

Есть ли лучший способ слияния интервалов между перекрывающимися датами?
Решение, которое я придумал, настолько просто, что теперь мне интересно, есть ли у кого-то еще лучшее представление о том, как это можно сделать.Объединить интервалы между точками перекрытия

/***** DATA EXAMPLE *****/ 
DECLARE @T TABLE (d1 DATETIME, d2 DATETIME) 
INSERT INTO @T (d1, d2) 
     SELECT '2010-01-01','2010-03-31' UNION SELECT '2010-04-01','2010-05-31' 
    UNION SELECT '2010-06-15','2010-06-25' UNION SELECT '2010-06-26','2010-07-10' 
    UNION SELECT '2010-08-01','2010-08-05' UNION SELECT '2010-08-01','2010-08-09' 
    UNION SELECT '2010-08-02','2010-08-07' UNION SELECT '2010-08-08','2010-08-08' 
    UNION SELECT '2010-08-09','2010-08-12' UNION SELECT '2010-07-04','2010-08-16' 
    UNION SELECT '2010-11-01','2010-12-31' UNION SELECT '2010-03-01','2010-06-13' 

/***** INTERVAL ANALYSIS *****/ 
WHILE (1=1) BEGIN 
    UPDATE t1 SET t1.d2 = t2.d2 
    FROM @T AS t1 INNER JOIN @T AS t2 ON 
      DATEADD(day, 1, t1.d2) BETWEEN t2.d1 AND t2.d2 
    IF @@ROWCOUNT = 0 BREAK 
END 

/***** RESULT *****/ 
SELECT StartDate = MIN(d1) , EndDate = d2 
FROM @T 
GROUP BY d2 
ORDER BY StartDate, EndDate 

/***** OUTPUT *****/ 
/***** 
StartDate EndDate 
2010-01-01 2010-06-13 
2010-06-15 2010-08-16 
2010-11-01 2010-12-31 
*****/ 
+1

ли интервалы с открытым открытым, закрытым, закрытым, открытым или закрытым закрытым открытым? Это важно, потому что конечные условия немного зависят друг от друга. Для многих целей открытое закрытие (включая первую дату, исключая вторую дату) является лучшим представлением; open-open (включая оба конца) часто имеют в виду люди. –

+0

Джонатан, я думал о случаях, когда дни (дата начала и дата окончания) являются частью периода. – leoinfo

+0

Это можно сделать однопроходным, но это реализация курсора, так что это зависит от размера набора данных. –

ответ

0

В этом решении я создал временную таблицу календаря, которая хранит значение для каждого дня в диапазоне. Этот тип таблицы можно сделать статическим. Кроме того, я сохраняю 400 нечетных дат, начиная с 2009-12-31. Очевидно, что если ваши даты охватывают более широкий диапазон, вам потребуется больше значений.

Кроме того, это решение будет работать только с SQL Server 2005+, поскольку я использую CTE.

With Calendar As 
    (
    Select DateAdd(d, ROW_NUMBER() OVER (ORDER BY s1.object_id), '1900-01-01') As [Date] 
    From sys.columns as s1 
     Cross Join sys.columns as s2 
    ) 
    , StopDates As 
    (
    Select C.[Date] 
    From Calendar As C 
     Left Join @T As T 
      On C.[Date] Between T.d1 And T.d2 
    Where C.[Date] >= (Select Min(T2.d1) From @T As T2) 
     And C.[Date] <= (Select Max(T2.d2) From @T As T2) 
     And T.d1 Is Null 
    ) 
    , StopDatesInUse As 
    (
    Select D1.[Date] 
    From StopDates As D1 
     Left Join StopDates As D2 
      On D1.[Date] = DateAdd(d,1,D2.Date) 
    Where D2.[Date] Is Null 
    ) 
    , DataWithEariestStopDate As 
    (
    Select * 
    , (Select Min(SD2.[Date]) 
     From StopDatesInUse As SD2 
     Where T.d2 < SD2.[Date]) As StopDate 
    From @T As T 
    ) 
Select Min(d1), Max(d2) 
From DataWithEariestStopDate 
Group By StopDate 
Order By Min(d1) 

EDIT Проблема с использованием даты в 2009 году, не имеет ничего общего с окончательным запросом. Проблема в том, что таблица «Календарь» недостаточно велика. Я запустил таблицу календаря в 2009-12-31. Я пересмотрел его с 1900-01-01.

+0

Ваш код объединяет интервалы, которые не должны объединяться.Используя эти начальные интервалы/**/SELECT '2009-01-01', '2009-01-01' UNION SELECT '2009-01-03', '2009-01-03'/**/код возвращает один период: 2009-01-01 - 2009-01-03. В этом случае 2009-01-02 не следует включать в результирующий интервал. – leoinfo

+0

Во-первых, вы должны добавить схему, а именно, D1 = D2. Ни один из данных вашего примера не указывает на это. Во-вторых, если вы ** добавили ** {2010-01-01,2010-01-01}, к вашим существующим данным примера, первый диапазон должен все же быть с 2010-01-01 по 2010-06-13, потому что первая запись в вашем примере охватывает 2010-01-01 - 2010-03-31. В-третьих, если вы вместо ** замените ** первую запись в вашем примере на {2010-01-01, 2010-01-01}, {2010-03-01, 2010-03-01}, результаты моего запроса все еще верны. Выполняя это изменение, первые две записи выходят как {2010-01-01, 2010-01-01}, {2010-03-01, 2010-06-13}. – Thomas

+0

Еще один сценарий, если вы замените все записи только {2010-01-01,2010-01-01}, {2010-03-01,2010-03-01}, вы получите те же две записи. – Thomas

0

Попробуйте

;WITH T1 AS 
(
    SELECT d1, d2, ROW_NUMBER() OVER(ORDER BY (SELECT 0)) AS R 
    FROM @T 
), NUMS AS 
(
    SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 0)) AS R 
    FROM T1 A 
    CROSS JOIN T1 B 
    CROSS JOIN T1 C 
), ONERANGE AS 
(
    SELECT DISTINCT DATEADD(DAY, ROW_NUMBER() OVER(PARTITION BY T1.R ORDER BY (SELECT 0)) - 1, T1.D1) AS ELEMENT 
    FROM T1 
    CROSS JOIN NUMS 
    WHERE NUMS.R <= DATEDIFF(DAY, d1, d2) + 1 
), SEQUENCE AS 
(
    SELECT ELEMENT, DATEDIFF(DAY, '19000101', ELEMENT) - ROW_NUMBER() OVER(ORDER BY ELEMENT) AS rownum 
    FROM ONERANGE 
) 
SELECT MIN(ELEMENT) AS StartDate, MAX(ELEMENT) as EndDate 
FROM SEQUENCE 
GROUP BY rownum 

Основная идея заключается в том, чтобы первый раскатать существующие данные, так что вы получите отдельную строку для каждого дня. Это делается в ONERANGE

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

13

Я искал такое же решение и наткнулся на это сообщение на Combine overlapping datetime to return single overlapping range record.

Существует еще одна нить на Packing Date Intervals.

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


SELECT 
     s1.StartDate, 
     --t1.EndDate 
     MIN(t1.EndDate) AS EndDate 
FROM @T s1 
INNER JOIN @T t1 ON s1.StartDate <= t1.EndDate 
    AND NOT EXISTS(SELECT * FROM @T t2 
       WHERE t1.EndDate >= t2.StartDate AND t1.EndDate < t2.EndDate) 
WHERE NOT EXISTS(SELECT * FROM @T s2 
       WHERE s1.StartDate > s2.StartDate AND s1.StartDate <= s2.EndDate) 
GROUP BY s1.StartDate 
ORDER BY s1.StartDate 

Результат:

StartDate | EndDate 
2010-01-01 | 2010-06-13 
2010-06-15 | 2010-06-25 
2010-06-26 | 2010-08-16 
2010-11-01 | 2010-12-31 
+0

Также был найден другой пример с объяснением того, как этого добиться здесь: http://www.sqlmag.com/blog/puzzled-by-t-sql-blog-15/tsql/packing-date-intervals-136831 – user1045402

+0

Вы можете отредактировать собственный ответ, чтобы добавить дополнительную информацию, просто нажмите ссылку «изменить» внизу вашего ответа. – ForceMagic

+0

Работает отлично и лаконично! – ensisNoctis

Смежные вопросы