2012-06-14 6 views
7

Considerando il follwing:SQL: modo migliore per costruire una linea temporale da due tabelle di cronologia

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') 

Sto cercando un modo pulito per costruire un calendario per le relazioni/PCP/Plan membri, in caso di variazione il PCP o il piano per un membro risulterebbe in una riga di inizio/fine separata nel risultato. Ad esempio, se nel corso di pochi anni, un membro cambiato il loro PCP due volte e il loro piano di una volta, ma ogni in date diverse, vorrei vedere qualcosa di simile al seguente:

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 

Come potete vedere, ho bisogno di un separato riga di risultato per ogni periodo di date che comporta una differenza nell'associazione Membro/PCP/Piano. Ho una soluzione in atto, ma è molto contorta con molte dichiarazioni CASE e la logica condizionale nella clausola WHERE. Sto solo pensando che c'è un modo molto più semplice per farlo.

Grazie.

+0

Possiamo vedere il tuo lavoro? –

+0

Puoi postare questa complicata istruzione CASE su [SQLFiddle] (http://sqlfiddle.com/) in modo che possiamo vedere cosa hai fatto? –

+0

Questa è una cosa davvero complicata da fare. Non so se c'è un * modo molto più semplice * per farlo. Quindi probabilmente dovresti pubblicare la tua soluzione e possiamo aiutarti a iniziare da lì – Lamak

risposta

0

Il mio approccio è quello di prendere la combinazione unica di date di inizio per ogni membro come punto di partenza e poi costruire fuori gli altri pezzi della query da lì:

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

Grazie a tutti. Tutti i suggerimenti sono fantastici. Ho taggato questo come risposta, poiché consente di lasciare spazi negli intervalli di attività di Plan/PCP. –

0

Forse questo vi darà alcune idee per un inizio:

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) 

mi sono imbattuto su Oracle con questi risultati:

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 

l'IDE a doveva prima definire i diversi intervalli di date. Questo è nella clausola "WITH". Quindi eseguire una ricerca su ciascun intervallo nelle altre tabelle. Un sacco di presupposti qui per quanto riguarda le gamme di sovrapposizione, ecc. Ma forse un inizio. Ho provato a guardare a questo senza funzioni analitiche poiché potrebbe non esserci un buon supporto per le funzioni analitiche con tsql? Non lo so. Quando si realizzano gli intervalli di date per reali, gli intervalli devono essere creati anche da memberid.

1

Compatibile con T-SQL. Sono d'accordo con Glenn sull'approccio generale.

Un altro suggerimento: se si consente il luppolo tra i periodi della propria attività, questo codice richiederà ulteriori ritocchi. In caso contrario, ritengo che il rinvio del valore di EndDate dal prossimo record di StartDate sarà migliore per avere un comportamento più controllato dal codice. In tal caso, si desidera garantire la regola prima che i dati raggiungano questa query.

Modifica: ho appena appreso Con istruzione e SQL Fiddle dal post di Andriy M. È possibile anche see my answer at SQL Fiddle.

Modifica: corretto il bug segnalato da 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

Non sembra funzionare correttamente quando le due tabelle della cronologia non coprono lo stesso intervallo di date. Ma potrebbe non essere necessario, e in caso contrario sembra funzionare correttamente ed è probabilmente più efficiente dell'espansione delle gamme e quindi di richiuderle come nella mia risposta. –

+0

Andriy, vedo che c'era un bug e ora corretto. La data di inizio dovrebbe partecipare al gruppo della data di fine e viceversa. Altrimenti, come hai detto tu, il periodo limite non verrà rilevato correttamente poiché non esiste una data di fine corrispondente (o una data di inizio). Ho modificato il mio esempio di SQL Fiddle per dimostrare questo caso. – kennethc

+0

Ottimo lavoro, avresti svalutato di nuovo se potessi! –

1

Come forse non la soluzione più efficiente ma almeno semplice e diretto, farei il seguente:

  • 1) ampliare gli intervalli;

  • 2) unire le gamme espanse;

  • 3) raggruppare i risultati.

Questo, naturalmente, presuppone che date solo sono utilizzati (cioè la parte di tempo è 00:00 per ogni StartDate e EndDate in entrambe le tabelle).

Per espandere intervalli di date, io preferisco usare un numbers table, come questo:

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) 

E allo stesso modo per MemberPlans.

per produrre una serie di fila combinato, userei FULL JOIN, anche se si sa in anticipo che entrambe le tabelle coprono esattamente lo stesso periodo di tempo, INNER JOIN farebbe altrettanto bene:

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

Ora avete solo bisogno per raggruppare le righe risultanti e trovare date minime e massime per ogni combinazione di (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 

si noti che se si utilizza INNER JOIN invece di FULL JOIN, non sarà bisogno di tutti quelle espressioni ISNULL(), sarebbe sufficiente selezionare la colonna di una tabella, ad es. pcp.MemberID anziché ISNULL(pcp.MemberID, plans.MemberID) e pcp.Date anziché ISNULL(pcp.Date, plans.Date).

La query completa potrebbe essere simile a questo, allora:

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 

si può provare questa query at SQL Fiddle.

Problemi correlati