2012-06-14 14 views
7

Uwzględniając follwing:SQL: Najlepszym sposobem, aby zbudować harmonogram z dwóch tabel historii

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

szukam czystej sposób skonstruować harmonogram relacji państw/PCP/zaplanować, gdzie zmiana albo PCP, albo plan dla członka spowoduje w wyniku oddzielny wiersz początkowy/końcowy. Na przykład, jeśli w ciągu kilku lat, członek zmienili PCP dwukrotnie, a ich plan raz, ale każdy w różnych terminach, chciałbym zobaczyć coś jak następuje:

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 

Jak widać, muszę oddzielny wiersz wyników dla każdego okresu, w którym występuje różnica w powiązaniu Członek/PCP/Plan. Mam rozwiązanie na miejscu, ale jest bardzo skomplikowane z wieloma instrukcjami CASE i logiką warunkową w klauzuli WHERE. Po prostu myślę, że jest o wiele prostszy sposób na zrobienie tego.

Dzięki.

+0

Czy widzimy twoją pracę? –

+0

Czy możesz umieścić tę skomplikowaną instrukcję CASE w [SQLFiddle] (http://sqlfiddle.com/), abyśmy mogli zobaczyć, co zrobiłeś? –

+0

To naprawdę skomplikowana sprawa. Nie wiem, czy jest to * znacznie prostszy sposób *. Więc prawdopodobnie powinieneś opublikować swoje rozwiązanie, a my pomożemy Ci zacząć tam – Lamak

Odpowiedz

0

Moje podejście ma mieć unikalną kombinację dat startowych dla każdego członka jako punkt wyjścia, a następnie zbudować inne kawałki zapytania stamtąd:

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

Dziękuję wszystkim. Wszystkie sugestie są świetne. Oznacziłem to jako odpowiedź, ponieważ pozwala to na luki w zakresach działań Plan/PCP. –

0

Może to daje pewne pomysły na start:

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) 

wpadłem na Oracle z tych wyników:

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 

IDE a było najpierw zdefiniować różne zakresy dat. Jest to w klauzuli "WITH". Następnie wykonaj wyszukiwanie w każdym zakresie w innych tabelach. Wiele założeń dotyczących nakładających się zakresów itp. Ale może początek. Próbowałem patrząc na to bez funkcji analitycznych, ponieważ może nie być dobre wsparcie dla funkcji analitycznych z tsql? Nie wiem Podczas budowania zakresów dat rzeczywistych, zakresy muszą być budowane również przez członków.

1

Kompatybilny z T-SQL. Zgadzam się z Glennem w sprawie ogólnego podejścia.

Inna propozycja: jeśli zezwolisz na przeskok między okresami w Twojej firmie, kod ten będzie wymagać dalszych poprawek. W przeciwnym razie, myślę, że odroczenie wartości EndDate z StartDate następnego rekordu będzie lepsze dla zachowania większej kontroli nad Twoim kodem. W takim przypadku chcesz zapewnić regułę zanim dane dotrą do tego zapytania.

Edycja: właśnie dowiedziałem się o oświadczeniu i SQL Fiddle z posta Andriya M. Możesz również see my answer at SQL Fiddle.

Edytuj: Naprawiono błąd wskazany przez Andriya.

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

Wygląda na to, że nie działa poprawnie, gdy dwie tabele historii nie obejmują tego samego zakresu dat. Ale to może nie być wymagane, a poza tym wydaje się, że działa dobrze i jest prawdopodobnie bardziej wydajne niż rozszerzanie zakresów, a następnie ich wycofywanie, jak w mojej odpowiedzi. –

+0

Andrij, widzę, że był błąd i teraz go poprawiono. Data rozpoczęcia powinna uczestniczyć w grupie daty końcowej i odwrotnie. W przeciwnym razie, jak powiedziałeś, okres krawędzi nie zostanie poprawnie wykryty, ponieważ nie ma odpowiedniej daty zakończenia (lub daty rozpoczęcia). Zmieniłem przykład SQL Fiddle, aby zademonstrować ten przypadek. – kennethc

+0

Świetna robota, wznowiłbym to jeszcze raz, gdybym mógł! –

1

Jak być może nie jest najbardziej efektywny, ale przynajmniej proste i łatwe rozwiązanie, chciałbym wykonać następujące czynności:

  • 1) rozszerzać zakresy;

  • 2) dołącz do rozszerzonych zakresów;

  • 3) pogrupuj wyniki.

To, oczywiście, zakłada się, że jedynie terminy są używane (to znaczy część czas 00:00 dla każdego StartDate i EndDate w obu tablicach).

Aby poszerzyć zakresy dat, wolę za pomocą numbers table coś takiego:

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) 

i podobnie dla MemberPlans.

celu uzyskania łącznej zestaw wierszy, użyłbym FULL JOIN, choć jeśli wiedzieć wcześniej, że obie tabele obejmują dokładnie ten sam okres czasu, INNER JOIN zrobi tak samo dobrze:

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

Teraz trzeba tylko do grupy powstałe rzędy i znaleźć minimalny i maksymalny termin dla każdej kombinacji (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 

Zauważ, że jeśli używasz INNER JOIN zamiast FULL JOIN, nie będzie potrzebował wszystkich w tych wyrażeniach ISNULL(), wystarczy wybrać kolumnę tabeli, np. pcp.MemberID zamiast ISNULL(pcp.MemberID, plans.MemberID) i pcp.Date zamiast ISNULL(pcp.Date, plans.Date).

Kompletny kwerenda może wyglądać to wtedy:

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 

Można spróbować tej kwerendy at SQL Fiddle.

Powiązane problemy