2017-01-03 7 views
2

In letzter Zeit ist ein interessantes SQL-Problem aufgetreten, das ich ohne mehrere Schritte in der Vergangenheit nicht lösen konnte.Abfrage, um die Anzahl der Elemente mit einem bestimmten Status innerhalb eines bestimmten Zeitraums zu ermitteln

Angenommen, Sie haben eine einfache "ItemStatus" -Tabelle, in der Sie den Status verschiedener Elemente verfolgen können. Spalten sind "itemId", "Datumsstatus geändert" und "Status". Beispiel:

ItemId  DateStatusChanged Status 
1   09/01/2016    New 
1   10/15/2016    Complete 
2   10/20/2016    New 
2   10/25/2016    Complete 

Es wird davon ausgegangen, dass ein Artikel denselben Status hat, bis er geändert wird. Also war der Punkt 1 "Neu", beginnend am 9/1, und blieb bis zum 15.10. "Neu", als er zu "Fertig" geändert wurde. Punkt 2 war "Neu" von 10/20 bis 10/25.

Also sagen Sie, dass Sie die Tabelle abfragen möchten, um eine Zählung zu erhalten, wie viele einzigartige Gegenstände den Status "Neu" jederzeit im Oktober 2016 hatten (was in diesem Fall "2" wäre). Gibt es eine einzige SQL-Abfrage, die dieses Ergebnis zurückgeben kann?

Antwort

1

Es ist eines der üblichen SQL-Probleme im Zusammenhang mit Timelining; Ja, es gibt eine Lösung. Eine einfachere und effizientere Abfrage kann geschrieben werden, wenn die Datensätze auch Terminierungstermine haben. Das bedeutet natürlich, dass Sie nicht nur auf einen Datensatz schließen, sondern auch auf die Probleme potenziell ungültiger Sequenzen (z. B. überlappende Zustände) oder Lücken in der Zeitleiste). So ...

select count(distinct ItemId) 
    from ItemStatus is1 
where status = 'New' 
    and DateStatusChanged < '2016-11-01' -- syntax may vary 
    and not exists 
     (select 1 
      from itemstatus is2 
     where is2.itemid = is1.itemid 
      and is2.status != 'New' 
      and is2.datestatuschanged > is1.datestatuschanged 
      and is2.datestatuschanged < '2016-10-01') 

Sie müssen einige < vs <= usw. einstellen kann, abhängig von der genauen gewünschten Logik, Zeitstempel Körnigkeit usw.

+0

Ah, ja, perfekt - das ist nach dem Vorbild dessen, was ich in pseudo-SQL denken aber übersetzen konnte nicht ganz. Ich wusste, dass eine "Platzhalter" -Variable für den Datumsstatus benötigt werden würde, wo Status = 'Neu' ist, da man nach dem * Vorhandensein von Nicht-'Neu'-Statusänderungssätzen für den betreffenden Artikel prüfen will * dieses Datum aber vor dem 1. Oktober. Vielen Dank! – Cmaso

+0

Ich glaube, dass die letzte Zeile ein Tippfehler war - wenn diese Abfrage ausgeführt wird, wird eine Zählung von 1 zurückgegeben.Um eine Zählung von 2 zu erhalten (was korrekt ist - beide Elemente waren zu irgendeinem Zeitpunkt im Oktober 2016 "neu"), sollte die letzte Zeile der Abfrage "und is2.datestatuschanged <'2016-10-01'" lauten. – Cmaso

+0

Ayup. Behoben. –

0

Sie bedingte Aggregation verwenden können, um jede Zeile zu machen ein Neues und vollständiges Datum Von dort ist die Abfrage eigentlich ziemlich einfach.

So etwas sollte Sie in die richtige Richtung weisen.

select count(distinct ItemID) as ItemCount 
from 
(
    select ItemID 
     , max(case when Status = 'New' then DateStatusChanged end) as NewDate 
     , Max(case when Status = 'Complete' then DateStatusChanged end) as CompleteDate 
    from YourTable 
    group by ItemID 
) MyItems 
where NewDate >= '2016-10-01' 
    and CompleteDate >= '2016-10-01' 
    and CompleteDate >= NewDate --just to ensure that is wasn't marked complete before it was marked new 
1

Sie können lead verwenden, um die nächste Statusänderung Datum für jede auf Itemid erhalten basierend Reihenfolge der datestatuschanged aufsteigend. Überprüfen Sie dann, ob die nächste Änderung oder die vorhandene Änderung zwischen den angegebenen Daten liegt und diese Elemente zählen.

select count(distinct ItemID) 
from (select i.* 
    ,lead(datestatuschanged) over(partition by itemid order by datestatuschanged) as next_change 
     from itemstatus i 
    ) x 
where status = 'New' 
and ((next_change >= '2016-10-01' and next_change <= '2016-10-31') 
     or 
     (datestatuschanged >= '2016-10-01' and datestatuschanged <= '2016-10-31') 
    ) 
+0

Gute Verwendung der Fensterfunktion ['Lead()'] (https://msdn.microsoft.com/en-us/library/hh213125.aspx). Obwohl ich denke, dass die Datumslogik übermäßig komplex ist. Müssen Sie nicht einfach überprüfen, ob der "neue" Datensatz ein dateStatusChanged <= 2016-10-1 hat und dass der next_Change nach 2016-10-1 aufgetreten ist? (vielleicht 'coalesce (next_change, getDate())> '2016-10-1'? (unter der Annahme, dass next_change ist ein Datum nicht Datum Zeit) – xQbert

+0

Sehr cool, wusste ich nicht über die Funktion lead() vor. Ich werde damit herumspielen. xQbert - das einzige Problem dabei ist, dass der "neue" Datensatz nach dem 01.10.2016 eine dateStatusChanged haben kann (wie bei ItemId 2). Ich möchte grundsätzlich alle Artikel, die am oder vor dem 31.10. Einen "neuen" Datensatz haben, wobei der nächste nicht "neue" Datensatz des Artikels nach 10/1 auftaucht (oder nicht existiert). – Cmaso

0

Sie können Abfrage versuchen, wie diese

select ItemId from (
select itemid, status, RowN = row_number() over (partition by itemid order by status) from youritem where MONTH(datestatuschanged) = 10 and year(datestatuschanged) = 2016 ) as SourceTable 
pivot(max(status) for RowN in ([1],[2])) p 
where [1] = 'Complete' and [2] = 'New' 

Idee ist es, sowohl Status in Spalten umzusetzen und zu vergleichen und wählen Sie nur den gewünschten Monat und Jahr

+0

Das ist ähnlich, wie ich es in der Vergangenheit gelöst habe, nur benutzte ich serverseitigen Code, um ein Abfrageobjekt im laufenden Betrieb zu erstellen und dann zurückzugehen und dieses Abfrageobjekt abzufragen. Danke, dass Sie eine All-SQL-Lösung gezeigt haben! – Cmaso

0

leicht angepasst an ein COUNT(DISTINCT ...) mit einem WHERE Status = 'New' , wird der folgende CTE Ihnen den Status für jeden Artikel an jedem Tag des Monats Oktober geben:

;WITH DATE_CTE (aDate) AS (
    SELECT CAST('2016-10-01' AS DATETIME) 

    UNION ALL 

    SELECT DATEADD(d, 1, cte.aDate) 
     FROM DATE_CTE cte 
    WHERE cte.aDate < CAST('2016-10-31' AS DATETIME) 
) 
SELECT i.itemid, dates.aDate, i.status 
    FROM DATE_CTE dates 
     INNER JOIN itemstatus i 
      ON i.DateStatusChanged <= dates.aDate 
     LEFT OUTER JOIN itemstatus i2 
      ON i.ItemId = i2.ItemId 
      AND i.DateStatusChanged < i2.DateStatusChanged 
      AND dates.aDate >= i2.DateStatusChanged 
WHERE i2.DateStatusChanged IS NULL 
ORDER BY i.itemid, dates.aDate 

Richtige Antwort:

;WITH DATE_CTE (aDate) AS (
    SELECT CAST('2016-10-01' AS DATETIME) 

    UNION ALL 

    SELECT DATEADD(d, 1, cte.aDate) 
     FROM DATE_CTE cte 
    WHERE cte.aDate < CAST('2016-10-31' AS DATETIME) 
) 
SELECT COUNT(DISTINCT i.itemid) 
    FROM DATE_CTE dates 
     INNER JOIN itemstatus i 
      ON i.DateStatusChanged <= dates.aDate 
     LEFT OUTER JOIN itemstatus i2 
      ON i.ItemId = i2.ItemId 
      AND i.DateStatusChanged < i2.DateStatusChanged 
      AND dates.aDate >= i2.DateStatusChanged 
WHERE i.Status = N'New' 
    AND i2.DateStatusChanged IS NULL 
Verwandte Themen