Lo scenario
C'era una volta un database di gestione temporanea presso una piccola azienda che partecipava a un processo ETL, fungendo da catalogo di ricezione per i vari formati di file da una serie di fonti di terze parti. L'E è stata gestita attraverso pacchetti DTS, con poche strutture di controllo per l'auditing o il controllo, ma è stata considerata "abbastanza buona" e, a tutti gli effetti, lo è stata.
I dati forniti dalla porzione E erano destinati al consumo da un'unica applicazione, sviluppata e gestita da una manciata di programmatori giovani e capaci. Sebbene non avessero esperienza o conoscenza delle tecniche di data warehousing dell'epoca, hanno avviato e creato i propri processi T e L dal codice dell'applicazione. Sorprendentemente, questi ingegneri software alle prime armi hanno inventato quella che gli estranei potrebbero definire una "ruota tutt'altro che ideale", ma con "Abbastanza buono" come un livello di servizio sempre presente, sono stati in grado di fornire un quadro operativo.
Per un po ', tutto andò bene nel regno strettamente accoppiato, con il catalogo Staging che si dilettava con i dati di una dozzina di terze parti, a sua volta alimentata dall'applicazione. Man mano che l'applicazione cresceva, aumentavano anche i suoi appetiti, ma con gli abili sviluppatori del cavaliere bianco che sorvegliavano il sistema, questi appetiti venivano affrontati rapidamente e in molti casi, anche bene.
Ma l'età d'oro non poteva durare per sempre, ovviamente. Con la prosperità garantita dall'applicazione di successo, il business è cresciuto e cresciuto. Man mano che cresceva, l'ambiente di gestione temporanea e l'applicazione erano costretti a crescere con esso. Nonostante tutta la loro vigilanza, il solo pugno di sviluppatori di eroi non ha potuto tenere il passo con il mantenimento del sistema ora espansivo, e i consumatori erano diventati titolari dei loro dati. Non era più una questione di ciò di cui avevano bisogno o addirittura desideravano, ma la popolazione sentiva che se lo meritava semplicemente, chiedendo ancora di più.
Armato di poco più di casse piene di malloppo, l'azienda ha raggiunto il mercato, assumendo sviluppatori e amministratori per aiutare a supportare il sistema in continua crescita. Mercenari di ogni genere si affollarono nell'azienda, ma con questo scatto di crescita arrivò poco in termini di guida esperta disponibile. I nuovi sviluppatori e amministratori hanno faticato a comprendere le complessità della suite prodotta in casa, fino a quando le frustrazioni non hanno portato alla guerra. Ogni dipartimento ha iniziato a tentare di risolvere da solo ogni problema, facendo di più per lavorare l'uno contro l'altro che per lavorare l'uno con l'altro. Un singolo progetto o iniziativa sarebbe attuato in diversi modi, ognuno leggermente diverso dal successivo. La tensione di tutto ciò si rivelò eccessiva per alcuni cavalieri bianchi e mentre cadevano, l'impero si sbriciolò. Presto il sistema era in rovina,
Nonostante la trasformazione di questi campi promettenti in codice spaghetti spaghetti, l'azienda ha resistito. Dopotutto, era "abbastanza buono".
La sfida
Qualche altro cambio di regime e assunzioni folli in seguito, mi ritrovo nell'impiego dell'azienda. Sono passati molti anni dalle grandi guerre, ma il danno fatto è ancora molto visibile. Sono riuscito a risolvere alcuni dei punti deboli nella parte E del sistema e ad aggiungere alcune tabelle di controllo con il pretesto di aggiornare i pacchetti DTS a SSIS, che ora vengono utilizzati da alcuni professionisti di data warehousing mentre creano un normale e documentata sostituzione T e L.
Il primo ostacolo era importare i dati dai file di terze parti in modo da non troncare i valori o modificare i tipi di dati nativi, ma includere anche alcune chiavi di controllo per ricariche ed eliminazioni. Tutto ciò andava bene, ma le applicazioni dovevano poter accedere a questi nuovi tavoli in modo trasparente e trasparente. Un pacchetto DTS può popolare una tabella, che viene quindi letta direttamente dall'applicazione. Gli aggiornamenti SSIS devono essere eseguiti in parallelo per motivi di QA, ma questi nuovi pacchetti includono varie chiavi di controllo e sfruttano anche uno schema di partizionamento, per non parlare delle sole modifiche ai metadati da sole possono essere abbastanza significative da garantire comunque una nuova tabella, quindi un nuova tabella è stata utilizzata per i nuovi pacchetti SSIS.
Con le importazioni di dati affidabili ora funzionanti e utilizzate dal team di magazzino, la vera sfida consiste nel fornire i nuovi dati alle applicazioni che accedono direttamente all'ambiente di gestione temporanea, con un impatto minimo (noto anche come "No") sul codice dell'applicazione. Per questo, ho deciso di vista di utilizzo, la ridenominazione di un tavolo, come dbo.DailyTransaction
per dbo.DailyTranscation_LEGACY
e riutilizzare il dbo.DailyTransaction
nome dell'oggetto per una vista, che di fatto appena seleziona tutto dalla societàLEGACY
tabella designata. Dal momento che ricaricare gli anni di dati contenuti in queste tabelle non è un'opzione dal punto di vista aziendale, poiché le nuove tabelle popolate e partizionate da SSIS si fanno strada nella produzione, le vecchie importazioni DTS vengono disattivate e le applicazioni devono essere in grado di accedere ai nuovi dati anche nelle nuove tabelle. A questo punto, le viste vengono aggiornate per selezionare i dati dalle nuove tabelle (ad esempio dbo.DailyTransactionComplete
, ad esempio) quando sono disponibili e selezionare dalle tabelle legacy quando non lo sono.
In effetti, si sta eseguendo qualcosa di simile al seguente:
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 );
Sebbene logicamente corretto, ciò non funziona affatto in numerosi casi di aggregazione, generando in genere un piano di esecuzione che esegue una scansione completa dell'indice rispetto ai dati nella tabella legacy. Questo probabilmente va bene per qualche dozzina di milioni di dischi, ma non tanto per qualche dozzina di milioni di dischi. Dato che quest'ultimo è in realtà il caso, ho dovuto ricorrere all'essere ... "creativo", portandomi a creare una vista indicizzata.
Ecco il piccolo caso di test che ho impostato, incluso il fatto che la FileDate
chiave di controllo sia stata trasferita alla DateCode_FK
porta compatibile con Data Warehouse per illustrare quanto poco mi preoccupi del fatto che le query sulla nuova tabella siano per il momento estese :
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
Sulla mia sandbox locale, quanto sopra mi dà una tabella legacy con circa 4,4 milioni di righe e una nuova tabella contenente 0,1 milioni di righe, con qualche sovrapposizione dei valori DateCode_FK
/ FileDate
.
A MAX( FileDate )
contro la tabella legacy senza indici aggiuntivi si imbatte in ciò che mi aspetterei.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Tabella "DailyTransaction_LEGACY". Conteggio scansioni 1, letture logiche 9228, letture fisiche 0, letture read-ahead 0, letture log lob 0, letture fisiche lob 0, letture read lob 0.
Tempi di esecuzione di SQL Server: tempo CPU = 889 ms, tempo trascorso = 886 ms.
Lanciare un semplice indice sul tavolo rende le cose molto migliori. Ancora una scansione, ma una scansione di un record invece dei 4,4 milioni di record. Sono a posto con quello.
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
Tempo di analisi e compilazione di SQL Server: tempo CPU = 0 ms, tempo trascorso = 1 ms. Tabella "DailyTransaction_LEGACY". Conteggio scansioni 1, letture logiche 3, letture fisiche 0, letture avanti 0, letture logiche lob 0, letture fisiche lob 0, letture read lob 0.
Tempi di esecuzione di SQL Server: tempo CPU = 0 ms, tempo trascorso = 0 ms.
E ora, creando la vista in modo che gli sviluppatori non debbano cambiare alcun codice perché apparentemente sarebbe la fine del mondo come la conosciamo. Una specie di cataclisma.
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
Sì, la query secondaria è abissale, ma questo non è il problema e probabilmente creerò semplicemente una colonna calcolata persistente e vi lancerò un indice a tale scopo quando il problema reale sarà risolto. Quindi senza ulteriori indugi,
Il problema
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tempo di analisi e compilazione di SQL Server: tempo CPU = 0 ms, tempo trascorso = 4 ms. Tabella "DailyTransaction_LEGACY". Conteggio scansioni 1, letture logiche 11972, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella 'Worktable'. Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella "File di lavoro". Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella 'DailyTransactionComplete'. Conteggio scansioni 2, letture logiche 620, letture fisiche 0, letture avanti 0, letture logiche lob 0, letture fisiche lob 0, letture read lob 0.
Tempi di esecuzione di SQL Server: tempo CPU = 983 ms, tempo trascorso = 983 ms.
Oh capisco, SQL Server sta cercando di dirmi che quello che sto facendo è un idiota. Mentre sono in gran parte d'accordo, ciò non cambia la mia situazione. Questo in realtà funziona brillantemente per le query in cui l' FileDate
sulla dbo.DailyTransaction
vista è incluso nel predicato, ma mentre il MAX
piano è già abbastanza grave, il TOP
piano di invia il tutto in esecuzione sud. Vero 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
Tabella 'DailyTransactionComplete'. Conteggio scansioni 2, letture logiche 1800110, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella 'DailyTransaction_LEGACY'. Conteggio scansioni 1, letture logiche 1254, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read avanti lob 0. Tabella "Tavolo da lavoro". Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella "File di lavoro". Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob 0.
Tempi di esecuzione di SQL Server: tempo CPU = 109559 ms, tempo trascorso = 109664 ms.
Ho accennato a diventare "creativo" in precedenza, il che probabilmente era fuorviante. Quello che intendevo dire era "più stupido", quindi i miei tentativi di far funzionare questa vista durante le operazioni di aggregazione sono stati quello di creare viste sulle tabelle dbo.DailyTransactionComplete
e dbo.DailyTransaction_LEGACY
, legare lo schema e indicizzare quest'ultima, quindi usare quelle vista in un'altra vista con un NOEXPAND
suggerimento nella vista legacy. Mentre funziona più o meno per quello che deve fare per ora, trovo che l'intera "soluzione" sia abbastanza sconvolgente, culminando con il seguente:
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
Forzare l'ottimizzatore a utilizzare l'indice fornito dalla vista indicizzata fa sì che i problemi MAX
e TOP
scompaiano, ma deve esserci un modo migliore per ottenere ciò che sto cercando di fare qui. Assolutamente ogni suggerimento / rimprovero sarebbe molto apprezzato !!
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tabella "v_DailyTransaction_LEGACY". Conteggio scansioni 1, letture logiche 3, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella 'DailyTransactionComplete'. Conteggio scansioni 1, letture logiche 310, letture fisiche 0, letture read-ahead 0, letture log lob 0, letture fisiche lob 0, letture read lob 0.
Tempi di esecuzione di SQL Server: tempo CPU = 31 ms, tempo trascorso = 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
Tabella "v_DailyTransaction_LEGACY". Conteggio scansioni 1, letture logiche 101, letture fisiche 0, letture read-ahead 0, letture log lob 0, letture fisiche lob 0, letture read lob 0. Tabella "Tavolo da lavoro". Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella "File di lavoro". Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0, letture logiche lob 0, letture fisiche lob 0, letture read lob iniziali 0. Tabella 'DailyTransactionComplete'. Conteggio scansioni 1, letture logiche 310, letture fisiche 0, letture read-ahead 0, letture log lob 0, letture fisiche lob 0, letture read lob 0.
Tempi di esecuzione di SQL Server: tempo CPU = 63 ms, tempo trascorso = 66 ms.
TL; DR:
Aiutami a capire cosa devo fare per fare query di aggregazione nella prima vista che ho menzionato eseguite in un ragionevole lasso di tempo con un ragionevole utilizzo delle risorse I / O.