Perché NOLOCK rallenta la scansione con assegnazione variabile?


11

Sto combattendo contro NOLOCK nel mio ambiente attuale. Un argomento che ho sentito è che l'overhead del blocco rallenta una query. Quindi, ho ideato un test per vedere quanto potrebbe essere questo sovraccarico.

Ho scoperto che NOLOCK in realtà rallenta la mia scansione.

All'inizio ero felice, ma ora sono solo confuso. Il mio test non è valido in qualche modo? NOLOCK non dovrebbe effettivamente consentire una scansione leggermente più veloce? Cosa sta succedendo qui?

Ecco la mia sceneggiatura:

USE TestDB
GO

--Create a five-million row table
DROP TABLE IF EXISTS dbo.JustAnotherTable
GO

CREATE TABLE dbo.JustAnotherTable (
ID INT IDENTITY PRIMARY KEY,
notID CHAR(5) NOT NULL )

INSERT dbo.JustAnotherTable
SELECT TOP 5000000 'datas'
FROM sys.all_objects a1
CROSS JOIN sys.all_objects a2
CROSS JOIN sys.all_objects a3

/********************************************/
-----Testing. Run each multiple times--------
/********************************************/
--How fast is a plain select? (I get about 587ms)
DECLARE @trash CHAR(5), @dt DATETIME = SYSDATETIME()

SELECT @trash = notID  --trash variable prevents any slowdown from returning data to SSMS
FROM dbo.JustAnotherTable
ORDER BY ID
OPTION (MAXDOP 1)

SELECT DATEDIFF(MILLISECOND,@dt,SYSDATETIME())

----------------------------------------------
--Now how fast is it with NOLOCK? About 640ms for me
DECLARE @trash CHAR(5), @dt DATETIME = SYSDATETIME()

SELECT @trash = notID
FROM dbo.JustAnotherTable (NOLOCK)
ORDER BY ID --would be an allocation order scan without this, breaking the comparison
OPTION (MAXDOP 1)

SELECT DATEDIFF(MILLISECOND,@dt,SYSDATETIME())

Quello che ho provato che non ha funzionato:

  • In esecuzione su server diversi (stessi risultati, i server erano 2016-SP1 e 2016-SP2, entrambi silenziosi)
  • In esecuzione su dbfiddle.uk su versioni diverse (risultati rumorosi, ma probabilmente gli stessi)
  • IMPOSTA LIVELLO ISOLAMENTO anziché suggerimenti (stessi risultati)
  • Disattivazione dell'escalation dei blocchi sul tavolo (stessi risultati)
  • Esame del tempo di esecuzione effettivo della scansione nel piano di query effettivo (stessi risultati)
  • Ricompila suggerimento (stessi risultati)
  • Filegroup di sola lettura (stessi risultati)

L'esplorazione più promettente viene dalla rimozione della variabile cestino e dall'utilizzo di una query senza risultati. Inizialmente questo ha mostrato NOLOCK leggermente più veloce, ma quando ho mostrato la demo al mio capo, NOLOCK è tornato ad essere più lento.

Cos'è NOLOCK che rallenta una scansione con assegnazione variabile?


Ci vorrebbe qualcuno con accesso al codice sorgente e un profiler per dare una risposta definitiva. Ma NOLOCK deve fare qualche lavoro aggiuntivo per assicurarsi che non entri in un ciclo infinito in presenza di dati mutanti. E potrebbero esserci delle ottimizzazioni disabilitate (ovvero mai testate) per le query NOLOCK.
David Browne - Microsoft,

1
Nessuna replica per me su Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) localdb.
Martin Smith,

Risposte:


12

NOTA: questo potrebbe non essere il tipo di risposta che stai cercando. Ma forse sarà utile ad altri potenziali risponditori per quanto riguarda la fornitura di indizi su dove iniziare a cercare

Quando eseguo queste query con la traccia ETW (usando PerfView), ottengo i seguenti risultati:

Plain  - 608 ms  
NOLOCK - 659 ms

Quindi la differenza è di 51ms . Questo è abbastanza morto con la tua differenza (~ 50ms). I miei numeri sono leggermente più alti nel complesso a causa del sovraccarico di campionamento del profiler.

Trovare la differenza

Ecco un confronto side-by-side che mostra che la differenza di 51ms è nel FetchNextRowmetodo in sqlmin.dll:

FetchNextRow

La selezione normale è a sinistra a 332 ms, mentre la versione nolock è a destra a 383 ( 51 ms più a lungo). Puoi anche vedere che i due percorsi del codice differiscono in questo modo:

  • pianura SELECT

    • sqlmin!RowsetNewSS::FetchNextRow chiamate
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • utilizzando NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow chiamate
      • sqlmin!DatasetSession::GetNextRowValuesNoLock che chiama neanche
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal o
        • kernel32!TlsGetValue

Ciò dimostra che nel FetchNextRowmetodo sono presenti alcune ramificazioni basate sul livello di isolamento / suggerimento nolock.

Perché il NOLOCKramo impiega più tempo?

Il ramo nolock in realtà impiega meno tempo a chiamare GetNextRowValuesInternal(25ms in meno). Ma il codice direttamente in GetNextRowValuesNoLock(esclusi i metodi che chiama AKA la colonna "Exc") funziona per 63ms - che è la maggior parte della differenza (63 - 25 = aumento netto di 38ms nel tempo della CPU).

Quindi quali sono gli altri 13ms (51ms totali - 38ms rappresentati finora) dell'overhead FetchNextRow?

Invio dell'interfaccia

Ho pensato che fosse più una curiosità che altro, ma la versione di nolock sembra incorrere in un overhead di invio dell'interfaccia chiamando il metodo API di Windows kernel32!TlsGetValuetramite kernel32!TlsGetValueStub- per un totale di 17ms. La selezione semplice sembra non passare attraverso l'interfaccia, quindi non colpisce mai lo stub e spende solo 6ms TlsGetValue(una differenza di 11ms ). Puoi vederlo sopra nel primo screenshot.

Probabilmente dovrei eseguire di nuovo questa traccia con più iterazioni della query, penso che ci siano alcune piccole cose, come gli interrupt hardware, che non sono stati rilevati dalla frequenza di campionamento di 1 ms di PerfView


Al di fuori di quel metodo, ho notato un'altra piccola differenza che fa rallentare la versione di nolock:

Rilascio di serrature

Il ramo nolock sembra eseguire il sqlmin!RowsetNewSS::ReleaseRowsmetodo in modo più aggressivo , che puoi vedere in questo screenshot:

Rilascio di serrature

La selezione semplice è in alto, a 12 ms, mentre la versione nolock è in basso a 26 ms ( 14 ms in più). Puoi anche vedere nella colonna "Quando" che il codice è stato eseguito più frequentemente durante l'esempio. Questo potrebbe essere un dettaglio di implementazione di nolock, ma sembra introdurre un po 'di sovraccarico per piccoli campioni.


Ci sono molte altre piccole differenze, ma quelli sono i grandi pezzi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.