Das Szenario
Es war einmal eine Staging-Datenbank in einem kleinen Unternehmen, die an einem ETL-Prozess teilnahm und als Empfangskatalog für die verschiedenen Dateiformate aus einer Reihe von Quellen Dritter fungierte. Das E wurde über DTS-Pakete mit wenigen Kontrollstrukturen für die Prüfung oder Kontrolle abgewickelt, wurde jedoch als "gut genug" eingestuft und war dies in jeder Hinsicht.
Die vom E-Teil bereitgestellten Daten waren für den Verbrauch durch eine einzelne Anwendung bestimmt, die von einer Handvoll junger und fähiger Programmierer entwickelt und verwaltet wurde. Obwohl es ihnen an Erfahrung oder Kenntnissen der damaligen Data Warehousing-Techniken mangelte, legten sie ihre eigenen T- und L-Prozesse aus dem Anwendungscode fest und erstellten sie. Diese jungen Software-Ingenieure haben das erfunden, was Außenstehende als "weniger als ideales Rad" bezeichnen könnten, aber mit "Good Enough" als allgegenwärtigem Service-Level konnten sie einen betrieblichen Rahmen schaffen.
Eine Zeit lang war im eng gekoppelten Bereich alles in Ordnung, und der Staging-Katalog stützte sich auf die Daten eines Dutzend Dritter, die wiederum von der Anwendung gespeist wurden. Als die Anwendung wuchs, wuchs auch ihr Appetit, aber mit den geschickten Entwicklern weißer Ritter, die über das System wachten, wurde dieser Appetit schnell und in vielen Fällen sogar gut angegangen.
Aber das goldene Zeitalter konnte natürlich nicht ewig dauern. Mit dem Wohlstand, der durch die erfolgreiche Bewerbung gewährt wurde, wuchs das Geschäft und wuchs. Während des Wachstums mussten die Staging-Umgebung und -Anwendung mitwachsen. Bei aller Wachsamkeit konnte nur eine Handvoll Heldenentwickler nicht mit der Aufrechterhaltung des jetzt expansiven Systems Schritt halten, und die Verbraucher hatten Anspruch auf ihre Daten. Es ging nicht mehr darum, was sie brauchten oder wollten, aber die Bevölkerung hatte das Gefühl, dass sie es einfach verdient hatten und noch mehr forderten.
Mit kaum mehr als Kassen voller Beute bewaffnet, erreichte das Unternehmen den Markt und stellte Entwickler und Administratoren ein, um das stetig wachsende System zu unterstützen. Söldner aller Ethos strömten in das Unternehmen, aber mit diesem Wachstumsschub standen der verfügbaren fachkundigen Anleitung wenig im Wege. Neue Entwickler und Administratoren hatten Mühe, die Feinheiten der selbst gebrauten Suite zu verstehen, bis die Frustrationen zu einem Krieg führten. Jede Abteilung begann zu versuchen, jedes Problem alleine zu lösen und mehr gegeneinander zu arbeiten als miteinander zu arbeiten. Ein einzelnes Projekt oder eine einzelne Initiative würde auf verschiedene Arten umgesetzt, die sich geringfügig voneinander unterscheiden. Die Belastung war für einige der weißen Ritter zu groß, und als sie fielen, brach das Reich zusammen. Bald war das System in Trümmern,
Trotz der Umwandlung dieser vielversprechenden Bereiche in blutigen Spaghetti-Code blieb das Unternehmen bestehen. Es war schließlich "Gut genug".
Die Herausforderung
Ein paar weitere Regimewechsel und Einstellungen später stelle ich fest, dass ich bei der Firma angestellt bin. Seit den großen Kriegen sind viele Jahre vergangen, aber der angerichtete Schaden ist immer noch sehr sichtbar. Ich habe es geschafft, einige der Schwachstellen im E-Teil des Systems zu beheben und einige Steuertabellen hinzuzufügen, während ich die DTS-Pakete auf SSIS aktualisiert habe, die jetzt von einigen tatsächlichen Data Warehousing-Experten verwendet werden, während sie eine normale erstellen und dokumentierter T- und L-Austausch.
Die erste Hürde bestand darin, die Daten aus den Dateien von Drittanbietern so zu importieren, dass die Werte nicht abgeschnitten oder die nativen Datentypen geändert werden, sondern auch einige Steuerschlüssel zum erneuten Laden und Löschen enthalten. Das war alles schön und gut, aber die Anwendungen mussten nahtlos und transparent auf diese neuen Tabellen zugreifen können. Ein DTS-Paket kann eine Tabelle füllen, die dann direkt von der Anwendung gelesen wird. Die SSIS-Upgrades müssen aus QS-Gründen parallel durchgeführt werden. Diese neuen Pakete enthalten jedoch verschiedene Steuerschlüssel und nutzen auch ein Partitionierungsschema. Ganz zu schweigen davon, dass die tatsächlichen Metadatenänderungen allein so bedeutend sein können, dass eine neue Tabelle ohnehin insgesamt gerechtfertigt ist Für die neuen SSIS-Pakete wurde eine neue Tabelle verwendet.
Da zuverlässige Datenimporte jetzt funktionieren und vom Warehousing-Team verwendet werden, besteht die eigentliche Herausforderung darin, die neuen Daten den Anwendungen bereitzustellen, die direkt auf die Staging-Umgebung zugreifen, mit minimalen (auch als "Nein" bezeichneten) Auswirkungen auf den Anwendungscode. Dazu habe ich Gebrauch Ansichten gewählt, eine Tabelle umbenennen, wie dbo.DailyTransaction
zu dbo.DailyTranscation_LEGACY
und Wiederverwendung des dbo.DailyTransaction
Objektnamens für eine Ansicht, die alles wählt effektiv nur von der jetztLEGACY
bezeichnete Tabelle. Da das erneute Laden der in diesen Tabellen enthaltenen jahrelangen Daten aus geschäftlicher Sicht keine Option ist, da die neuen SSIS-gefüllten und partitionierten Tabellen ihren Weg in die Produktion finden, werden die alten DTS-Importe deaktiviert und die Anwendungen müssen dies können Greifen Sie auch auf die neuen Daten in den neuen Tabellen zu. Zu diesem Zeitpunkt werden die Ansichten aktualisiert, um die Daten aus den neuen Tabellen ( dbo.DailyTransactionComplete
z. B.) auszuwählen, wenn sie verfügbar sind, und aus den Legacy-Tabellen auszuwählen, wenn dies nicht der Fall ist.
In der Tat wird so etwas wie das Folgende getan:
CREATE VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE t.FileDate = l.FileDate );
Dies ist zwar logisch einwandfrei, funktioniert jedoch in einer Reihe von Aggregationsfällen überhaupt nicht. Dies führt im Allgemeinen zu einem Ausführungsplan, der einen vollständigen Index-Scan für die Daten in der Legacy-Tabelle durchführt. Dies ist wahrscheinlich in Ordnung für ein paar Dutzend Millionen Datensätze, aber nicht so sehr für ein paar Dutzend Hundert Millionen Datensätze. Da letzteres tatsächlich der Fall ist, musste ich ... "kreativ" sein, was mich dazu führte, eine indizierte Ansicht zu erstellen.
Hier ist der kleine Testfall, den ich eingerichtet habe, einschließlich des FileDate
Steuerschlüssels, der auf den Data Warehouse-kompatiblen DateCode_FK
Port portiert wurde, um zu veranschaulichen, wie wenig mir die Abfragen gegen die neue Tabelle vorerst wichtig sind:
USE tempdb;
GO
SET NOCOUNT ON;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction_LEGACY'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransaction_LEGACY;
CREATE TABLE dbo.DailyTransaction_LEGACY
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
FileDate DATETIME NOT NULL,
Foo INT NOT NULL
);
INSERT INTO dbo.DailyTransaction_LEGACY ( FileDate, Foo )
SELECT DATEADD( DAY, ( 1 - ROW_NUMBER()
OVER( ORDER BY so1.object_id ) - 800 ) % 1000,
CONVERT( DATE, GETDATE() ) ),
so1.object_id % 1000 + so2.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransaction_LEGACY
ADD CONSTRAINT PK__DailyTrainsaction
PRIMARY KEY CLUSTERED ( DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransactionComplete'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransactionComplete;
CREATE TABLE dbo.DailyTransactionComplete
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
DateCode_FK INTEGER NOT NULL,
Foo INTEGER NOT NULL
);
INSERT INTO dbo.DailyTransactionComplete ( DateCode_FK, Foo )
SELECT TOP 100000
CONVERT( INTEGER, CONVERT( VARCHAR( 8 ), DATEADD( DAY,
( 1 - ROW_NUMBER() OVER( ORDER BY so1.object_id ) ) % 100,
GETDATE() ), 112 ) ),
so1.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransactionComplete
ADD CONSTRAINT PK__DailyTransaction
PRIMARY KEY CLUSTERED ( DateCode_FK, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
In meiner lokalen Sandbox erhalte ich oben eine Legacy-Tabelle mit etwa 4,4 Millionen Zeilen und eine neue Tabelle mit 0,1 Millionen Zeilen mit einer gewissen Überlappung der DateCode_FK
/ FileDate
-Werte.
Eine MAX( FileDate )
gegen die Legacy-Tabelle ohne zusätzliche Indizes läuft ungefähr so, wie ich es erwarten würde.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Tabelle 'DailyTransaction_LEGACY'. Scananzahl 1, logische Lesevorgänge 9228, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.
SQL Server-Ausführungszeiten: CPU-Zeit = 889 ms, verstrichene Zeit = 886 ms.
Wenn Sie einen einfachen Index auf den Tisch werfen, werden die Dinge viel besser. Immer noch ein Scan, aber ein Scan eines Datensatzes anstelle der 4,4 Millionen Datensätze. Ich bin cool damit.
CREATE NONCLUSTERED INDEX IX__DailyTransaction__FileDate
ON dbo.DailyTransaction_LEGACY ( FileDate );
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
SQL Server-Analyse- und Kompilierungszeit: CPU-Zeit = 0 ms, verstrichene Zeit = 1 ms. Tabelle 'DailyTransaction_LEGACY'. Scananzahl 1, logische Lesevorgänge 3, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.
SQL Server-Ausführungszeiten: CPU-Zeit = 0 ms, verstrichene Zeit = 0 ms.
Und jetzt erstellen Sie die Ansicht, damit die Entwickler keinen Code ändern müssen, denn das wäre anscheinend das Ende der Welt, wie wir sie kennen. Eine Art Katastrophe.
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate = CONVERT(
DATETIME, CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
t.DateCode_FK ), 112 ) = l.FileDate );
GO
Ja, die Unterabfrage ist miserabel, aber dies ist nicht das Problem, und ich werde wahrscheinlich einfach eine persistierte berechnete Spalte erstellen und zu diesem Zweck einen Index darauf werfen, wenn das eigentliche Problem gelöst ist. Also ohne weiteres,
Das Problem
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
SQL Server-Analyse- und Kompilierungszeit: CPU-Zeit = 0 ms, verstrichene Zeit = 4 ms. Tabelle 'DailyTransaction_LEGACY'. Scananzahl 1, logische Lesevorgänge 11972, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'Arbeitstabelle'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'Arbeitsdatei'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'DailyTransactionComplete'. Scananzahl 2, logische Lesevorgänge 620, physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.
SQL Server-Ausführungszeiten: CPU-Zeit = 983 ms, verstrichene Zeit = 983 ms.
Oh, ich verstehe, SQL Server versucht mir zu sagen, dass das, was ich tue, idiotisch ist. Obwohl ich weitgehend zustimme, ändert dies nichts an meiner Lage. Das funktioniert hervorragend für Abfragen , wo die FileDate
auf der dbo.DailyTransaction
Ansicht in dem Prädikat enthalten ist, aber während des MAX
Plan schon schlimm genug ist, der TOP
sendet Plan die ganze Sache läuft Süden. Echter Süden.
SET STATISTICS IO, TIME ON;
SELECT TOP 10 FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Tabelle 'DailyTransactionComplete'. Scananzahl 2, logische Lesevorgänge 1800110, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'DailyTransaction_LEGACY'. Scananzahl 1, logische Lesevorgänge 1254, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'Arbeitstabelle'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'Arbeitsdatei'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.
SQL Server-Ausführungszeiten: CPU-Zeit = 109559 ms, verstrichene Zeit = 109664 ms.
Ich erwähnte früher, "kreativ" zu werden, was wahrscheinlich irreführend war. Was ich sagen wollte, war "dümmer". Meine Versuche, diese Ansicht während Aggregationsvorgängen zum Laufen zu bringen, bestanden darin, Ansichten für die Tabellen dbo.DailyTransactionComplete
und zu erstellen dbo.DailyTransaction_LEGACY
, die letztere zu binden und zu indizieren und diese Ansicht dann in einer anderen Ansicht mit einem NOEXPAND
Hinweis zu verwenden auf der Legacy-Ansicht. Während es mehr oder weniger für das funktioniert, was es jetzt tun muss, finde ich die gesamte "Lösung" ziemlich verstörend und gipfelt in Folgendem:
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransactionComplete'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransactionComplete AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransactionComplete
AS SELECT DailyTransaction_PK, FileDate = CONVERT( DATETIME,
CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ),
Foo
FROM dbo.DailyTransactionComplete;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransaction_LEGACY'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransaction_LEGACY AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransaction_LEGACY
WITH SCHEMABINDING
AS SELECT l.DailyTransaction_PK,
l.FileDate,
l.Foo,
CountBig = COUNT_BIG( * )
FROM dbo.DailyTransaction_LEGACY l
INNER JOIN dbo.DailyTransactionComplete n
ON l.FileDate <> CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
n.DateCode_FK ), 112 )
GROUP BY l.DailyTransaction_PK,
l.FileDate,
l.Foo;
GO
CREATE UNIQUE CLUSTERED INDEX CI__v_DailyTransaction_LEGACY
ON dbo.v_DailyTransaction_LEGACY ( FileDate, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 80 );
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransaction_LEGACY WITH ( NOEXPAND );
GO
Wenn der Optimierer gezwungen wird, den von der indizierten Ansicht bereitgestellten Index zu verwenden, verschwinden die MAX
und TOP
Probleme, aber es muss einen besseren Weg geben, um das zu erreichen, was ich hier versuche. Absolut alle Vorschläge / Schelte wäre sehr dankbar !!
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tabelle 'v_DailyTransaction_LEGACY'. Scananzahl 1, logische Lesevorgänge 3, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'DailyTransactionComplete'. Scananzahl 1, logische Lesevorgänge 310, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.
SQL Server-Ausführungszeiten: CPU-Zeit = 31 ms, verstrichene Zeit = 36 ms.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT TOP 10 @ConsumeOutput1 = FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Tabelle 'v_DailyTransaction_LEGACY'. Scananzahl 1, logische Lesevorgänge 101, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'Arbeitstabelle'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorauslesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'Arbeitsdatei'. Scananzahl 0, logische Lesevorgänge 0, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0. Tabelle 'DailyTransactionComplete'. Scananzahl 1, logische Lesevorgänge 310, physische Lesevorgänge 0, Vorlesevorgänge 0, Lob-Lesevorgänge 0, Lob-Lesevorgänge 0, Lobvorlesevorgänge 0.
SQL Server-Ausführungszeiten: CPU-Zeit = 63 ms, verstrichene Zeit = 66 ms.
TL; DR:
Helfen Sie mir zu verstehen, was ich tun muss, um Aggregationsabfragen in der ersten Ansicht, die ich erwähnt habe, in angemessener Zeit mit angemessener E / A-Ressourcennutzung auszuführen.