Ho preso in giro i dati dei test che riproducono principalmente il tuo problema:
INSERT INTO [dbo].[TestTable] WITH (TABLOCK)
SELECT TOP (7000000) N'*NOT GDPR*', N'*NOT GDPR*', N'*NOT GDPR*', 0, DATEADD(DAY, q.RN / 16965, '20160801')
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
) q
ORDER BY q.RN
OPTION (MAXDOP 1);
DROP INDEX IF EXISTS [dbo].[TestTable].IX_TestTable_Date;
CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date]);
Statistiche per la query che utilizza l'indice non cluster:
Tabella "Tabella di test". Conteggio scansioni 1, letture logiche 1299838, 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 = 984 ms, tempo trascorso = 988 ms.
Statistiche per la query che utilizza l'indice cluster:
Tabella "Tabella di test". Conteggio scansioni 1, letture logiche 72609, 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 = 781 ms, tempo trascorso = 772 ms.
Come arrivare alla tua domanda:
È possibile sfruttare questo fatto per migliorare le prestazioni della mia query?
Sì. È possibile utilizzare l'indice non cluster che è già necessario per trovare in modo efficiente il id
valore massimo che deve essere aggiornato. Se lo si salva in una variabile e si filtra contro di esso, si ottiene un piano di query per l'aggiornamento che esegue la scansione dell'indice cluster (senza ordinamento) che si interrompe in anticipo e quindi fa meno IO. Ecco una implementazione:
DECLARE @Id INT;
SELECT TOP (1) @Id = Id
FROM dbo.TestTable
WHERE [Date] <= '25 August 2016'
ORDER BY [Date] DESC, Id DESC;
UPDATE TestTable
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Id] < @Id AND [Date] <= '25 August 2016'
AND [Anonymised] <> 1 -- optional
OPTION (MAXDOP 1);
Esegui le statistiche per la nuova query:
Tabella "Tabella di test". Conteggio scansioni 1, letture logiche 3, letture fisiche 0, letture avanti 0, letture logiche lob 0, letture fisiche lob 0, letture read lob 0.
Tabella "Tabella di test". Conteggio scansioni 1, letture logiche 4776, 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 = 515 ms, tempo trascorso = 510 ms.
Oltre al piano di query:
Detto questo, il tuo desiderio di velocizzare la query mi suggerisce che hai intenzione di eseguire la query più di una volta. In questo momento la tua query ha un filtro aperto sulla date
colonna. È davvero necessario anonimizzare le righe più di una volta? È possibile evitare l'aggiornamento o la scansione di righe che erano già anonime? Dovrebbe certamente essere più veloce aggiornare un intervallo di date con date su entrambi i lati. È inoltre possibile aggiungere la Anonymised
colonna al proprio indice, ma tale indice dovrà essere aggiornato durante la UPDATE
query. In sintesi, evitare di elaborare gli stessi dati più volte, se possibile.
La query originale che hai con l'ordinamento è più lenta a causa del lavoro svolto nell'operatore Clustered Index Update
. La quantità di tempo impiegata per la ricerca dell'indice e l'ordinamento è di soli 407 ms. Puoi vederlo nel piano reale. Il piano viene eseguito in modalità riga quindi il tempo impiegato per l'ordinamento è il tempo di quell'operatore insieme a ogni operatore figlio:
Ciò lascia l'operatore di ordinamento con circa 1600 ms di tempo. SQL Server deve leggere le pagine dall'indice cluster per eseguire l'aggiornamento. Si può vedere che l' Clustered Index Update
operatore esegue letture logiche 1205921. Puoi leggere ulteriori informazioni sull'ordinamento delle ottimizzazioni per DML e prefetch ottimizzato in questo post di Paul White .
L'altro piano di query disponibile (senza ordinamento) richiede 683 ms per la scansione dell'indice cluster e circa 550 ms per l' Clustered Index Update
operatore. L'operatore di aggiornamento non esegue alcun I / O per questa query.
La semplice risposta sul perché il piano con l'ordinamento è più lento è che SQL Server esegue letture più logiche sull'indice cluster per quel piano rispetto al piano di scansione dell'indice cluster. Anche se tutti i dati necessari sono in memoria, c'è ancora un sovraccarico e un costo per fare quelle letture logiche. Una risposta migliore è molto più difficile da ottenere, in quanto per quanto ne so i piani non ti forniranno ulteriori dettagli. È possibile utilizzare PerfView o un altro strumento basato sulla traccia ETW per confrontare gli stack di chiamate tra le query:
A sinistra è la query che esegue la scansione dell'indice cluster e a destra è la query che esegue l'ordinamento. Ho contrassegnato le pile di chiamate in blu o rosso che compaiono solo in una query. Non sorprende che le diverse pile di chiamate con un numero elevato di cicli CPU campionati per la query di ordinamento sembrano avere a che fare con le letture logiche richieste per eseguire l'aggiornamento sull'indice cluster. Inoltre, ci sono differenze nel numero di cicli campionati tra le query per la stessa operazione. Ad esempio, la query con l'ordinamento impiega 31 cicli acquisendo i latch mentre la query con la scansione impiega solo 9 cicli acquisendo i latch.
Ho il sospetto che SQL Server stia scegliendo il piano più lento a causa di una limitazione dei costi dell'operatore del piano di query. Forse parte della differenza nel tempo di esecuzione è dovuta all'hardware o all'edizione di SQL Server. In ogni caso, SQL Server non è in grado di capire che la colonna della data sia implicitamente ordinata esattamente come l'indice cluster. I dati vengono restituiti dalla scansione dell'indice cluster nell'ordine delle chiavi cluster, quindi non è necessario eseguire un ordinamento nel tentativo di ottimizzare l'Io quando si esegue l'aggiornamento dell'indice cluster.