T-SQL Ninja #30

FIRST_VALUE und LAST_VALUE

Was tun FIRST_VALUE und LAST_VALUE?

Wie schon die in den Wochen 12 bis 14 vorgestellten ROW_NUMBER und RANK-Funktionen, gehören auch FIRST_VALUE und LAST_VALUE zu den Window-Funktionen. Das heißt sie sind Funktionen, die auf einer geordneten Menge von Zeilen in der Ergebnismenge operieren. Den Namen entsprechend geben die Funktionen die erste beziehungsweise letzte Zeile des Fensters zurück, auf dem sie operieren.

Wie könnt ihr mit FIRST_VALUE und LAST_VALUE arbeiten?

Um erste und letzte Zeilen eines Ergebnisfensters zu ermitteln, nehmen wir uns in der WideWorldImporters-Datenbank die Sales.Order-Tabelle vor, um die erste Bestellung jedes Kunden zu finden. Folgende Spalten der Tabelle wollen wir uns ansehen:

SELECT 
     OrderDate
    ,OrderID
    ,ExpectedDeliveryDate
    ,CustomerID
FROM [Sales].[Orders]

Um nun für jede Bestellung die erste Bestellung des jeweiligen Kunden zu finden, benutzen wir die FIRST_VALUE-Funktion. Diese Funktion berechnet nun zu jeder Zeile in der Orders-Tabelle die entsprechenden ersten Werte:

SELECT 
     OrderDate as DateCurrentOrder
    ,FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as DateFirstOrder
    ,OrderID as IDCurrentOrder
    ,FIRST_VALUE(OrderID) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as IDirstOrder
    ,ExpectedDeliveryDate as ExpectedDeliveryCurrentOrder
    ,FIRST_VALUE(ExpectedDeliveryDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as ExpectedDeliveryFirstOrder
    ,CustomerID
FROM [Sales].[Orders]
ORDER BY CustomerID

Nun können wir mit Hilfe dieser Werte beispielsweise zu jeder Bestellung berechnen, wie viele Tage nach der ersten Bestellung sie aufgegeben wurde:

SELECT 
     OrderDate as DateCurrentOrder
    ,FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as DateFirstOrder
    ,DATEDIFF(DAY, FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate), OrderDate) DaysSinceFirstOrder
    ,OrderID as IDCurrentOrder
    ,FIRST_VALUE(OrderID) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as IDirstOrder
    ,ExpectedDeliveryDate as ExpectedDeliveryCurrentOrder
    ,FIRST_VALUE(ExpectedDeliveryDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as ExpectedDeliveryFirstOrder
    ,CustomerID
FROM [Sales].[Orders]
ORDER BY CustomerID

Auf die selbe Art können wir nun natürlich auch die Tage von der Bestellung zur jeweils letzten Bestellung des Kunden ermitteln, indem wir LAST_VALUE verwenden:

SELECT 
     OrderDate as DateCurrentOrder
    ,FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as DateFirstOrder
    ,LAST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) DateLastOrder
    ,DATEDIFF(DAY, FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate), OrderDate) DaysSinceFirstOrder
    ,OrderID as IDCurrentOrder
    ,FIRST_VALUE(OrderID) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as IDirstOrder
    ,ExpectedDeliveryDate as ExpectedDeliveryCurrentOrder
    ,FIRST_VALUE(ExpectedDeliveryDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as ExpectedDeliveryFirstOrder
    ,CustomerID
FROM [Sales].[Orders]
ORDER BY CustomerID

Doch zu unserer großen Überraschung sehen wir nun als LAST_VALUE immer den Wert der aktuellen Zeile als Letztes Bestelldatum des Kunden. Das liegt daran, dass wir schon in den ersten Beiträgen zu Window-Funktionen erklärt hatten, dass das Standard-Fenster immer vom Anfang des Definitionsbereichs bis zur aktuellen Zeile heranreicht. Um das zu ändern müssen eine sogenannte „range clause“ angeben, die die Form ROWS BETWEEN a AND b hat:

SELECT 
     OrderDate as DateCurrentOrder
    ,FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as DateFirstOrder
    ,LAST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) DateLastOrder
    ,DATEDIFF(DAY, FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate), OrderDate) DaysSinceFirstOrder
    ,OrderID as IDCurrentOrder
    ,FIRST_VALUE(OrderID) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as IDirstOrder
    ,ExpectedDeliveryDate as ExpectedDeliveryCurrentOrder
    ,FIRST_VALUE(ExpectedDeliveryDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as ExpectedDeliveryFirstOrder
    ,CustomerID
FROM [Sales].[Orders]
ORDER BY CustomerID

Mit diesem Hintergrundwissen können wir nun auch den Abstand zur jeweils aktuellsten Bestellung des Kunden angeben:

SELECT 
     OrderDate as DateCurrentOrder
    ,FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as DateFirstOrder
    ,LAST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) DateLastOrder
    ,DATEDIFF(DAY, FIRST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate), OrderDate) DaysSinceFirstOrder
    ,DATEDIFF(DAY, OrderDate, LAST_VALUE(OrderDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)) DaysToLastOrder
    ,OrderID as IDCurrentOrder
    ,FIRST_VALUE(OrderID) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as IDirstOrder
    ,ExpectedDeliveryDate as ExpectedDeliveryCurrentOrder
    ,FIRST_VALUE(ExpectedDeliveryDate) OVER (PARTITION BY CustomerID ORDER BY OrderDate) as ExpectedDeliveryFirstOrder
    ,CustomerID
FROM [Sales].[Orders]
ORDER BY CustomerID

FIRST_VALUE und LAST_VALUE sind echt nützliche Funktionen, trotz allem solltet ihr bei den hier gezeigten Techniken vorsichtig sein, denn Fenster-Funktionen sind potenzielle Performance-Killer, da im Zweifel ein Vielfaches der zurückgegebenen Ergebnismenge vom Server gelesen und verarbeitet werden muss um die Fenster komplett auszuführen.

Referenzen

Ninja-Notebooks @ GitHub

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.