2012-06-14 13 views
7

Teniendo en cuenta la siguientes aparatos:SQL: mejor manera de construir una línea de tiempo a partir de dos tablas de historial

CREATE TABLE Members (MemberID INT) 
INSERT Members VALUES (1001) 

CREATE TABLE PCPs (PCPID INT) 
INSERT PCPs VALUES (231) 
INSERT PCPs VALUES (327) 
INSERT PCPs VALUES (390) 

CREATE TABLE Plans (PlanID INT) 
INSERT Plans VALUES (555) 
INSERT Plans VALUES (762) 

CREATE TABLE MemberPCP (
    MemberID INT 
    , PCP INT 
    , StartDate DATETIME 
    , EndDate DATETIME) 
INSERT MemberPCP VALUES (1001, 231, '2002-01-01', '2002-06-30') 
INSERT MemberPCP VALUES (1001, 327, '2002-07-01', '2003-05-31') 
INSERT MemberPCP VALUES (1001, 390, '2003-06-01', '2003-12-31') 

CREATE TABLE MemberPlans (
    MemberID INT 
    , PlanID INT 
    , StartDate DATETIME 
    , EndDate DATETIME) 
INSERT MemberPlans VALUES (1001, 555, '2002-01-01', '2003-03-31') 
INSERT MemberPlans VALUES (1001, 762, '2003-04-01', '2003-12-31') 

Estoy buscando una manera limpia para construir una línea de tiempo para las relaciones/PCP/miembro del plan, donde un cambio en ya sea el PCP o el plan para un miembro daría como resultado una fila de inicio/final por separado en el resultado. Por ejemplo, si en unos pocos años, un miembro cambió su PCP dos veces y su plan de una vez, pero cada uno en diferentes fechas, me gustaría ver algo como lo siguiente:

MemberID PCP PlanID StartDate EndDate 
1001  231 555  2002-01-01 2002-06-30 
1001  327 555  2002-07-01 2003-03-31 
1001  327 762  2003-04-01 2003-05-31 
1001  390 762  2003-06-01 2003-12-31 

Como se puede ver, necesito una por separado fila de resultados para cada período de fecha que implique una diferencia en la asociación Miembro/PCP/Plan. Tengo una solución en su lugar, pero es muy compleja con muchas sentencias CASE y lógica condicional en la cláusula WHERE. Solo estoy pensando que hay una manera mucho más simple de hacer esto.

Gracias.

+0

¿Podemos ver su trabajo? –

+0

¿Puedes publicar esta declaración CASE complicada en [SQLFiddle] (http://sqlfiddle.com/) para que podamos ver lo que has hecho? –

+0

Esto es algo realmente complicado de hacer. No sé si hay una * manera mucho más simple * de hacerlo. Entonces, probablemente debería publicar su solución y podemos ayudarlo a comenzar allí – Lamak

Respuesta

0

Mi enfoque es tomar la combinación única de las fechas de inicio para cada miembro como punto de partida y luego construir un vistazo a las otras piezas de la consulta a partir de ahí:

-- 
-- Traverse down a list of 
-- unique Member ID and StartDates 
-- 
-- For each row find the most 
-- recent PCP for that member 
-- which started on or before 
-- the start date of the current 
-- row in the traversal 
-- 
-- For each row find the most 
-- recent PlanID for that member 
-- which started on or before 
-- the start date of the current 
-- row in the traversal 
-- 
-- For each row find the earliest 
-- end date for that member 
-- (from a collection of unique 
-- member end dates) that happened 
-- after the start date of the 
-- current row in the traversal 
-- 
SELECT MemberID, 
    (SELECT TOP 1 PCP 
    FROM MemberPCP 
    WHERE MemberID = s.MemberID 
    AND StartDate <= s.StartDate 
    ORDER BY StartDate DESC 
) AS PCP, 
    (SELECT TOP 1 PlanID 
    FROM MemberPlans 
    WHERE MemberID = s.MemberID 
    AND StartDate <= s.StartDate 
    ORDER BY StartDate DESC 
) AS PlanID, 
    StartDate, 
    (SELECT TOP 1 EndDate 
    FROM (
    SELECT MemberID, EndDate 
    FROM MemberPlans 
    UNION 
    SELECT MemberID, EndDate 
    FROM MemberPCP) e 
    WHERE EndDate >= s.StartDate 
    ORDER BY EndDate 
) AS EndDate 
FROM ( 
    SELECT 
    MemberID, 
    StartDate 
    FROM MemberPlans 
    UNION 
    SELECT 
    MemberID, 
    Startdate 
    FROM MemberPCP 
) s 
ORDER BY StartDate 
+0

Gracias a todos. Todas las sugerencias son geniales Marqué esta como la respuesta, ya que permite brechas en los rangos de actividad de Plan/PCP. –

0

Tal vez esto le dará algunas ideas para una empezar:

SELECT y.memberid, y.pcp, z.planid, x.startdate, x.enddate 
    FROM (
     WITH startdates AS (

      SELECT startdate FROM memberpcp 
      UNION 
      SELECT startdate FROM memberplans 
      UNION 
      SELECT enddate + 1 FROM memberpcp 
      UNION 
      SELECT enddate + 1 FROM memberplans 

      ), enddates AS (
      SELECT enddate FROM memberpcp 
      UNION 
      SELECT enddate FROM memberplans 

     ) 

     SELECT s.startdate, e.enddate 
      FROM startdates s 
       ,enddates e 
      WHERE e.enddate = (SELECT MIN(enddate) 
           FROM enddates 
           WHERE enddate > s.startdate) 
     ) x 
     ,memberpcp y 
     ,memberplans z 

    WHERE (y.startdate, y.enddate) = (SELECT startdate, enddate FROM memberpcp WHERE startdate <= x.startdate AND enddate >= x.enddate) 
    AND (z.startdate, z.enddate) = (SELECT startdate, enddate FROM memberplans WHERE startdate <= x.startdate AND enddate >= x.enddate) 

me encontré en Oracle con estos resultados:

1001 231 555 01-JAN-02 30-JUN-02 
1001 327 555 01-JUL-02 31-MAR-03 
1001 327 762 01-APR-03 31-MAY-03 
1001 390 762 01-JUN-03 31-DEC-03 

el IDE a fue para definir primero los diferentes intervalos de fechas. Eso está en la cláusula "CON". Luego haga una búsqueda en cada rango en las otras tablas. Una gran cantidad de suposiciones aquí con respecto a los rangos superpuestos, etc. Pero tal vez un comienzo. Intenté ver esto sin funciones analíticas, ya que podría no haber un buen soporte para las funciones analíticas con tsql. No lo sé. Al construir los rangos de fechas de verdad, los rangos también deben ser compilados por memberid.

1

Compatible con T-SQL. Estoy de acuerdo con Glenn en el enfoque general.

Otra sugerencia: Si permite saltos entre los períodos en su negocio, este código necesitará más ajustes. De lo contrario, creo que posponer el valor EndDate del StartDate del próximo registro será mejor para tener un comportamiento más controlado de tu código. En ese caso, debe asegurarse de que la regla antes de que los datos lleguen a esta consulta.

Editar: acabo de enterarme de With statement y SQL Fiddle de la publicación de Andriy M. También puedes see my answer at SQL Fiddle.

Editar: Se corrigió el error señalado por Andriy.

WITH StartDates AS (
SELECT MemberId, StartDate FROM MemberPCP UNION 
SELECT MemberId, StartDate FROM MemberPlans UNION 
SELECT MemberId, EndDate + 1 FROM MemberPCP UNION 
SELECT MemberId, EndDate + 1 FROM MemberPlans 
), 
EndDates AS (
SELECT MemberId, EndDate = StartDate - 1 FROM MemberPCP UNION 
SELECT MemberId, StartDate - 1 FROM MemberPlans UNION 
SELECT MemberId, EndDate FROM MemberPCP UNION 
SELECT MemberId, EndDate FROM MemberPlans 
), 
Periods AS (
SELECT s.MemberId, s.StartDate, EndDate = min(e.EndDate) 
    FROM StartDates s 
     INNER JOIN EndDates e 
      ON s.StartDate <= e.EndDate 
      AND s.MemberId = e.MemberId 
GROUP BY s.MemberId, s.StartDate 
) 
SELECT MemberId = p.MemberId, 
     pcp.PCP, pl.PlanId, 
     p.StartDate, p.EndDate 
    FROM Periods p 
     LEFT JOIN MemberPCP pcp 
      -- because of the way we divided period, 
      -- there will be one and only one record that fits this join clause 
      ON p.StartDate >= pcp.StartDate 
      AND p.EndDate <= pcp.EndDate 
      AND p.MemberId = pcp.MemberId 
     LEFT JOIN MemberPlans pl 
      ON p.StartDate >= pl.StartDate 
      AND p.EndDate <= pl.EndDate 
      AND p.MemberId = pl.MemberId 
ORDER BY p.MemberId, p.StartDate 
+0

Parece que no funciona correctamente cuando las dos tablas de historial no cubren el mismo rango de fechas. Pero eso puede no ser necesario, y de lo contrario esto parece funcionar bien y es probablemente más eficiente que expandir los rangos y luego colapsarlos como en mi respuesta. –

+0

Andriy, veo que hubo un error y ahora se corrigió. La fecha de inicio debe participar en el grupo de fecha de finalización y viceversa. De lo contrario, como dijiste, el período límite no se detectará correctamente ya que no hay una fecha de finalización (o fecha de inicio) correspondiente. Cambié mi ejemplo de SQL Fiddle para demostrar ese caso. – kennethc

+0

¡Gran trabajo, habría votado nuevamente si pudiera! –

1

Como tal vez no sea la solución más eficiente, pero al menos sencilla y directa, lo haría el siguiente:

  • 1) ampliar los rangos;

  • 2) une los rangos ampliados;

  • 3) agrupe los resultados.

Esto, por supuesto, supone que sólo se utilizan las fechas (es decir, el tiempo es parte 00:00 para cada StartDate y EndDate en ambas tablas).

Para ampliar los intervalos de fechas, prefiero usar un numbers table, así:

SELECT 
    m.MemberID, 
    m.PCP, 
    Date = DATEADD(DAY, n.Number, m.StartDate) 
FROM MemberPCP m 
    INNER JOIN Numbers n 
    ON n.Number BETWEEN 0 AND DATEDIFF(DAY, m.StartDate, m.EndDate) 

y lo mismo para MemberPlans.

para producir un conjunto de filas combinadas, me gustaría utilizar FULL JOIN, aunque si se sabe de antemano que ambas tablas cubren exactamente el mismo período de tiempo, INNER JOIN podría hacer igual de bien:

SELECT * 
FROM MemberPCPExpanded pcp 
    FULL JOIN MemberPlansExpanded plans 
    ON pcp.MemberID = plans.MemberID AND pcp.Date = plans.Date 

Ahora sólo necesita para agrupar las filas resultantes y encontrar fechas mínimos y máximos para cada combinación de (MemberID, PCP, PlanID):

SELECT 
    MemberID = ISNULL(pcp.MemberID, plans.MemberID),, 
    pcp.PCP, 
    plans.PlanID, 
    StartDate = MIN(ISNULL(pcp.Date, plans.Date)), 
    EndDate = MAX(ISNULL(pcp.Date, plans.Date)) 
FROM MemberPCPExpanded pcp 
    FULL JOIN MemberPlansExpanded plans 
    ON pcp.MemberID = plans.MemberID AND pcp.Date = plans.Date 
GROUP BY 
    ISNULL(pcp.MemberID, plans.MemberID), 
    pcp.PCP, 
    plans.PlanID 

Tenga en cuenta que si se utiliza en lugar de INNER JOINFULL JOIN, no será necesario todo esas expresiones ISNULL(), bastaría con elegir la columna de cada tabla, p. pcp.MemberID en lugar de ISNULL(pcp.MemberID, plans.MemberID) y pcp.Date en lugar de ISNULL(pcp.Date, plans.Date).

La consulta completa podría tener este aspecto a continuación:

WITH MemberPCPExpanded AS (
    SELECT 
    m.MemberID, 
    m.PCP, 
    Date = DATEADD(DAY, n.Number, m.StartDate) 
    FROM MemberPCP m 
    INNER JOIN Numbers n 
     ON n.Number BETWEEN 0 AND DATEDIFF(DAY, m.StartDate, m.EndDate) 
), 
MemberPlansExpanded AS (
    SELECT 
    m.MemberID, 
    m.PlanID, 
    Date = DATEADD(DAY, n.Number, m.StartDate) 
    FROM MemberPlans m 
    INNER JOIN Numbers n 
     ON n.Number BETWEEN 0 AND DATEDIFF(DAY, m.StartDate, m.EndDate) 
) 
SELECT 
    MemberID = ISNULL(pcp.MemberID, plans.MemberID), 
    pcp.PCP, 
    plans.PlanID, 
    StartDate = MIN(ISNULL(pcp.Date, plans.Date)), 
    EndDate = MAX(ISNULL(pcp.Date, plans.Date)) 
FROM MemberPCPExpanded pcp 
    FULL JOIN MemberPlansExpanded plans 
    ON pcp.MemberID = plans.MemberID AND pcp.Date = plans.Date 
GROUP BY 
    ISNULL(pcp.MemberID, plans.MemberID), 
    pcp.PCP, 
    plans.PlanID 
ORDER BY 
    MemberID, 
    StartDate 

puede probar esta consulta at SQL Fiddle.

Cuestiones relacionadas