Warum verlangsamt NOLOCK einen Scan mit variabler Zuweisung?

11

Ich kämpfe in meiner aktuellen Umgebung gegen NOLOCK. Ein Argument, das ich gehört habe, ist, dass der Aufwand für das Sperren eine Abfrage verlangsamt. Also habe ich einen Test entwickelt, um zu sehen, wie hoch dieser Overhead sein könnte.

Ich habe festgestellt, dass NOLOCK meinen Scan tatsächlich verlangsamt.

Anfangs war ich begeistert, aber jetzt bin ich nur noch verwirrt. Ist mein Test irgendwie ungültig? Sollte NOLOCK nicht tatsächlich einen etwas schnelleren Scan zulassen? Was passiert hier?

Hier ist mein Skript:

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())

Was ich versucht habe, hat nicht funktioniert:

  • Läuft auf verschiedenen Servern (gleiche Ergebnisse, Server waren 2016-SP1 und 2016-SP2, beide leise)
  • Laufen auf dbfiddle.uk auf verschiedenen Versionen (laut, aber wahrscheinlich die gleichen Ergebnisse)
  • SET ISOLATION LEVEL anstelle von Hinweisen (gleiche Ergebnisse)
  • Deaktivieren der Sperreneskalation auf dem Tisch (gleiche Ergebnisse)
  • Untersuchen der tatsächlichen Ausführungszeit des Scans im tatsächlichen Abfrageplan (gleiche Ergebnisse)
  • Hinweis neu kompilieren (gleiche Ergebnisse)
  • Schreibgeschützte Dateigruppe (gleiche Ergebnisse)

Die vielversprechendste Untersuchung ergibt sich aus dem Entfernen der Papierkorbvariablen und der Verwendung einer Abfrage ohne Ergebnisse. Anfangs zeigte dies, dass NOLOCK etwas schneller war, aber als ich meinem Chef die Demo zeigte, war NOLOCK wieder langsamer.

Was ist mit NOLOCK, das einen Scan mit variabler Zuweisung verlangsamt?

Wald
quelle
Es würde jemanden mit Quellcodezugriff und einem Profiler brauchen, um eine endgültige Antwort zu geben. NOLOCK muss jedoch einige zusätzliche Arbeiten ausführen, um sicherzustellen, dass es bei mutierten Daten nicht in eine Endlosschleife eintritt. Und es gibt möglicherweise Optimierungen, die für NOLOCK-Abfragen deaktiviert (auch bekannt als nie getestet) sind.
David Browne - Microsoft
1
Kein Repro für mich unter Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) localdb.
Martin Smith

Antworten:

12

HINWEIS: Dies ist möglicherweise nicht die Art von Antwort, nach der Sie suchen. Aber vielleicht ist es für andere potenzielle Antwortende hilfreich, Hinweise zu geben, wo sie anfangen sollen zu suchen

Wenn ich diese Abfragen unter ETW-Ablaufverfolgung (mit PerfView) ausführe, erhalte ich die folgenden Ergebnisse:

Plain  - 608 ms  
NOLOCK - 659 ms

Der Unterschied beträgt also 51 ms . Dies ist ziemlich tot mit Ihrem Unterschied (~ 50ms). Meine Zahlen sind aufgrund des Overheads der Profiler-Stichproben insgesamt etwas höher.

Den Unterschied finden

Hier ist ein Vergleich nebeneinander, der zeigt, dass der Unterschied von 51 ms in der FetchNextRowMethode in sqlmin.dll liegt:

FetchNextRow

Die einfache Auswahl befindet sich bei 332 ms links, während die Nolock-Version bei 383 ms ( 51 ms länger) rechts liegt . Sie können auch sehen, dass sich die beiden Codepfade folgendermaßen unterscheiden:

  • Einfach SELECT

    • sqlmin!RowsetNewSS::FetchNextRow Anrufe
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • Verwenden von NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow Anrufe
      • sqlmin!DatasetSession::GetNextRowValuesNoLock was entweder anruft
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal oder
        • kernel32!TlsGetValue

Dies zeigt, dass die FetchNextRowMethode basierend auf der Isolationsstufe / dem Nolock-Hinweis eine gewisse Verzweigung aufweist.

Warum dauert die NOLOCKFiliale länger?

Der Nolock-Zweig verbringt tatsächlich weniger Zeit mit Anrufen GetNextRowValuesInternal(25 ms weniger). Der Code direkt in GetNextRowValuesNoLock(ohne Methoden, die AKA als "Exc" -Spalte bezeichnen) wird jedoch für 63 ms ausgeführt - was den größten Teil des Unterschieds ausmacht (63 - 25 = 38 ms Nettozunahme der CPU-Zeit).

Wie hoch sind die anderen 13 ms (insgesamt 51 ms - bisher 38 ms) des Overheads FetchNextRow?

Schnittstellenversand

Ich dachte, dies sei mehr eine Kuriosität als alles andere, aber die Nolock-Version scheint einen gewissen Aufwand für den Schnittstellenversand zu verursachen, wenn die Windows-API-Methode kernel32!TlsGetValueüber kernel32!TlsGetValueStub- insgesamt 17 ms - aufgerufen wird. Die einfache Auswahl scheint nicht über die Schnittstelle zu gehen, trifft also nie den Stub und verbringt nur 6 ms TlsGetValue(ein Unterschied von 11 ms ). Sie können dies oben im ersten Screenshot sehen.

Ich sollte diesen Trace wahrscheinlich mit mehr Iterationen der Abfrage erneut ausführen. Ich denke, es gibt einige kleine Dinge, wie Hardware-Interrupts, die von der 1-ms-Abtastrate von PerfView nicht erfasst wurden


Außerhalb dieser Methode habe ich einen weiteren kleinen Unterschied festgestellt, der dazu führt, dass die Nolock-Version langsamer ausgeführt wird:

Schlösser lösen

Der Nolock-Zweig scheint die sqlmin!RowsetNewSS::ReleaseRowsMethode aggressiver auszuführen , was Sie in diesem Screenshot sehen können:

Sperren lösen

Die einfache Auswahl befindet sich oben bei 12 ms, während die Nolock-Version unten bei 26 ms ( 14 ms länger) liegt. Sie können auch in der Spalte "Wann" sehen, dass der Code während des Beispiels häufiger ausgeführt wurde. Dies mag ein Implementierungsdetail von nolock sein, aber es scheint für kleine Stichproben einen erheblichen Overhead zu bedeuten.


Es gibt viele andere kleine Unterschiede, aber das sind die großen Stücke.

Josh Darnell
quelle