Optimale Möglichkeit, einen zeitabhängigen Wert aufzuzeichnen und abzurufen

7

Ich habe einen Tisch mit Transaktionen für einen Bus (Boarding Riders). Angesichts der Routen-ID und des Datums muss ich in einer anderen Tabelle nachschlagen, welchen Servicetyp es an diesem Tag ausgeführt hat. Die Busfahrpläne ändern sich höchstens alle 6 Monate, wobei die meisten Jahre unverändert bleiben.

Derzeit ist die Zeitplantabelle wie folgt definiert:

CREATE TABLE [dbo].[Routes](
    [ID] [int] NOT NULL,
    [RouteID] [int] NOT NULL,
    [Type] [varchar](50) NOT NULL,
    [StartDate] [datetime] NOT NULL,
PRIMARY KEY CLUSTERED 
(
    [ID] ASC
));

Ein Beispiel könnte folgendermaßen aussehen:

ID  RouteID  Type          StartDate
--  -------  ------------  ----------
 1      301  Standard      2015-01-01
 2      301  Discontinued  2016-06-01
 3      302  Standard      2015-01-01
 4      302  ParaTrans     2017-01-01

Wenn ich also eine Transaktion vom 20.04.2015 für RouteID 301 habe , möchte ich "Standard" zurückerhalten. Wenn die Transaktion jedoch vom 20.01.2018 stammt , sollte sie "Eingestellt" zurückgeben. Bei Transaktionen vor dem 01.01.2015 sollte NULL (oder "" oder etwas anderes als ein Ergebnis zurückgegeben werden, das möglicherweise mit einer gültigen Antwort in Konflikt steht, z. B. "Standard", "Paratrans" oder "Discontinued").

Grundsätzlich stellt die Tabelle , dass Route 301 war eine Standardroute zwischen 2015.01.01 und 2016.05.31 (und damit alle Transaktionen während dieser Zeit sollte als „Standard“ eingestuft werden), dann wurde es aufgegebene auf 2016-06- 01 (über die aktuellen Tag, implizit , da es keine spätere Planänderung ist angegeben), während 302 eine Standardroute war 2015.01.01 durch 2016.12.31 , dann ein ParaTrans (it) Route nach.

Route   Type          Start       End
-----   ----          -----       ---
301
        Standard      2015-01-01  2016-05-31
        Discontinued  2016-06-01  Present
302
        Standard      2015-01-01  2016-12-31
        ParaTrans     2017-01-01  Present

Derzeit sieht die Abfrage dazu folgendermaßen aus:

SELECT
    TRANSIT_DAY, 
    ROUTE_ID, 
    (SELECT TOP (1) Type FROM Routes
     WHERE (RouteID = dbo.DAILY_SALES_DETAIL.ROUTE_ID) 
     AND (StartDate <= dbo.DAILY_SALES_DETAIL.TRANSIT_DAY)
     ORDER BY StartDate DESC) AS NCTD_MODE 
FROM dbo.DAILY_SALES_DETAIL

Fragen

Was ich wissen möchte, ist: Ist dies die effektivste Kombination aus (a) Struktur der RoutesTabelle und (b) Abfrage, um dieses Ergebnis zu erzielen? Mit anderen Worten, könnte eine effizientere Abfrage mit der vorhandenen Struktur verwendet werden? Könnte eine Änderung der Routentabelle eine effizientere Abfrage ermöglichen?

Überlegungen

Die Transaktionstabelle wird täglich von einem Lieferanten importiert. Daher ist das Ändern des Schemas dieser Tabelle nicht trivial und wird bevorzugt vermieden. Noch wichtiger ist, dass diese Suche in einer Reihe von Tabellen und Datenbanken unter Verwendung von Transaktionen oder anderen busbezogenen Daten verwendet wird, die von mehreren Anbietern bezogen wurden. Dies ist nur ein einziges Beispiel. Wir haben einen Anbieter (und damit eine Datenbank) für Geldtransaktionen, einen anderen für die Anzahl der Fahrer und einen weiteren für die Leistung usw., wobei die Routennummer und das Datum die einzige zuverlässig konsistente Kennung für alle sind.

Die Routentabelle hat einen Index von (RouteID, StartDate). Derzeit befinden sich 56 Zeilen in der Routentabelle und 26 Millionen Zeilen in der Transaktionstabelle. Die Routentabelle besteht aus 45 Routen. Derzeit gibt es keine Routen mit mehr als 2 Zeilen oder einer Änderung. Es gibt keine Begrenzung für die Anzahl der Änderungen, die eine einzelne Route haben könnte, aber ich füge diese Statistik hinzu, um zu zeigen, dass die Anzahl auf absehbare Zeit wahrscheinlich gering bleiben wird.

Ich kann alle erforderlichen Indizes hinzufügen, um eine vorgeschlagene Abfrage zu optimieren. Bei der Frage geht es mehr darum, die beste Strategie zu finden, vorausgesetzt, alle angemessenen Optimierungen werden an den betrachteten Strategien vorgenommen, als darum, die beste Optimierung einer bestimmten Strategie zu finden.


db <> hier fummeln

cpcodes
quelle
Das heißt nicht, dass Vorschläge zur Optimierung der aktuellen Strategie unerwünscht wären, wenn Sie der Meinung sind, dass dies bereits die beste Option ist. Abgesehen davon stellen die 56 Zeilen in der Routentabelle 45 Routen dar, wobei bisher keine Routen mehr als zwei Zeilen (eine Änderung) aufweisen. Cross (inner) Joins sind vielleicht eine praktikable Strategie, aber ich habe sie nicht gründlich untersucht.
cpcodes

Antworten:

5

Sie können die Leistung Ihres Setups wie in Ihrer Frage gezeigt steigern, indem Sie die dbo.RoutesTabelle in Folgendes ändern :

CREATE TABLE dbo.Routes(
      RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , CONSTRAINT PK_Routes
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate DESC)
) WITH (DATA_COMPRESSION = PAGE)
ON [PRIMARY];

Der Schlüssel hier ist , dass wir den Clustered - Index sind zu definieren, die ist die Tabelle, auf die Verbindung von RouteIDund StartDate DESC. Dadurch werden die Daten genau so bereitgestellt, wie es für die von Ihnen geschriebene Abfrage am effizientesten ist. Der Vorbehalt hier ist das Einfügen in die dbo.Routesfür eine vorhandene Route mit einem neuen Datum. Dies führt zu Seitenaufteilungen, da die Zeilen in absteigender Reihenfolge des Datums gefüllt werden. Bei einer kleinen Anzahl von Zeilen in der Routentabelle und der gelegentlichen Indexpflege sollte dies jedoch kein großes Problem sein.

Stattdessen würde ich in Betracht ziehen, die dbo.RoutesTabelle so zu ändern, dass sie eine EndDateSpalte enthält. Dadurch entfällt die Notwendigkeit, eine Unterabfrage mit TOP(1)und durchzuführen ORDER BY .... Etwas wie:

CREATE TABLE dbo.Routes(
      RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , EndDate datetime NOT NULL
    , CONSTRAINT PK_Routes
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate ASC)
);

Beachten Sie, dass der Clustered-Index jetzt aktiviert ist (RouteID, StartDate ASC).

Die Abfrage kann jetzt INNER JOINanstelle der korrelierten Unterabfrage eine verwenden und sieht folgendermaßen aus:

SELECT
      t.TRANSIT_DAY
    , t.ROUTE_ID
    ,  NCTD_MODE = r.Type 
FROM Transactions t
    INNER JOIN dbo.Routes r ON t.ROUTE_ID = r.RouteID 
        AND t.TRANSIT_DAY >= r.StartDate 
        AND t.TRANSIT_DAY < r.EndDate
ORDER BY t.TRANSIT_DAY
    , t.ROUTE_ID;

Auf diese Weise kann SQL Server einen einfachen Join für die innere Schleife ausführen, um Ergebnisse zu erhalten. Zugegeben, wenn Sie eine große Anzahl von Zeilen zurückgeben, ist eine erhebliche Sortierung erforderlich, die wahrscheinlich auf tempdb übergeht.

Mit dem unten gezeigten MCVE können wir Pläne für die beiden Varianten vergleichen. Der erste Plan ist Ihre ursprüngliche Abfrage mit der korrelierten Unterabfrage. Der zweite Plan enthält die EndDateSpalte.

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Die 2. Variante hat Plankosten, die etwa viermal niedriger sind als die 1. Variante. Die Sortieroperatoren in beiden Plänen fordern 108 MB Speicher an und verschütten über 9.000 Seiten an Tempdb. Es ist jedoch ziemlich unwahrscheinlich, dass Sie die gesamte Ergebnismenge anfordern, anstatt eine einzelne Route oder möglicherweise einen Datumsbereich zu erhalten. Wenn Sie einen Filter für eine einzelne Route hinzufügen, gibt es keine große Speicherzuweisung oder Verschüttung für Tempdb.

Was folgt, ist ein Beispiel- MCVE mit 10.000 Routenzeilen und 1.000.000 Transaktionszeilen, mit denen Tests für verschiedene Designs ausgeführt werden können:

Tun Sie dies in tempdb, um "Unfälle" mit echten Tabellen zu vermeiden.

USE tempdb;

Löschen Sie die Tabellen, falls vorhanden (dies funktioniert unter SQL Server 2016+):

DROP TABLE IF EXISTS dbo.Routes;
DROP TABLE IF EXISTS dbo.Transactions;

Erstellen Sie die dbo.RoutesTabelle mit einem Clustered-Index für RouteID, StartDate DESC:

CREATE TABLE dbo.Routes(
        RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , CONSTRAINT PK_Routes
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate DESC)
);

Fügen Sie 10.000 Routenzeilen ein:

;WITH src AS (
    SELECT t.n
    FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9))t(n)
)
, src2 AS (
SELECT RouteID = (s1.n * 1000) + (s2.n * 100) + (s3.n * 10)
    , Type = REPLICATE(CHAR(65 + CONVERT(int, CRYPT_GEN_RANDOM(1) % 26)), 50)
FROM src s1
    CROSS JOIN src s2
    CROSS JOIN src s3
    CROSS JOIN src s4
)
INSERT INTO dbo.Routes (RouteID, [Type], StartDate)
SELECT s.RouteID
    , s.Type
    , StartDate = DATEADD(DAY, ROW_NUMBER() OVER (PARTITION BY RouteID ORDER BY s.RouteID) - 1, '1997-01-01T00:00:00')
FROM src2 s

Erstellen Sie das dbo.Transactionsmit einem Clustered-Index ROUTE_ID, TRANSIT_DAY. Durch das Erstellen eines solchen Clustered-Index werden Abfragen optimiert, die sowohl nach Route als auch nach Tag gefiltert werden.

CREATE TABLE dbo.Transactions(
     TRANSIT_DAY datetime NOT NULL
    , ROUTE_ID int NOT NULL
    , CONSTRAINT PK_Transactions
        PRIMARY KEY CLUSTERED
        (ROUTE_ID, TRANSIT_DAY)
);

Fügen Sie 1.000.000 Zeilen in die dbo.TransactionsTabelle ein:

;WITH src AS (
    SELECT t.n
    FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9))t(n)
)
INSERT INTO dbo.Transactions (TRANSIT_DAY, ROUTE_ID)
SELECT DATEADD(DAY, CONVERT(int, CRYPT_GEN_RANDOM(1)), '1997-01-01') + DATEADD(MILLISECOND, ABS(CONVERT(int, CRYPT_GEN_RANDOM(4))), '00:00:00')
    , r.RouteID
FROM dbo.Routes r
CROSS JOIN src s1
CROSS JOIN src s2

Für eine RoutesTabelle mit einer EndDateSpalte, die für Vergleichstests verwendet werden kann, habe ich Folgendes verwendet:

CREATE TABLE dbo.RoutesEndDate(
      RouteID int NOT NULL
    , [Type] varchar(50) NOT NULL
    , StartDate datetime NOT NULL
    , EndDate datetime NOT NULL
    , CONSTRAINT PK_RoutesEndDate
        PRIMARY KEY CLUSTERED
        (RouteID, StartDate ASC)
);

INSERT INTO dbo.RoutesEndDate (RouteID, [Type], StartDate, EndDate)
SELECT r.RouteID
    , R.Type
    , R.StartDate
    , EndDate = COALESCE(LEAD(r.StartDate) OVER (PARTITION BY r.RouteID ORDER BY r.StartDate), GETDATE())
FROM dbo.Routes r

Fragen Sie beide Tabellen nach einer bestimmten Route ab:

SELECT
      t.TRANSIT_DAY
    , t.ROUTE_ID
    ,  NCTD_MODE = (
        SELECT TOP (1) Type 
        FROM Routes r
        WHERE (r.RouteID = t.ROUTE_ID) AND (r.StartDate <= t.TRANSIT_DAY)
        ORDER BY r.StartDate DESC
        ) 
FROM Transactions t
WHERE t.ROUTE_ID = 750
ORDER BY t.TRANSIT_DAY
    , t.ROUTE_ID;

Der Plan für die obige Abfrage:

Geben Sie hier die Bildbeschreibung ein

E / A- und Zeitstatistik:

Tabelle 'Routen'. Scananzahl 1000, logische Lesevorgänge 2142, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.
Tabelle 'Transaktionen'. Scananzahl 1, logische Lesevorgänge 7, physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.

 SQL Server-Ausführungszeiten:
   CPU-Zeit = 2 ms, verstrichene Zeit = 2 ms.
SQL Server-Analyse- und Kompilierungszeit: 
   CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms.

Abfrage für alle Transaktionen / Routen:

SELECT
      t.TRANSIT_DAY
    , t.ROUTE_ID
    ,  NCTD_MODE = (
        SELECT TOP (1) Type 
        FROM Routes r
        WHERE (r.RouteID = t.ROUTE_ID) AND (r.StartDate <= t.TRANSIT_DAY)
        ORDER BY r.StartDate DESC
        ) 
FROM Transactions t
ORDER BY t.TRANSIT_DAY
    , t.ROUTE_ID;

Der Plan:

Geben Sie hier die Bildbeschreibung ein

Böse Verschüttung an Tempdb für Sortieroperator:

Geben Sie hier die Bildbeschreibung ein

Wenn wir den Clustered-Index so ändern dbo.Transactions, dass er ist (TRANSIT_DAY, ROUTE_ID), und die vollständige Abfrage erneut ausführen, sehen wir einen Plan ohne diese hässliche Sortierung und Spill-to-Tempdb:

Geben Sie hier die Bildbeschreibung ein

Max Vernon
quelle
Sehr ausführlich. Um sicherzustellen, dass ich es vollständig habe, sollte das Enddatum auf das gleiche Datum wie das Datum gesetzt werden, an dem der neue Zeitplan begonnen hat (da der Join "<" anstelle von "<=" verwendet), und für diejenigen, die noch aktiv sind, Das Enddatum würde auf einen Punkt festgelegt, der weit genug in der Zukunft liegt (wie "20991231"), richtig? Wenn ja, haben Sie die Anweisung "EndDate = COALESCE (LEAD (r.StartDate) OVER (PARTITION BY r.RouteID ORDER BY r.StartDate), GETDATE ())" für die Generierung der Testtabelle mit einem weit zukünftigen Datum gemeint eher als "GETDATE ()". Aber davon abgesehen eine sehr gute Antwort. Vielen Dank.
cpcodes
Das ist genau richtig.
Max Vernon