Database
 sql >> Datenbank >  >> RDS >> Database

Erstellen Sie ein Set oder eine Sequenz ohne Schleifen – Teil 3

Früher in dieser Serie (Teil 1 | Teil 2) haben wir über das Generieren einer Reihe von Zahlen mit verschiedenen Techniken gesprochen. Obwohl es in einigen Szenarien interessant und nützlich ist, besteht eine praktischere Anwendung darin, eine Reihe zusammenhängender Datumsangaben zu generieren. B. ein Bericht, der alle Tage eines Monats anzeigen muss, auch wenn an manchen Tagen keine Transaktionen stattgefunden haben.

In einem früheren Beitrag habe ich erwähnt, dass es einfach ist, eine Reihe von Tagen aus einer Reihe von Zahlen abzuleiten. Da wir bereits mehrere Möglichkeiten zum Ableiten einer Reihe von Zahlen etabliert haben, schauen wir uns an, wie der nächste Schritt aussieht. Fangen wir ganz einfach an und stellen uns vor, wir wollen einen Bericht für drei Tage, vom 1. Januar bis zum 3. Januar, erstellen und für jeden Tag eine Zeile einfügen. Der altmodische Weg wäre, eine #temp-Tabelle zu erstellen, eine Schleife zu erstellen, eine Variable zu haben, die den aktuellen Tag enthält, innerhalb der Schleife eine Zeile in die #temp-Tabelle bis zum Ende des Bereichs einzufügen und dann die # Temp-Tabelle zum Outer-Join mit unseren Quelldaten. Das ist mehr Code, als ich hier präsentieren möchte, ganz zu schweigen davon, ihn in Produktion zu bringen, zu warten und Kollegen davon lernen zu lassen.

Einfach anfangen

Mit einer festgelegten Zahlenfolge (unabhängig von der gewählten Methode) wird diese Aufgabe viel einfacher. Für dieses Beispiel kann ich komplexe Sequenzgeneratoren durch eine sehr einfache Vereinigung ersetzen, da ich nur drei Tage benötige. Ich werde dieses Set aus vier Reihen machen, damit es auch einfach zu demonstrieren ist, wie man genau die Reihe abschneidet, die man braucht.

Zuerst haben wir ein paar Variablen, um den Anfang und das Ende des Bereichs zu halten, an dem wir interessiert sind:

DECLARE @s DATE = '2012-01-01', @e DATE = '2012-01-03';

Wenn wir jetzt nur mit dem einfachen Reihengenerator beginnen, könnte es so aussehen. Ich werde einen ORDER BY hinzufügen auch hier, nur um sicherzugehen, da wir uns niemals auf Annahmen verlassen können, die wir über die Reihenfolge machen.

;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4)
SELECT n FROM n ORDER BY n;
 
-- result:
 
n
----
1
2
3
4

Um das in eine Reihe von Daten umzuwandeln, können wir einfach DATEADD() anwenden ab dem Startdatum:

;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4)
SELECT DATEADD(DAY, n, @s) FROM n ORDER BY n;
 
-- result:
 
----
2012-01-02
2012-01-03
2012-01-04
2012-01-05

Das ist noch nicht ganz richtig, da unser Sortiment am 2. statt am 1. beginnt. Um also unser Startdatum als Basis zu verwenden, müssen wir unser Set von 1-basiert auf 0-basiert umwandeln. Wir können das tun, indem wir 1 subtrahieren:

;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4)
SELECT DATEADD(DAY, n-1, @s) FROM n ORDER BY n;
 
-- result:
 
----
2012-01-01
2012-01-02
2012-01-03
2012-01-04

Fast dort! Wir müssen nur das Ergebnis aus unserer größeren Serienquelle begrenzen, was wir tun können, indem wir den DATEDIFF füttern , in Tagen, zwischen Anfang und Ende des Bereichs, bis zu einem TOP Operator – und dann 1 hinzufügen (da DATEDIFF meldet im Wesentlichen einen offenen Bereich).

;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4)
SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM n ORDER BY n;
 
-- result:
 
----
2012-01-01
2012-01-02
2012-01-03

Echte Daten hinzufügen

Um nun zu sehen, wie wir mit einer anderen Tabelle verknüpfen würden, um einen Bericht abzuleiten, können wir einfach unsere neue Abfrage und äußere Verknüpfung mit den Quelldaten verwenden.

;WITH n(n) AS 
(
  SELECT 1 UNION ALL SELECT 2 UNION ALL 
  SELECT 3 UNION ALL SELECT 4
),
d(OrderDate) AS
(
  SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) 
  FROM n ORDER BY n
)
SELECT 
  d.OrderDate,
  OrderCount = COUNT(o.SalesOrderID)
FROM d
LEFT OUTER JOIN Sales.SalesOrderHeader AS o
ON o.OrderDate >= d.OrderDate
AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate)
GROUP BY d.OrderDate
ORDER BY d.OrderDate;

(Beachten Sie, dass wir COUNT(*) nicht mehr sagen können , da dies die linke Seite zählt, die immer 1 ist.)

Eine andere Schreibweise wäre:

;WITH d(OrderDate) AS
(
  SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) 
  FROM 
  (
    SELECT 1 UNION ALL SELECT 2 UNION ALL 
    SELECT 3 UNION ALL SELECT 4
  ) AS n(n) ORDER BY n
)
SELECT 
  d.OrderDate,
  OrderCount = COUNT(o.SalesOrderID)
FROM d
LEFT OUTER JOIN Sales.SalesOrderHeader AS o
ON o.OrderDate >= d.OrderDate
AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate)
GROUP BY d.OrderDate
ORDER BY d.OrderDate;

Dies sollte es einfacher machen, sich vorzustellen, wie Sie den führenden CTE durch die Generierung einer Datumsfolge aus einer beliebigen Quelle ersetzen würden. Wir werden diese (mit Ausnahme des rekursiven CTE-Ansatzes, der nur dazu diente, Diagramme zu verzerren) mit AdventureWorks2012 durchgehen, aber wir werden SalesOrderHeaderEnlarged verwenden Tabelle, die ich aus diesem Skript von Jonathan Kehayias erstellt habe. Ich habe einen Index hinzugefügt, um bei dieser speziellen Abfrage zu helfen:

CREATE INDEX d_so ON Sales.SalesOrderHeaderEnlarged(OrderDate);

Beachten Sie auch, dass ich einen willkürlichen Datumsbereich wähle, von dem ich weiß, dass er in der Tabelle vorhanden ist.

    Zahlentabelle
    ;WITH d(OrderDate) AS
    (
      SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) 
      FROM dbo.Numbers ORDER BY n
    )
    SELECT 
      d.OrderDate,
      OrderCount = COUNT(s.SalesOrderID)
    FROM d
    LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s
    ON s.OrderDate >= @s AND s.OrderDate <= @e
    AND CONVERT(DATE, s.OrderDate) = d.OrderDate
    WHERE d.OrderDate >= @s AND d.OrderDate <= @e
    GROUP BY d.OrderDate
    ORDER BY d.OrderDate;

    Plan (zum Vergrößern anklicken):

    spt_values
    DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29';
     
    ;WITH d(OrderDate) AS
    (
      SELECT DATEADD(DAY, n-1, @s) 
      FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1)
       ROW_NUMBER() OVER (ORDER BY Number) FROM master..spt_values) AS x(n)
    )
    SELECT 
      d.OrderDate,
      OrderCount = COUNT(s.SalesOrderID)
    FROM d
    LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s
    ON s.OrderDate >= @s AND s.OrderDate <= @e
    AND CONVERT(DATE, s.OrderDate) = d.OrderDate
    WHERE d.OrderDate >= @s AND d.OrderDate <= @e
    GROUP BY d.OrderDate
    ORDER BY d.OrderDate;

    Plan (zum Vergrößern anklicken):

    sys.all_objects
    DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29';
     
    ;WITH d(OrderDate) AS
    (
      SELECT DATEADD(DAY, n-1, @s) 
      FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1)
       ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_objects) AS x(n)
    )
    SELECT 
      d.OrderDate,
      OrderCount = COUNT(s.SalesOrderID)
    FROM d
    LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s
    ON s.OrderDate >= @s AND s.OrderDate <= @e
    AND CONVERT(DATE, s.OrderDate) = d.OrderDate
    WHERE d.OrderDate >= @s AND d.OrderDate <= @e
    GROUP BY d.OrderDate
    ORDER BY d.OrderDate;

    Plan (zum Vergrößern anklicken):

    Gestapelte CTEs
    DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29';
     
    ;WITH e1(n) AS 
    (
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
    ),
    e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b),
    d(OrderDate) AS
    (
      SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) 
        d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY n)-1, @s) 
      FROM e2
    )
    SELECT 
      d.OrderDate, 
      OrderCount = COUNT(s.SalesOrderID)
    FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s
    ON s.OrderDate >= @s AND s.OrderDate <= @e
    AND d.OrderDate = CONVERT(DATE, s.OrderDate)
    WHERE d.OrderDate >= @s AND d.OrderDate <= @e
    GROUP BY d.OrderDate
    ORDER BY d.OrderDate;

    Plan (zum Vergrößern anklicken):

    Nun, für eine einjährige Reichweite reicht das nicht aus, da es nur 100 Reihen produziert. Für ein Jahr müssten wir 366 Zeilen abdecken (um potenzielle Schaltjahre zu berücksichtigen), also würde es so aussehen:

    DECLARE @s DATE = '2006-10-23', @e DATE = '2007-10-22';
     
    ;WITH e1(n) AS 
    (
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
    ),
    e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b),
    e3(n) AS (SELECT 1 FROM e2 CROSS JOIN (SELECT TOP (37) n FROM e2) AS b),
    d(OrderDate) AS
    (
      SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) 
        d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY N)-1, @s) 
      FROM e3
    )
    SELECT 
      d.OrderDate, 
      OrderCount = COUNT(s.SalesOrderID)
    FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s
    ON s.OrderDate >= @s AND s.OrderDate <= @e
    AND d.OrderDate = CONVERT(DATE, s.OrderDate)
    WHERE d.OrderDate >= @s AND d.OrderDate <= @e
    GROUP BY d.OrderDate
    ORDER BY d.OrderDate;

    Plan (zum Vergrößern anklicken):

    Kalendertabelle

    Dies ist eine neue Frage, über die wir in den beiden vorherigen Beiträgen nicht viel gesprochen haben. Wenn Sie Datumsreihen für viele Abfragen verwenden, sollten Sie sowohl eine Zahlentabelle als auch eine Kalendertabelle verwenden. Das gleiche Argument gilt, wie viel Speicherplatz wirklich benötigt wird und wie schnell der Zugriff erfolgt, wenn die Tabelle häufig abgefragt wird. Um beispielsweise Daten für 30 Jahre zu speichern, sind weniger als 11.000 Zeilen erforderlich (die genaue Anzahl hängt davon ab, wie viele Schaltjahre Sie überspannen) und benötigen nur 200 KB. Ja, Sie haben richtig gelesen:200 Kilobyte . (Und komprimiert sind es nur 136 KB.)

    Um eine Kalendertabelle mit Daten aus 30 Jahren zu generieren, können wir Folgendes tun, vorausgesetzt, Sie sind bereits davon überzeugt, dass eine Zahlentabelle eine gute Sache ist:

    DECLARE @s DATE = '2005-07-01'; -- earliest year in SalesOrderHeader
    DECLARE @e DATE = DATEADD(DAY, -1, DATEADD(YEAR, 30, @s));
     
    SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) 
     d = CONVERT(DATE, DATEADD(DAY, n-1, @s))
     INTO dbo.Calendar
     FROM dbo.Numbers ORDER BY n;
     
    CREATE UNIQUE CLUSTERED INDEX d ON dbo.Calendar(d);

    Um diese Kalendertabelle nun in unserer Verkaufsberichtabfrage zu verwenden, können wir eine viel einfachere Abfrage schreiben:

    DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29';
     
    SELECT
      OrderDate = c.d, 
      OrderCount = COUNT(s.SalesOrderID)
    FROM dbo.Calendar AS c
    LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s
    ON s.OrderDate >= @s AND s.OrderDate <= @e
    AND c.d = CONVERT(DATE, s.OrderDate)
    WHERE c.d >= @s AND c.d <= @e
    GROUP BY c.d
    ORDER BY c.d;

    Plan (zum Vergrößern anklicken):

Leistung

Ich habe sowohl komprimierte als auch unkomprimierte Kopien der Zahlen- und Kalendertabellen erstellt und einen Bereich von einer Woche, einem Monat und einem Jahr getestet. Ich habe auch Abfragen mit kaltem Cache und warmem Cache ausgeführt, aber das stellte sich als weitgehend belanglos heraus.


Dauer in Millisekunden zum Generieren eines einwöchigen Bereichs


Dauer in Millisekunden zum Generieren eines einmonatigen Bereichs


Dauer in Millisekunden zum Generieren eines einjährigen Bereichs

Nachtrag

Paul White (Blog | @SQL_Kiwi) wies darauf hin, dass Sie die Numbers-Tabelle zwingen können, einen viel effizienteren Plan zu erstellen, indem Sie die folgende Abfrage verwenden:

SELECT
  OrderDate = DATEADD(DAY, n, 0),
  OrderCount = COUNT(s.SalesOrderID)
FROM dbo.Numbers AS n
LEFT OUTER JOIN Sales.SalesOrderHeader AS s 
ON s.OrderDate >= CONVERT(DATETIME, @s)
  AND s.OrderDate < DATEADD(DAY, 1, CONVERT(DATETIME, @e))
  AND DATEDIFF(DAY, 0, OrderDate) = n
WHERE
  n.n >= DATEDIFF(DAY, 0, @s)
  AND n.n <= DATEDIFF(DAY, 0, @e)
GROUP BY n
ORDER BY n;

An dieser Stelle werde ich nicht alle Leistungstests wiederholen (Übung für den Leser!), aber ich gehe davon aus, dass sie bessere oder ähnliche Timings erzeugen werden. Dennoch denke ich, dass eine Kalendertabelle eine nützliche Sache ist, auch wenn sie nicht unbedingt notwendig ist.

Schlussfolgerung

Die Ergebnisse sprechen für sich. Für die Generierung einer Reihe von Zahlen gewinnt der Numbers-Table-Ansatz, aber nur marginal – selbst bei 1.000.000 Zeilen. Und für eine Reihe von Datteln am unteren Ende werden Sie keinen großen Unterschied zwischen den verschiedenen Techniken feststellen. Es ist jedoch ziemlich klar, dass die Kalendertabelle ihren Wert wirklich unter Beweis stellt, wenn Ihr Datumsbereich größer wird, insbesondere wenn Sie es mit einer großen Quelltabelle zu tun haben – insbesondere angesichts ihres geringen Speicherbedarfs. Selbst mit Kanadas verrücktem metrischem System sind 60 Millisekunden viel besser als etwa 10 *Sekunden*, wenn nur 200 KB auf der Festplatte benötigt werden.

Ich hoffe, Ihnen hat diese kleine Serie gefallen; Es ist ein Thema, das ich schon seit Ewigkeiten wieder aufgreifen wollte.

[ Teil 1 | Teil 2 | Teil 3 ]