2012-02-14 14 views
8

Mam tabelę następującą strukturę danych w SQL Server:Tworzenie grupy kolejnych dni spełniających kryteria danego

ID Date  Allocation 
1, 2012-01-01, 0 
2, 2012-01-02, 2 
3, 2012-01-03, 0 
4, 2012-01-04, 0 
5, 2012-01-05, 0 
6, 2012-01-06, 5 

itp

Co muszę zrobić, to wszystkie kolejne okresy dziennie gdzie Przydział = 0, w następującej postaci:

Start Date End Date  DayCount 
2012-01-01 2012-01-01 1 
2012-01-03 2012-01-05 3 

itp

Czy poss zrobić to w SQL, a jeśli tak, to w jaki sposób?

+0

@ istari to data zakończenia kolumny w strukturze tabeli – Devjosh

+0

Czy próbowałeś używać kursora? lub nie potrzebujesz kursorów – Vikram

+0

Masz na myśli "kolejne", jak w "jeden dzień od siebie", lub jak w "sąsiednich, gdy wiersze są sortowane według daty"? tj. czy każda niepowtarzalna data pojawia się dokładnie raz w kolumnie "data"? – gcbenison

Odpowiedz

3

W tej odpowiedzi, będę zakładać, że „ID” numerów polowych wiersze kolejno po posortowane według daty wzrasta, podobnie jak ma to miejsce w przykładowych danych. (Taką kolumnę można utworzyć, jeśli nie istnieje).

To jest przykład techniki opisanej jako here i here.

1) Dołącz tabelę do siebie na sąsiednich wartościach "id". Te pary sąsiednich wierszy. Wybierz wiersze, w których zmieniło się pole "przydział". Zapisz wynik w tabeli tymczasowej, zachowując jednocześnie indeks bieżący.

SET @idx = 0; 
CREATE TEMPORARY TABLE boundaries 
SELECT 
    (@idx := @idx + 1) AS idx, 
    a1.date AS prev_end, 
    a2.date AS next_start, 
    a1.allocation as allocation 
FROM allocations a1 
JOIN allocations a2 
ON (a2.id = a1.id + 1) 
WHERE a1.allocation != a2.allocation; 

Daje to tablica zawierająca „koniec poprzedniego okresu”, „początkiem kolejnego okresu” i „wartość«alokacja»w poprzednim okresie” w każdym rzędzie:

+------+------------+------------+------------+ 
| idx | prev_end | next_start | allocation | 
+------+------------+------------+------------+ 
| 1 | 2012-01-01 | 2012-01-02 |   0 | 
| 2 | 2012-01-02 | 2012-01-03 |   2 | 
| 3 | 2012-01-05 | 2012-01-06 |   0 | 
+------+------------+------------+------------+ 

2) Potrzebujemy początku i końca każdego okresu w tym samym wierszu, więc musimy ponownie połączyć sąsiednie wiersze. Aby to zrobić, tworząc drugą tabelę tymczasową jak boundaries ale o pole idx 1 większa:

+------+------------+------------+ 
| idx | prev_end | next_start | 
+------+------------+------------+ 
| 2 | 2012-01-01 | 2012-01-02 | 
| 3 | 2012-01-02 | 2012-01-03 | 
| 4 | 2012-01-05 | 2012-01-06 | 
+------+------------+------------+ 

teraz dołączyć na polu idx i otrzymujemy odpowiedź:

SELECT 
    boundaries2.next_start AS start, 
    boundaries.prev_end AS end, 
    allocation 
FROM boundaries 
JOIN boundaries2 
USING(idx); 

+------------+------------+------------+ 
| start  | end  | allocation | 
+------------+------------+------------+ 
| 2012-01-02 | 2012-01-02 |   2 | 
| 2012-01-03 | 2012-01-05 |   0 | 
+------------+------------+------------+ 

** Należy pamiętać, że ta odpowiedź poprawnie "pobiera" okresy "wewnętrzne", ale pomija dwa okresy "krawędzi", w których przydział = 0 na początku i przydział = 5 na końcu. Można je pobrać za pomocą klauzul UNION, ale chciałem przedstawić główną ideę bez tej komplikacji.

0

Rozwiązanie bez CTE:

SELECT a.aDate AS StartDate 
    , MIN(c.aDate) AS EndDate 
    , (datediff(day, a.aDate, MIN(c.aDate)) + 1) AS DayCount 
FROM (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x 
    JOIN table1 y ON y.aDate <= x.aDate 
    GROUP BY x.id, x.aDate, x.allocation 
) AS a 
LEFT JOIN (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x 
    JOIN table1 y ON y.aDate <= x.aDate 
    GROUP BY x.id, x.aDate, x.allocation 
) AS b ON a.idn = b.idn + 1 AND b.allocation = a.allocation 
LEFT JOIN (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x 
    JOIN table1 y ON y.aDate <= x.aDate 
    GROUP BY x.id, x.aDate, x.allocation 
) AS c ON a.idn <= c.idn AND c.allocation = a.allocation 
LEFT JOIN (
    SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x 
    JOIN table1 y ON y.aDate <= x.aDate 
    GROUP BY x.id, x.aDate, x.allocation 
) AS d ON c.idn = d.idn - 1 AND d.allocation = c.allocation 
WHERE b.idn IS NULL AND c.idn IS NOT NULL AND d.idn IS NULL AND a.allocation = 0 
GROUP BY a.aDate 

Example

+0

Podczas uruchamiania tego otrzymuję następujący komunikat o błędzie: Msg 530, poziom 16, Stan 1, wiersz 1 Oświadczenie zostało usunięte. Maksymalna rekursja 100 została wyczerpana przed stwierdzeniem c – Istari

3

Po byłoby jednym ze sposobów, aby to zrobić. Istotą tego rozwiązania jest

  • Użyj CTE aby uzyskać listę wszystkich kolejnych początku i enddates z Allocation = 0
  • użyć funkcji ROW_NUMBER okno przypisać rownumbers zależności zarówno Start- i enddates.
  • wybrać tylko te rekordy, w których zarówno ROW_NUMBERS równy 1.
  • Zastosowanie DATEDIFF Aby obliczyć DayCount

oświadczenie SQL

;WITH r AS (
    SELECT StartDate = Date, EndDate = Date 
    FROM YourTable 
    WHERE Allocation = 0 
    UNION ALL 
    SELECT r.StartDate, q.Date 
    FROM r 
      INNER JOIN YourTable q ON DATEDIFF(dd, r.EndDate, q.Date) = 1 
    WHERE q.Allocation = 0   
) 
SELECT [Start Date] = s.StartDate 
     , [End Date ] = s.EndDate 
     , [DayCount] = DATEDIFF(dd, s.StartDate, s.EndDate) + 1 
FROM (
      SELECT * 
        , rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC) 
        , rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC) 
      FROM r   
     ) s 
WHERE s.rn1 = 1 
     AND s.rn2 = 1 
OPTION (MAXRECURSION 0) 

skrypt testowy

;WITH q (ID, Date, Allocation) AS (
    SELECT * FROM (VALUES 
    (1, '2012-01-01', 0) 
    , (2, '2012-01-02', 2) 
    , (3, '2012-01-03', 0) 
    , (4, '2012-01-04', 0) 
    , (5, '2012-01-05', 0) 
    , (6, '2012-01-06', 5) 
) a (a, b, c) 
) 
, r AS (
    SELECT StartDate = Date, EndDate = Date 
    FROM q 
    WHERE Allocation = 0 
    UNION ALL 
    SELECT r.StartDate, q.Date 
    FROM r 
      INNER JOIN q ON DATEDIFF(dd, r.EndDate, q.Date) = 1 
    WHERE q.Allocation = 0   
) 
SELECT s.StartDate, s.EndDate, DATEDIFF(dd, s.StartDate, s.EndDate) + 1 
FROM (
      SELECT * 
        , rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC) 
        , rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC) 
      FROM r   
     ) s 
WHERE s.rn1 = 1 
     AND s.rn2 = 1 
OPTION (MAXRECURSION 0) 
+0

@Istari - Dodałem opcję maxrecursion, aby naprawić komunikat o błędzie. –

1

Alternatywny sposób z CTE bez ROW_NUMBER()

dane próbki:

if object_id('tempdb..#tab') is not null 
    drop table #tab 

create table #tab (id int, date datetime, allocation int) 

insert into #tab 
select 1, '2012-01-01', 0 union 
select 2, '2012-01-02', 2 union 
select 3, '2012-01-03', 0 union 
select 4, '2012-01-04', 0 union 
select 5, '2012-01-05', 0 union 
select 6, '2012-01-06', 5 union 
select 7, '2012-01-07', 0 union 
select 8, '2012-01-08', 5 union 
select 9, '2012-01-09', 0 union 
select 10, '2012-01-10', 0 

Zapytanie:

;with cte(s_id, e_id, b_id) as (
    select s.id, e.id, b.id 
    from #tab s 
    left join #tab e on dateadd(dd, 1, s.date) = e.date and e.allocation = 0 
    left join #tab b on dateadd(dd, -1, s.date) = b.date and b.allocation = 0 
    where s.allocation = 0 
) 
select ts.date as [start date], te.date as [end date], count(*) as [day count] from (
    select c1.s_id as s, (
     select min(s_id) from cte c2 
     where c2.e_id is null and c2.s_id >= c1.s_id 
    ) as e 
    from cte c1 
    where b_id is null 
) t 
join #tab t1 on t1.id between t.s and t.e and t1.allocation = 0 
join #tab ts on ts.id = t.s 
join #tab te on te.id = t.e 
group by t.s, t.e, ts.date, te.date 

Live example at data.SE.

1

Stosując te dane próbki:

CREATE TABLE MyTable (ID INT, Date DATETIME, Allocation INT); 
INSERT INTO MyTable VALUES (1, {d '2012-01-01'}, 0); 
INSERT INTO MyTable VALUES (2, {d '2012-01-02'}, 2); 
INSERT INTO MyTable VALUES (3, {d '2012-01-03'}, 0); 
INSERT INTO MyTable VALUES (4, {d '2012-01-04'}, 0); 
INSERT INTO MyTable VALUES (5, {d '2012-01-05'}, 0); 
INSERT INTO MyTable VALUES (6, {d '2012-01-06'}, 5); 
GO 

Spróbuj:

WITH DateGroups (ID, Date, Allocation, SeedID) AS (
    SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, MyTable.ID 
     FROM MyTable 
     LEFT JOIN MyTable Prev ON Prev.Date = DATEADD(d, -1, MyTable.Date) 
          AND Prev.Allocation = 0 
    WHERE Prev.ID IS NULL 
     AND MyTable.Allocation = 0 
    UNION ALL 
    SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, DateGroups.SeedID 
     FROM MyTable 
     JOIN DateGroups ON MyTable.Date = DATEADD(d, 1, DateGroups.Date) 
    WHERE MyTable.Allocation = 0 

), StartDates (ID, StartDate, DayCount) AS (
    SELECT SeedID, MIN(Date), COUNT(ID) 
     FROM DateGroups 
    GROUP BY SeedID 

), EndDates (ID, EndDate) AS (
    SELECT SeedID, MAX(Date) 
     FROM DateGroups 
    GROUP BY SeedID 

) 
SELECT StartDates.StartDate, EndDates.EndDate, StartDates.DayCount 
    FROM StartDates 
    JOIN EndDates ON StartDates.ID = EndDates.ID; 

Pierwsza sekcja zapytania jest rekurencyjne SELECT, który jest zamocowany przez wszystkie rzędy które przydział = 0, i którego poprzedni dzień albo nie istnieje albo ma przydział! = 0. To skutecznie zwraca ID: 1 i 3, które są datami początkowymi okresów, które chcesz powrócić.

Część rekurencyjna tego samego zapytania rozpoczyna się od wierszy zakotwiczenia i znajduje wszystkie kolejne daty, które również mają przypisanie = 0.Identyfikator SeedID śledzi zakotwiczone ID we wszystkich iteracjach.

Rezultatem tej pory to:

ID   Date     Allocation SeedID 
----------- ----------------------- ----------- ----------- 
1   2012-01-01 00:00:00.000 0   1 
3   2012-01-03 00:00:00.000 0   3 
4   2012-01-04 00:00:00.000 0   3 
5   2012-01-05 00:00:00.000 0   3 

Następny kwerendy sub wykorzystuje prosty GROUP BY odfiltrować wszystkie daty rozpoczęcia dla każdego SeedID, a także zlicza dni.

Ostatnie pod-zapytanie robi to samo z datami końcowymi, ale tym razem liczba dni nie jest potrzebna, ponieważ już to mamy.

Ostateczne zapytanie SELECT łączy te dwa elementy, aby połączyć daty rozpoczęcia i zakończenia i zwraca je wraz z liczbą dni.

1

Sprawdź, czy to działa dla ciebie Tutaj SDATE dla twojej DATE pozostaje taka sama jak twoja tabela.

SELECT SDATE, 
CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) >0 THEN(
CASE WHEN (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) IS NULL THEN SDATE 
ELSE (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) END 
)ELSE (SELECT SDATE FROM TABLE1 WHERE ID = (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID))END AS EDATE 
,CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) <0 THEN 
(SELECT COUNT(*) FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID)) ELSE 
(SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) END AS DAYCOUNT 
FROM TABLE1 TBL1 WHERE ALLOCATION = 0 
AND (((SELECT ALLOCATION FROM TABLE1 WHERE ID=(SELECT MAX(ID) FROM TABLE1 WHERE ID < TBL1.ID))<> 0) OR (SELECT MAX(ID) FROM TABLE1 WHERE ID < TBL1.ID)IS NULL); 
Powiązane problemy