2010-04-01 36 views
6

¿Existe una forma mejor de combinar los intervalos de fechas superpuestas?
La solución que se me ocurrió es tan simple que ahora me pregunto si alguien más tiene una mejor idea de cómo se podría hacer esto.Fusionar intervalos de fechas superpuestas

/***** 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

son los intervalos abiertos abierto, cerrado cerrado, abierto-cerrado o cerrado-abierto? Importa porque las condiciones finales varían levemente dependiendo. Para muchos propósitos, open-closed (incluida la primera fecha, excluyendo la segunda fecha) es la mejor representación; abrir-abrir (ambos extremos incluidos) es a menudo lo que las personas tienen en mente. –

+0

Jonathan, estaba pensando en casos cuando los días (fecha de inicio y fecha de finalización) son parte del período. – leoinfo

+0

Es posible hacerlo de una sola pasada, pero es una implementación de cursor, por lo que depende del tamaño del conjunto de datos. –

Respuesta

0

En esta solución, creé una tabla de calendario temporal que almacena un valor para cada día en un rango. Este tipo de tabla se puede hacer estático. Además, solo estoy almacenando 400 fechas extrañas a partir del 2009-12-31. Obviamente, si sus fechas abarcan un rango mayor, necesitaría más valores.

Además, esta solución solo funcionará con SQL Server 2005+ en que estoy usando un 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) 

EDITAR El problema con el uso de fechas en el año 2009 no tiene nada que ver con la consulta final. El problema es que la tabla de calendario no es lo suficientemente grande. Empecé la tabla de calendario en 2009-12-31. Lo he revisado a partir de 1900-01-01.

+0

Su código está fusionando intervalos que no deben fusionarse.Con estos intervalos iniciales/**/SELECT '2009-01-01', '2009-01-01' UNION SELECT '2009-01-03', '2009-01-03'/**/el código devuelve un único período: 2009-01-01 a 2009-01-03. En este caso, 2009-01-02 no debe incluirse en el intervalo resultante. – leoinfo

+0

Primero, debe agregar el esquema y específicamente si D1 = D2. Ninguno de sus datos de ejemplo sugiere eso. En segundo lugar, si ** agrega ** {2010-01-01,2010-01-01} a sus datos de ejemplo existentes, el primer rango debe ser 2010-01-01 a 2010-06-13 porque la primera entrada en su ejemplo cubre 2010-01-01 a 2010-03-31. En tercer lugar, si en su lugar ** reemplaza ** la primera entrada en su ejemplo con {2010-01-01, 2010-01-01}, {2010-03-01, 2010-03-01}, los resultados de mi consulta siguen siendo correctos. Haciendo ese cambio, las dos primeras entradas salen como {2010-01-01, 2010-01-01}, {2010-03-01, 2010-06-13}. – Thomas

+0

Un escenario más, si reemplaza todas las entradas con solo {2010-01-01,2010-01-01}, {2010-03-01,2010-03-01}, obtendrá esas mismas dos entradas. – Thomas

0

probar este

;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 

La idea básica consiste en desenrollar primero los datos existentes, por lo que se obtiene una fila separada para cada día. Esto se hace en ONERANGE

A continuación, identifique la relación entre cómo se incrementan las fechas y la forma en que lo hacen los números de fila. La diferencia permanece constante dentro de un rango/isla existente. Tan pronto como llegue a una nueva isla de datos, la diferencia entre ellos aumenta porque la fecha aumenta en más de 1, mientras que el número de fila aumenta en 1.

13

Estaba buscando la misma solución y encontré esta publicación en Combine overlapping datetime to return single overlapping range record.

Hay otro hilo en Packing Date Intervals.

Probé esto con varios rangos de fechas, incluidos los enumerados aquí, y funciona correctamente todo el tiempo.


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 

El resultado es:

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

También encontró otro ejemplo con explicación sobre cómo lograr esto aquí: http://www.sqlmag.com/blog/puzzled-by-t-sql-blog-15/tsql/packing-date-intervals-136831 – user1045402

+0

Usted puede editar su propia respuesta para agregar más información, simplemente haga clic en el enlace "editar" en la parte inferior de su respuesta. – ForceMagic

+0

¡Funciona perfecto y es conciso! – ensisNoctis

Cuestiones relacionadas