Le scénario
Il était une fois une base de données Staging dans une petite entreprise qui participait à un processus ETL, agissant comme un catalogue de réception pour les différents formats de fichiers provenant d'un certain nombre de sources tierces. L'E a été géré par le biais de packages DTS, avec peu de structures de contrôle pour l'audit ou le contrôle, mais a été jugé «assez bon» et à toutes fins utiles, il l'était.
Les données fournies par la partie E étaient destinées à être utilisées par une application unique, développée et gérée par une poignée de programmeurs jeunes et compétents. Bien que manquant d'expérience ou de connaissances des techniques d'entreposage de données de l'époque, ils ont exposé et créé leurs propres processus T et L à partir du code d'application. Fulgurant, ces ingénieurs logiciels novices ont inventé ce que les étrangers pourraient appeler une "roue moins qu'idéale", mais avec "Good Enough" comme niveau de service toujours présent, ils ont pu fournir un cadre opérationnel.
Pendant un temps, tout allait bien dans le domaine étroitement couplé, le catalogue Staging se nourrissant des données d'une douzaine de tiers, à son tour alimenté par l'application. Au fur et à mesure que l'application grandissait, ses appétits augmentaient également, mais avec les développeurs habiles du chevalier blanc qui surveillaient le système, ces appétits ont été traités rapidement et, dans de nombreux cas, même bien.
Mais l'âge d'or ne pouvait pas durer éternellement, bien sûr. Avec la prospérité accordée par l'application réussie, l'entreprise a grandi et grandi. Au fur et à mesure de sa croissance, l'environnement et l'application Staging ont été contraints de croître avec lui. Malgré toute leur vigilance, la seule poignée de développeurs de héros n'a pas pu suivre le maintien du système désormais étendu, et les consommateurs avaient désormais droit à leurs données. Ce n'était plus une question de ce dont ils avaient besoin ou même voulu, mais la population estimait qu'ils le méritaient simplement, exigeant encore plus.
Armée d'un peu plus que des coffres remplis de butin, l'entreprise a pénétré le marché en embauchant des développeurs et des administrateurs pour aider à soutenir le système en constante croissance. Des mercenaires de tous les horizons affluent vers l'entreprise, mais avec cette poussée de croissance, peu de conseils d'experts sont disponibles. Les nouveaux développeurs et administrateurs ont eu du mal à comprendre les subtilités de la suite maison, jusqu'à ce que les frustrations entraînent une guerre totale. Chaque département a commencé à tenter de résoudre chaque problème seul, en faisant plus pour travailler les uns contre les autres que travailler les uns avec les autres. Un seul projet ou initiative serait mis en œuvre de plusieurs manières différentes, chacune légèrement différente de la suivante. La tension de tout cela s'est avérée être trop pour certains des chevaliers blancs et comme ils sont tombés, l'empire s'est effondré. Bientôt, le système était en ruine,
Malgré la transformation de ces domaines de promesse en code de spaghetti sanglant, l'entreprise a perduré. C'était, après tout, «assez bon».
Le défi
Quelques changements de régime et embauches plus tard, je me retrouve dans l'emploi de l'entreprise. Cela fait de nombreuses années depuis les grandes guerres, mais les dégâts causés sont encore très visibles. J'ai réussi à corriger certaines des faiblesses de la partie E du système et à ajouter des tables de contrôle sous le prétexte de mettre à niveau les packages DTS vers SSIS, qui sont maintenant utilisés par certains professionnels de l'entreposage de données car ils créent un environnement normal et remplacement de T et L documenté.
Le premier obstacle était d'importer les données à partir des fichiers tiers d'une manière qui ne tronquerait pas les valeurs ou ne changerait pas les types de données natifs, mais inclurait également des clés de contrôle pour les rechargements et les purges. Tout allait bien, mais les applications devaient pouvoir accéder à ces nouvelles tables de manière transparente et transparente. Un package DTS peut remplir une table, qui est ensuite directement lue par l'application. Les mises à niveau SSIS doivent être effectuées en parallèle pour des raisons d'assurance qualité, mais ces nouveaux packages incluent diverses clés de contrôle et tirent également parti d'un schéma de partitionnement, sans oublier que les changements de métadonnées réels peuvent à eux seuls être suffisamment importants pour justifier une nouvelle table de toute façon, donc un une nouvelle table a été utilisée pour les nouveaux packages SSIS.
Les importations de données fiables étant désormais opérationnelles et utilisées par l'équipe d'entreposage, le véritable défi consiste à servir les nouvelles données aux applications qui accèdent directement à l'environnement de transfert, avec un impact minimal (alias "Non") sur le code de l'application. Pour cela, j'ai choisi de vue de l' utilisation, de renommer une table par exemple dbo.DailyTransaction
pour dbo.DailyTranscation_LEGACY
et réutiliser le dbo.DailyTransaction
nom de l' objet pour une vue, qui a effectivement tout sélectionne tout le maintenantLEGACY
table désignée. Étant donné que le rechargement des années de données contenues dans ces tables n'est pas une option du point de vue de l'entreprise, alors que les nouvelles tables SSIS peuplées et partitionnées entrent en production, les anciennes importations DTS sont désactivées et les applications doivent pouvoir accéder également aux nouvelles données dans les nouvelles tables. À ce stade, les vues sont mises à jour pour sélectionner les données des nouvelles tables ( dbo.DailyTransactionComplete
par exemple, par exemple) lorsqu'elles sont disponibles et les sélectionner dans les tables héritées lorsqu'elles ne le sont pas.
En fait, quelque chose comme ce qui suit est en cours:
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 );
Bien que logique, cela ne fonctionne pas du tout dans un certain nombre de cas d'agrégation, ce qui entraîne généralement un plan d'exécution qui effectue une analyse complète de l'index par rapport aux données de la table héritée. C'est probablement bien pour quelques dizaines de millions d'enregistrements, mais pas tant pour quelques dizaines de centaines de millions d'enregistrements. Étant donné que ce dernier est en fait le cas, j'ai dû recourir à être ... "créatif", ce qui m'a conduit à créer une vue indexée.
Voici le petit cas de test que j'ai mis en place, y compris la FileDate
clé de contrôle ayant été portée sur le DateCode_FK
port compatible de Data Warehouse pour illustrer à quel point je me soucie peu des requêtes contre la nouvelle table pouvant être discutées pour le moment:
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
Sur mon sandbox local, ce qui précède me donne une table héritée avec environ 4,4 millions de lignes et une nouvelle table contenant 0,1 million de lignes, avec un certain chevauchement des valeurs DateCode_FK
/ FileDate
.
Une MAX( FileDate )
contre la table héritée sans index supplémentaire fonctionne à peu près à quoi je m'attendrais.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Tableau 'DailyTransaction_LEGACY'. Nombre de balayages 1, lectures logiques 9228, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 889 ms, temps écoulé = 886 ms.
Lancer un simple index sur la table rend les choses bien meilleures. Encore une analyse, mais une analyse d'un enregistrement au lieu des 4,4 millions d'enregistrements. Je suis cool avec ça.
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
Temps d'analyse et de compilation SQL Server: temps CPU = 0 ms, temps écoulé = 1 ms. Tableau 'DailyTransaction_LEGACY'. Nombre de balayages 1, lectures logiques 3, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 0 ms.
Et maintenant, créer la vue pour que les développeurs n'aient pas à changer de code car ce serait apparemment la fin du monde tel que nous le connaissons. Un cataclysme en quelque sorte.
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
Oui, la sous-requête est épouvantable, mais ce n'est pas le problème et je vais probablement simplement créer une colonne calculée persistante et y jeter un index à cette fin lorsque le vrai problème sera résolu. Alors sans plus tarder,
Le problème
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Temps d'analyse et de compilation SQL Server: temps CPU = 0 ms, temps écoulé = 4 ms. Tableau 'DailyTransaction_LEGACY'. Nombre de balayages 1, lectures logiques 11972, lectures physiques 0, lectures anticipées 0, lob lectures logiques 0, lob physiques lectures 0, lob lectures anticipées lisent 0. Tableau 'Table de travail'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob physiques lectures 0, lob lectures anticipées 0. Tableau 'Workfile'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Tableau 'DailyTransactionComplete'. Nombre de balayages 2, lectures logiques 620, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 983 ms, temps écoulé = 983 ms.
Oh je vois, Sql Server essaie de me dire que ce que je fais est idiot. Bien que je sois largement d'accord, cela ne change pas ma situation. Cela fonctionne en fait brillamment pour les requêtes où la vue FileDate
sur la dbo.DailyTransaction
vue est incluse dans le prédicat, mais bien que le MAX
plan soit suffisamment mauvais, le TOP
plan envoie le tout vers le sud. Vrai sud.
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
Tableau 'DailyTransactionComplete'. Nombre de balayages 2, lectures logiques 1800110, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Tableau 'DailyTransaction_LEGACY'. Nombre de balayages 1, lectures logiques 1254, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Tableau 'Table de travail'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob physiques lectures 0, lob lectures anticipées 0. Tableau 'Workfile'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 109559 ms, temps écoulé = 109664 ms.
J'ai mentionné plus tôt que j'étais "créatif", ce qui était probablement trompeur. Ce que je voulais dire était "plus stupide", donc mes tentatives pour faire fonctionner cette vue pendant les opérations d'agrégation ont été de créer des vues sur les tables dbo.DailyTransactionComplete
et dbo.DailyTransaction_LEGACY
, de lier le schéma et d'indexer cette dernière, puis d'utiliser ces vues dans une autre vue avec un NOEXPAND
indice sur la vue héritée. Bien qu'il fonctionne plus ou moins pour ce qu'il doit faire pour l'instant, je trouve que toute la "solution" est assez bouleversante, culminant avec ce qui suit:
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
Forcer l'optimiseur à utiliser l'index fourni par la vue indexée fait disparaître les problèmes MAX
et TOP
, mais il doit y avoir un meilleur moyen de réaliser ce que j'essaie de faire ici. Absolument toute suggestion / réprimande serait très appréciée !!
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tableau 'v_DailyTransaction_LEGACY'. Nombre de balayages 1, lectures logiques 3, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Tableau 'DailyTransactionComplete'. Nombre de balayages 1, lectures logiques 310, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 31 ms, temps écoulé = 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
Tableau 'v_DailyTransaction_LEGACY'. Nombre de balayages 1, lectures logiques 101, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Tableau 'Table de travail'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob physiques lectures 0, lob lectures anticipées 0. Tableau 'Workfile'. Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0. Tableau 'DailyTransactionComplete'. Nombre de balayages 1, lectures logiques 310, lectures physiques 0, lectures anticipées 0, lectures logiques 0, lob lectures physiques 0, lob lectures anticipées 0.
Temps d'exécution SQL Server: temps CPU = 63 ms, temps écoulé = 66 ms.
TL; DR:
Aidez-moi à comprendre ce que je dois faire pour que les requêtes d'agrégation sur la première vue que j'ai mentionnée s'exécutent dans des délais raisonnables avec une utilisation raisonnable des ressources d'E / S.