Themenseite · SQL Server Locking

NOLOCK, RCSI oder SNAPSHOT? Isolationsstufen im Alltag

Angenommen, ein Bericht zählt seit Jahren 20.000 Verträge. Heute zählt er 19.583. Kein Vertrag wurde gelöscht, keiner hinzugefügt. Die Abfrage hatte WITH (NOLOCK), und während sie lief, hat jemand neue Zeilen eingefügt. In unserem Lab passierte genau das in 70 bis 87 Prozent aller Lesungen, auf jeder Version von 2017 bis 2025.

Stand 11.11.2026 · ca. 15 Minuten

Thema NOLOCK · RCSI · SNAPSHOTSQL-Abfrage 1Lab SQL Server 2017–2025
Ein Scan, während Konten im Index wandernAnteil der Lesungen mit falscher KontenmengeKonten fehlenKonten doppeltNOLOCK86 %30 %READ COMMITTED29 %95 %REPEATABLE READ18 %keineSERIALIZABLEkeinekeineRCSIkeinekeineREAD COMMITTED mit Sperren. Gemessen im PSG-Lab auf SQLServer 2025 CU10, je Stufe zwei Läufe à 8 s.

Das ist der bekannte Teil. Weniger bekannt: Auch ohne NOLOCK, unter dem ganz normalen READ COMMITTED, haben wir Zeilen doppelt und gar nicht gelesen. Und wer deshalb auf Read Committed Snapshot (RCSI) umsteigt, löst diese Probleme, ändert aber, was Trigger und Prüflogik für wahr halten. Diese Seite zeigt mit Laborcode, welche Stufe welche Fehlergebnisse erlaubt und wie Sie sicher umstellen.

In 60 Sekunden

StufeLeser warten auf SchreiberSchreiber warten auf LeserFehlende oder doppelte bestätigte Zeilen im LabNicht bestätigte Daten lesbarVersionen
READ UNCOMMITTED / NOLOCKneinneinja, beidesja–
READ COMMITTED (Standard, mit Sperren)jakurzja, beidesnein–
READ COMMITTED SNAPSHOT (RCSI)neinneinneinneinStand bei Anweisungsbeginn
REPEATABLE READjajanur fehlendenein–
SNAPSHOTneinneinnicht gemessenneinStand bei Transaktionsbeginn
SERIALIZABLEjajaneinnein–
  • Schreibsperren gelten in jeder Stufe bis zum Commit. Die Isolationsstufe regelt nur, wie gelesen wird.
  • NOLOCK liest nicht nur nicht bestätigte Daten. Es kann auch Zeilen übersehen oder doppelt liefern, die seit Jahren bestätigt sind.
  • READ COMMITTED ist kein Zeitpunktbild. Wandern Zeilen während eines Scans im Index, fehlen sie oder erscheinen doppelt.
  • RCSI liefert jeder Anweisung den bestätigten Stand von ihrem Beginn. Leser warten nicht mehr. Code, der liest, prüft und dann schreibt, braucht dafür eine Prüfung.
  • SNAPSHOT ist eine eigene Entscheidung: konsistent über die ganze Transaktion, mit Abbruch bei Update-Konflikten (Fehler 3960).

Was NOLOCK wirklich riskiert

Dass NOLOCK Daten liest, die später zurückgerollt werden, ist bekannt. Schwerer wiegt etwas anderes: Ohne Sperren darf SQL Server eine Tabelle in der Reihenfolge der Speicherzuordnung lesen statt in Schlüsselreihenfolge (Allokationsscan). Teilt sich während des Lesens eine Seite, wandern Zeilen auf eine neue Seite. Liegt diese hinter der Leseposition, liest der Scan die Zeilen ein zweites Mal. Liegt sie davor, übersieht er sie.

Lubor Kollar vom SQL Server Customer Advisory Team hat das 2007 beschrieben, Paul White 2015 erklärt. Wir haben es auf allen aktuellen Versionen mit eigenem Code nachgestellt: 20.000 stabile Zeilen, die nie geändert werden, auf vollen Seiten. Ein Schreiber fügt dazwischen neue Zeilen ein. Jeder Leser muss genau 20.000 stabile Zeilen sehen.

-- Verkürzt aus dem Lab-CodeCREATE TABLE dbo.N1 (    id     int       NOT NULL CONSTRAINT PK_N1 PRIMARY KEY CLUSTERED,    stabil bit       NOT NULL,    pad    char(700) NOT NULL CONSTRAINT DF_N1_pad DEFAULT 'x');-- 20.000 stabile Zeilen mit id 10, 20, 30, …, danach REBUILD mit FILLFACTOR = 100 -- Schreiber, im Autocommit in einer Schleife: neue Zeile zwischen zwei stabilenINSERT dbo.N1 (id, stabil) VALUES (@id, 0); -- LeserSELECT id INTO #r FROM dbo.N1 WITH (NOLOCK) WHERE stabil = 1;SELECT COUNT(*) AS gelesen, COUNT(DISTINCT id) AS verschieden FROM #r;   -- Soll: 20.000 und 20.000
VersionLesungenmit fehlenden Zeilenmit doppelten Zeilenhöchstens fehlend / doppelt
201775387 %63 %417 / 38
20191.03677 %65 %298 / 42
20221.12578 %62 %313 / 33
202583170 %66 %420 / 48

Zwei Gegenproben blieben in allen rund 2.700 Lesungen fehlerfrei: derselbe NOLOCK-Scan mit einem Bereichsprädikat auf den Schlüssel (dann liest SQL Server in Schlüsselreihenfolge) und das Lesen unter READ COMMITTED mit Sperren. Neu gegenüber den älteren Quellen: Die Doppelungen entstanden schon durch reines Einfügen, ganz ohne UPDATE.

Wie oft trifft das Sie? Unsere Last war extrem: Zehntausende Einfügungen je Lauf, fast jede teilt eine Seite. In Produktion hängt die Häufigkeit davon ab, wie viele Seiten sich teilen, während ein Scan läuft. Auf Tabellen, die sich während des Lesens nicht ändern, passiert nichts. Für Berichte über laufende Buchungsdaten ist „selten“ aber kein gutes Argument, und der Fehler fällt niemandem auf.

Fehler 601 („Could not continue scan with NOLOCK due to data movement“) haben wir in rund 102.000 NOLOCK-Scans mit wachsenden und gelöschten Zeilen nicht ausgelöst. Die Zählungen waren trotzdem in 8 bis 19 Prozent der Lesungen falsch. Die Quellen beschreiben 601 als selten; ausgeschlossen ist er damit nicht.

Auch READ COMMITTED ist kein Zeitpunktbild

Unter dem klassischen READ COMMITTED nimmt ein Scan für jede Zeile kurz eine Lesesperre und gibt sie sofort wieder frei. Ändert eine andere Transaktion den Indexschlüssel einer Zeile, die der Scan schon gelesen hat, und schiebt sie dabei nach vorn in den noch ungelesenen Bereich, liest der Scan sie ein zweites Mal. Wandert eine noch ungelesene Zeile nach hinten, fehlt sie. Paul White hat das 2014 beschrieben, ein lauffähiges Skript fanden wir nirgends.

ScanIndex in SchlüsselreihenfolgeKonto CKonto DKonto EKonto FKonto GKonto HKonto AKonto AKonto AKonto BKonto BKonto B2×Konto A rückt nach vornschon gelesen, wird noch einmal gelesen: doppeltKonto B rückt zurücklandet hinter dem Scan, wird nie gelesen: fehltSchema, keine Messung. Der Scan nimmt je Zeile kurz eine Lesesperre und gibt sie gleich wieder frei; eineandere Transaktion ändert währenddessen die Schlüssel von A und B.
ScanIndex in SchlüsselreihenfolgeKonto CKonto DKonto EKonto FKonto GKonto HKonto AKonto AKonto AKonto BKonto BKonto B2×Konto B rückt zurücklandet hinter dem Scan, wird nie gelesen: fehltKonto A rückt nach vornschon gelesen, wird noch einmal gelesen: doppeltSchema, keine Messung. Der Scan nimmt je Zeilekurz eine Lesesperre und gibt sie gleich wiederfrei; eine andere Transaktion ändertwährenddessen die Schlüssel von A und B.
So entstehen fehlende und doppelte Zeilen unter READ COMMITTED mit Sperren. Im PSG-Lab auf SQL Server 2025 CU10 fehlte in 29 % der Lesungen mindestens ein Konto, in 95 % war eines doppelt (324 Lesungen, künstlich hohe Änderungsrate).

Also haben wir eines geschrieben:

-- Verkürzt aus dem Lab-CodeCREATE TABLE dbo.K3 (    schluessel int       NOT NULL CONSTRAINT PK_K3 PRIMARY KEY CLUSTERED,    kontonr    int       NOT NULL CONSTRAINT UQ_K3_kontonr UNIQUE NONCLUSTERED,    pad        char(400) NOT NULL CONSTRAINT DF_K3_pad DEFAULT 'k');-- 50.000 Konten, Schlüssel 2, 4, 6, … -- Schreiber, im Autocommit in einer Schleife: ein Konto bekommt einen neuen SchlüsselUPDATE dbo.K3 SET schluessel = @neu WHERE kontonr = @konto; -- Leser, READ COMMITTED mit Sperren, Scan in SchlüsselreihenfolgeINSERT #r (id) SELECT kontonr FROM dbo.K3 WITH (INDEX(PK_K3)) OPTION (MAXDOP 1);-- Soll: 50.000 verschiedene Konten, jedes genau einmal
VersionLesungenREAD COMMITTED: mit fehlenden / doppelten KontenREPEATABLE READ: fehlende / doppelteSERIALIZABLE, RCSI
2017181100 % / 100 %38 % / 0exakt
2019270100 % / 100 %40 % / 0exakt
202228628 % / 94 %17 % / 0exakt
202532429 % / 95 %18 % / 0exakt

Kein Konto kam hinzu, keines fiel weg. Trotzdem lieferte READ COMMITTED auf allen vier Versionen regelmäßig falsche Mengen. Auf 2022 und 2025 schaffte derselbe Schreiber nur rund ein Zwanzigstel der Verschiebungen; selbst dann fehlte in fast jeder dritten Lesung ein Konto. Die Ursache des langsameren Schreibers haben wir nicht geklärt.

  • REPEATABLE READ hält gelesene Zeilen bis zum Ende gesperrt. Doppelungen verschwinden, Zeilen, die hinter die Leseposition wandern, fehlen weiterhin.
  • SERIALIZABLE lieferte immer exakt 50.000, der Schreiber wurde dafür je Version 15- bis 49-mal Deadlock-Opfer.
  • RCSI lieferte immer exakt 50.000, ohne jemanden warten zu lassen.

Die Änderungsrate war künstlich hoch; in Anwendungen ändern sich Schlüssel selten. Die Aussage bleibt: Wer READ COMMITTED für ein Zeitpunktbild hält, irrt, mit und ohne NOLOCK.

RCSI: Leser warten nicht mehr

Mit READ_COMMITTED_SNAPSHOT ON liest jede Anweisung unter READ COMMITTED den Stand, der bei ihrem Beginn bestätigt war. Geänderte Zeilen hinterlassen dafür eine Version, je nach Konfiguration in der TempDB oder mit ADR im Persistent Version Store der Datenbank. Leser nehmen keine Lesesperren mehr und warten nicht auf Schreiber. Auf der Übersicht sehen Sie den Effekt: ein Leser mit Lock-Timeout nach 503,53 ms gegenüber 1,64 ms mit RCSI.

Schreiber bleiben Schreiber: UPDATE und DELETE suchen ihre Zeilen weiterhin mit Update-Sperren auf dem aktuellen Stand und warten aufeinander. Das ändert erst Optimized Locking in SQL Server 2025, siehe unten.

Wo RCSI anders rechnet

Unter klassischem READ COMMITTED wartet ein Leser, bis eine fremde Änderung bestätigt oder zurückgerollt ist, und sieht dann das Ergebnis. Unter RCSI wartet er nicht, sondern sieht den letzten bestätigten Stand. Für Code, der liest, eine Bedingung prüft und dann schreibt, ist das ein Unterschied. Wir haben drei Muster mit je zwei Sitzungen nachgestellt: Sitzung A ändert und hält ihre Transaktion zwei Sekunden offen, Sitzung B startet 0,7 Sekunden später. Alle Ergebnisse waren auf 2017, 2019, 2022 und 2025 identisch und in jeder Wiederholung gleich.

Muster 1: Prüflogik im Trigger

CREATE TRIGGER dbo.trg_Kind_Pruefung ON dbo.Kind AFTER INSERT ASBEGIN    SET NOCOUNT ON;    IF EXISTS (SELECT 1 FROM inserted AS i               WHERE NOT EXISTS (SELECT 1 FROM dbo.Eltern AS e WHERE e.id = i.eltern_id))    BEGIN        ROLLBACK TRANSACTION;        THROW 50001, 'Kind ohne Eltern abgewiesen', 1;    END;END;

Sitzung A löscht Eltern 1 (ihr eigener Trigger findet noch keine Kinder), Sitzung B fügt ein Kind zu Eltern 1 ein.

  • READ COMMITTED mit Sperren: B wartet auf A (LCK_M_S), sieht nach dem Commit, dass die Eltern fehlen, und weist das Kind ab.
  • RCSI: B wartet nicht, sieht die Eltern noch als bestätigten Stand und fügt das Kind ein. Nach beiden Commits gibt es ein Kind ohne Eltern.
  • Abhilfe: FROM dbo.Eltern AS e WITH (READCOMMITTEDLOCK) im Trigger. Dann wartet B wieder und weist ab. Wo es fachlich passt, ist ein Fremdschlüssel besser: Laut Microsoft und Paul White prüft er auch unter RCSI mit Sperren (nicht nachgemessen).

Muster 2: UPDATE mit Unterabfrage

UPDATE dbo.PlatzSET gast = 'B', nr = (SELECT COALESCE(MAX(p.nr), 0) FROM dbo.Platz AS p) + 1WHERE platz = 2;

A vergibt die nächste Laufnummer für Platz 1 und hält, B für Platz 2. Unter Sperren wartet B und bekommt Nummer 2. Unter RCSI liest die Unterabfrage den bestätigten Stand, in dem die Nummer von A noch fehlt: Beide erhalten Nummer 1. Mit WITH (UPDLOCK) oder WITH (READCOMMITTEDLOCK) an der Unterabfrage wartet B und rechnet richtig.

Ob ein UPDATE mit Unterabfrage betroffen ist, entscheidet der Plan. Die bekannte Sitzplatz-Variante

UPDATE dbo.Platz SET gast = 'B'WHERE platz = (SELECT MIN(p.platz) FROM dbo.Platz AS p WHERE p.gast IS NULL);

blieb im Lab auf allen Versionen korrekt. Der Optimierer legt hier Unterabfrage und Ziel zu einem einzigen Scan mit Update-Sperren zusammen; B wartet und bekommt den nächsten freien Platz. Bei der Laufnummer bleiben es zwei getrennte Zugriffe, und die Unterabfrage liest eine Version. Verlassen Sie sich nicht auf den Plan, er kann sich ändern.

Muster 3: Erst lesen, dann schreiben

SELECT @saldo = saldo FROM dbo.Konto WHERE id = 1;-- … Prüfung in der Anwendung oder Prozedur …UPDATE dbo.Konto SET saldo = @saldo - @betrag WHERE id = 1;

Saldo 100. A bucht 30 ab und hält ihre Transaktion offen, B will 50 abbuchen.

  • READ COMMITTED mit Sperren: B wartet schon beim Lesen, liest 70 und schreibt 20. Richtig.
  • RCSI: B liest 100, wartet erst beim UPDATE und schreibt danach 50. Die Buchung von A ist verloren.
READ COMMITTED mit SperrenB wartet schon beim LesenAliest 100, schreibt 70Transaktion offenCOMMITBwill lesenwartet · LCK_M_Sliest 70, schreibt 70 − 50Saldo 20richtig: 100 − 30 − 50RCSIB liest ohne Warten die letzte bestätigteVersionAliest 100, schreibt 70Transaktion offenCOMMITBliest 100 (Version)wartet · LCK_M_Xschreibt 100 − 50Saldo 50die Buchung von A ist verlorenSaldo 100. A liest, bucht 30 ab und hält ihre Transaktion 2 s offen; B startet 0,7 s später und will 50abbuchen. Zeitachse maßstäblich, Werte aus dem Lab.
READ COMMITTED mit SperrenB wartet schon beim LesenAliest 100, schreibt 70Transaktion offenCOMMITBwill lesenwartet · LCK_M_Sliest 70, schreibt 70 − 50Saldo 20richtig: 100 − 30 − 50RCSIB liest ohne Warten die letzte bestätigteVersionAliest 100, schreibt 70Transaktion offenCOMMITBliest 100 (Version)wartet · LCK_M_Xschreibt 100 − 50Saldo 50die Buchung von A ist verlorenSaldo 100. A liest, bucht 30 ab und hält ihreTransaktion 2 s offen; B startet 0,7 s späterund will 50 abbuchen. Zeitachse maßstäblich,Werte aus dem Lab.
Erst lesen, dann schreiben, mit zwei Sitzungen nachgestellt: Unter RCSI geht die Buchung von A verloren, mit Sperren nicht. Identisch auf SQL Server 2017, 2019, 2022 und 2025 und in jeder Wiederholung, mit Optimized Locking ebenso. PSG-Lab.

Die Sperren schützen dabei nur zufällig: In derselben Konstellation, in der beide zuerst lesen und dann schreiben, endete es auch unter READ COMMITTED mit Sperren bei 50. Das Muster war also schon vorher fehlerhaft, RCSI macht den Fehler nur häufiger. Robust sind zwei Wege:

-- Eine Anweisung: Lesen und Schreiben fallen zusammenUPDATE dbo.Konto SET saldo = saldo - @betrag WHERE id = 1; -- Oder beim Lesen schon die Änderungsabsicht anmeldenSELECT @saldo = saldo FROM dbo.Konto WITH (UPDLOCK) WHERE id = 1;

Mit UPDLOCK wartete B in allen Konfigurationen beim Lesen und schrieb 20.

Kurz: Das Gegenmittel ist nicht der Verzicht auf RCSI, sondern Code, der seine Absicht ausdrückt: eine Anweisung statt zwei, UPDLOCK dort, wo gelesen wird, um zu schreiben, READCOMMITTEDLOCK oder ein Constraint dort, wo geprüft wird.

SNAPSHOT ist eine andere Entscheidung

Unter SNAPSHOT sieht eine Transaktion von ihrer ersten Leseanweisung an durchgehend denselben bestätigten Stand. Das ist gut für Berichte aus mehreren Abfragen, die zusammenpassen müssen. Dafür bricht SNAPSHOT ab, wenn eine Transaktion eine Zeile ändern will, die seit ihrem Beginn eine andere geändert hat. Fehler 3960, Meldungstext im Original aus dem Lab:

Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table 'dbo.Konto' directly or indirectly in database 'a5_si' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement.

Im Lab gab es 3960 auch dann, wenn die andere Transaktion nur eine andere Spalte derselben Zeile geändert hatte, und sogar bei SET saldo = saldo ohne neuen Wert. Hält die andere Transaktion noch offen, wartet SNAPSHOT erst und bricht nach deren Commit ab; rollt sie zurück, läuft SNAPSHOT durch. Anwendungen unter SNAPSHOT brauchen also eine Wiederholungslogik.

Write Skew: Zwei Ärzte haben Dienst, die Regel lautet „mindestens einer im Dienst“. Beide prüfen gleichzeitig COUNT(*) >= 2 und tragen sich aus. Unter SNAPSHOT waren danach null im Dienst. Das ist das Lehrbuchbeispiel, aber im Lab kein SNAPSHOT-Sonderrisiko: Unter READ COMMITTED mit Sperren und unter RCSI endete es genauso. Verhindert haben es nur SERIALIZABLE (eine Sitzung wird mit Fehler 1205 Deadlock-Opfer und muss wiederholen) und UPDLOCK beim Zählen (unter SNAPSHOT wartet die zweite Sitzung dann und bricht mit 3960 ab). Regeln über mehrere Zeilen brauchen eine Sperre oder ein Constraint, keine Isolationsstufe allein.

Was RCSI kostet

14 Byte je Zeile, aber erst bei der ersten Änderung. Das Einschalten selbst ändert keine Seite. Erst wenn eine Zeile zum ersten Mal geändert wird, bekommt sie die Versionsinformation. Auf vollen Seiten passt das nicht mehr:

Seiten nach der ersten Änderung400.000 Zeilen, vorher 5.000 volle Seitenohne RCSI5.000RCSI10.000ADR (ohne RCSI)10.00005.00010.000Log dieser ersten ÄnderungEin UPDATE über alle Zeilenohne RCSI41,2 MBRCSI85,4 MBADR (ohne RCSI)93,3 MB050100 MBFüllfaktor 100, Satz 99 Byte. Das Einschalten allein änderte nichts; erst die erste Änderung jeder Zeileschrieb den Zusatz (RCSI 113, ADR 134 Byte), und jede Seite teilte sich einmal.
Seiten nach der ersten Änderung400.000 Zeilen, vorher 5.000 volle Seitenohne RCSI5.000RCSI10.000ADR (ohne RCSI)10.00005.00010.000Log dieser ersten ÄnderungEin UPDATE über alle Zeilenohne RCSI41,2 MBRCSI85,4 MBADR (ohne RCSI)93,3 MB050100 MBFüllfaktor 100, Satz 99 Byte. Das Einschaltenallein änderte nichts; erst die erste Änderungjeder Zeile schrieb den Zusatz (RCSI 113, ADR134 Byte), und jede Seite teilte sich einmal.
Bei vollen Seiten verdoppelt die erste Änderung nach dem Einschalten Tabelle und Log. Gemessen im PSG-Lab auf SQL Server 2025 CU10, gleiche Werte auf 2017 bis 2022 (RCSI) bzw. 2019 und 2022 (ADR). Mit freiem Platz auf den Seiten nicht gemessen.

400.000 Zeilen, Füllfaktor 100, ein UPDATE über alle Zeilen, byte-gleich auf allen Versionen. Jede Seite teilte sich genau einmal, die Tabelle verdoppelte sich. Auch jede weitere Änderungswelle schrieb mit RCSI mehr Log als ohne (55,9 statt 41,2 MB). Aus „bis zu 14 Prozent mehr“ der Dokumentation wurden hier 100 Prozent Platz für halb volle Seiten. Mit freiem Platz auf den Seiten fällt das deutlich kleiner aus; das haben wir nicht gemessen. Wer nach dem Umstellen große Tabellen auf einmal ändert, sollte Platz und Log dafür einplanen.

Wer Versionen festhält. Eine Version darf erst weg, wenn keine Anweisung sie mehr brauchen kann. Nach drei Änderungswellen über 400.000 Zeilen hielt eine einzige lange Leseanweisung rund 200 MB im Version Store der TempDB fest (mit ADR rund 153 MB im Persistent Version Store), 65 Sekunden lang, bis sie endete. Auf 2022 und 2025 tat das auch eine untätige offene Transaktion: ein BEGIN TRAN, ein SELECT, dann nichts mehr. Unter RCSI braucht keine ihrer späteren Anweisungen diese Versionen, festgehalten wurden sie trotzdem. Eine vergessene Transaktion im Verbindungspool wirkt damit wie ein Langläufer. Wie Sie solche Halter finden, steht auf der Seite Version Store und Transaktionslog.

RCSI sicher einschalten

1. Code lesen, bevor Sie schalten

Welche Prozeduren, Trigger und Funktionen nach den Mustern oben aussehen, findet eine Textsuche über die Module der Datenbank. Sie ersetzt kein Lesen, sagt Ihnen aber, wo Sie anfangen.

Abfrage: RCSI-Verdachtsliste

Abfrage 3: RCSI-Verdachtsliste · nur lesend · im PSG-Lab geprüft, Versionen im Kopf
/* PSG Stack · SQL Server Locking · Abfrage 3   Verdachtsliste vor dem Einschalten von READ_COMMITTED_SNAPSHOT:   Welche Module (Trigger, Prozeduren, Funktionen, Sichten) sollte man vorher lesen?    !!! WARNUNG — bitte vor dem Ausführen lesen !!!   - Nur lesend. Die Abfrage ändert keine Daten, keine Einstellung und keinen Code.   - Verdachtsliste, kein Urteil. Sie sucht nur nach Textmustern im Modultext (sys.sql_modules).     Treffer in Kommentaren oder Zeichenketten erscheinen ebenfalls (Fehlalarm). Code außerhalb der     Datenbank (Anwendung, ORM, Ad-hoc-SQL, Agent-Jobs, andere Datenbanken) sieht sie nicht.     Ein leeres Ergebnis heißt nicht „RCSI ist sicher“.   - Gilt nur für die aktuelle Datenbank. In jeder Anwendungsdatenbank einzeln ausführen.   - Große Codebasen: Der Text aller Module wird einmal gelesen und durchsucht. Das kostet etwas CPU,     aber keine Sperren auf Anwendungstabellen. Nicht in einer Schleife ausführen.   - Rechte: Benutzer in der Datenbank plus VIEW DEFINITION (Datenbank) bzw. VIEW ANY DEFINITION.     Ohne diese Rechte fehlen Module ganz oder ihr Text ist leer; die Übersicht am Ende zeigt das an.     Verschlüsselte Module (WITH ENCRYPTION) erscheinen als „Text nicht lesbar“.   - MAXDOP 1: läuft seriell und belegt keine weiteren Kerne.   - Ab SQL Server 2017. Geprüft im PSG-Lab auf 2017, 2019, 2022 und 2025 (10.11.2026).    Spalten mit 1 = Muster gefunden:     trigger_mit_pruefung   Trigger mit EXISTS oder COUNT: prüft er Zeilen anderer Transaktionen, sieht                            er unter RCSI den letzten bestätigten Stand statt zu warten.     lesen_dann_schreiben   Variable aus SELECT und danach INSERT/UPDATE/DELETE im selben Modul: unter                            RCSI kann der gelesene Wert beim Schreiben schon überholt sein.     update_mit_unterabfrage UPDATE/DELETE mit (SELECT …) (grobe Suche): die Unterabfrage liest unter                            RCSI Versionen.     funktion_liest_tabellen Skalar-/Tabellenwertfunktion mit FROM: kann einen späteren Stand sehen als                            die aufrufende Anweisung.     nolock                 NOLOCK, READUNCOMMITTED, READ UNCOMMITTED: unabhängig von RCSI prüfen.     readcommittedlock      erzwingt Sperren trotz RCSI (oft gewollt, Beleg für bewusste Prüflogik).     sperrhinweis           UPDLOCK, XLOCK, HOLDLOCK, TABLOCK, SERIALIZABLE, REPEATABLEREAD.     isolationsstufe        SET TRANSACTION ISOLATION LEVEL im Modul.*/SET NOCOUNT ON; WITH m AS (    SELECT o.object_id,           s.name                                    AS schema_name,           o.name                                    AS objekt,           o.type                                    AS typ,           o.type_desc                               AS typ_text,           OBJECT_SCHEMA_NAME(tr.parent_id) + N'.' + OBJECT_NAME(tr.parent_id) AS trigger_tabelle,           UPPER(REPLACE(REPLACE(REPLACE(sm.definition, NCHAR(13), N' '), NCHAR(10), N' '), NCHAR(9), N' ')) AS d    FROM sys.sql_modules AS sm    JOIN sys.objects AS o ON o.object_id = sm.object_id    JOIN sys.schemas AS s ON s.schema_id = o.schema_id    LEFT JOIN sys.triggers AS tr ON tr.object_id = sm.object_id    WHERE o.is_ms_shipped = 0),f AS (    SELECT m.*,           CASE WHEN typ = 'TR' AND (d LIKE N'%EXISTS%' OR d LIKE N'%COUNT(%' OR d LIKE N'%COUNT (%') THEN 1 ELSE 0 END AS trigger_mit_pruefung,           CASE WHEN (d LIKE N'%SELECT @%' OR d LIKE N'%= (SELECT%' OR d LIKE N'%=(SELECT%')                 AND (d LIKE N'%UPDATE %' OR d LIKE N'%DELETE %' OR d LIKE N'%INSERT %') THEN 1 ELSE 0 END AS lesen_dann_schreiben,           CASE WHEN d LIKE N'%UPDATE %(SELECT%' OR d LIKE N'%UPDATE %( SELECT%'                  OR d LIKE N'%DELETE %(SELECT%' OR d LIKE N'%DELETE %( SELECT%' THEN 1 ELSE 0 END AS update_mit_unterabfrage,           CASE WHEN typ IN ('FN', 'IF', 'TF') AND d LIKE N'%FROM %' THEN 1 ELSE 0 END AS funktion_liest_tabellen,           CASE WHEN d LIKE N'%NOLOCK%' OR d LIKE N'%READUNCOMMITTED%' OR d LIKE N'%READ UNCOMMITTED%' THEN 1 ELSE 0 END AS nolock,           CASE WHEN d LIKE N'%READCOMMITTEDLOCK%' THEN 1 ELSE 0 END AS readcommittedlock,           CASE WHEN d LIKE N'%UPDLOCK%' OR d LIKE N'%XLOCK%' OR d LIKE N'%HOLDLOCK%' OR d LIKE N'%TABLOCK%'                  OR d LIKE N'%(SERIALIZABLE%' OR d LIKE N'%, SERIALIZABLE%' OR d LIKE N'%REPEATABLEREAD%' THEN 1 ELSE 0 END AS sperrhinweis,           CASE WHEN d LIKE N'%ISOLATION LEVEL%' THEN 1 ELSE 0 END AS isolationsstufe    FROM m)SELECT DB_NAME()               AS datenbank,       schema_name             AS [schema],       objekt,       typ_text                AS typ,       trigger_tabelle,       CASE WHEN d IS NULL THEN N'Text nicht lesbar (verschlüsselt oder fehlendes Recht)'            WHEN trigger_mit_pruefung = 1 OR lesen_dann_schreiben = 1 THEN N'1 zuerst lesen'            WHEN update_mit_unterabfrage = 1 OR funktion_liest_tabellen = 1 THEN N'2 lesen'            ELSE N'3 Hinweis' END AS prioritaet,       trigger_mit_pruefung, lesen_dann_schreiben, update_mit_unterabfrage, funktion_liest_tabellen,       nolock, readcommittedlock, sperrhinweis, isolationsstufeFROM fWHERE d IS NULL   OR trigger_mit_pruefung + lesen_dann_schreiben + update_mit_unterabfrage + funktion_liest_tabellen      + nolock + readcommittedlock + sperrhinweis + isolationsstufe > 0ORDER BY prioritaet, [schema], objektOPTION (MAXDOP 1); /* Übersicht: Wie viel Code war sichtbar? Steht hier „nein“ beim Recht oder sind viele Texte leer,   ist die Liste oben unvollständig. */SELECT DB_NAME()                                                  AS datenbank,       CASE d.is_read_committed_snapshot_on WHEN 1 THEN N'an' ELSE N'aus' END AS rcsi_jetzt,       (SELECT COUNT(*) FROM sys.sql_modules AS sm JOIN sys.objects AS o ON o.object_id = sm.object_id         WHERE o.is_ms_shipped = 0)                               AS module_sichtbar,       (SELECT COUNT(*) FROM sys.sql_modules AS sm JOIN sys.objects AS o ON o.object_id = sm.object_id         WHERE o.is_ms_shipped = 0 AND sm.definition IS NULL)     AS davon_text_nicht_lesbar,       CASE HAS_PERMS_BY_NAME(DB_NAME(), N'DATABASE', N'VIEW DEFINITION') WHEN 1 THEN N'ja' ELSE N'nein' END AS recht_view_definitionFROM sys.databases AS dWHERE d.database_id = DB_ID()OPTION (MAXDOP 1);

Im Lab fand die Abfrage alle 13 absichtlich angelegten Verdachtsfälle auf allen vier Versionen und keinen der harmlosen Prüffälle. Sie meldet aber auch Fehlalarme, etwa die sichere Sitzplatz-Prozedur aus Muster 2 als „Lesen dann Schreiben“; den Unterschied im Plan kann eine Textsuche nicht sehen. Mit nur db_datareader erscheinen die Module ohne Text, und die Übersicht zeigt beim Recht nein. VIEW DEFINITION auf die Datenbank genügt.

2. Testen

Die Muster oben lassen sich in einem Test mit zwei Sitzungen nachstellen: eine hält ihre Transaktion offen, die andere läuft dagegen. Wer Code nur mit einer Sitzung testet, sieht keinen Unterschied zwischen den Stufen.

3. Umschalten im Wartungsfenster

ALTER DATABASE [MeineDB] SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;

Ohne Zusatz wartet ALTER DATABASE auf jede andere Verbindung in der Datenbank, auch auf eine untätige ohne Transaktion, und auch über den Commit einer offenen Transaktion hinaus, bis die Verbindung getrennt wird. Laut Dokumentation scheitert die Anweisung mit WITH NO_WAIT sofort, wenn sie nicht gleich fertig werden kann. Im Lab tat sie das auf keiner Version: Sie wartete ohne Meldung genauso lange wie ohne Zusatz. Mit WITH ROLLBACK IMMEDIATE war der Schalter nach rund drei Sekunden umgelegt; offene Transaktionen wurden zurückgerollt, die Verbindungen getrennt, und die ändernde Sitzung meldete:

Nonqualified transactions are being rolled back. Estimated rollback completion: 0%.
Nonqualified transactions are being rolled back. Estimated rollback completion: 100%.

Für ADR und Optimized Locking verhielt sich ALTER DATABASE im Lab genauso. ALLOW_SNAPSHOT_ISOLATION haben wir nicht geprüft; laut Dokumentation wartet diese Option nur auf laufende Transaktionen. Der Rückweg ist derselbe Befehl mit OFF.

Neu seit SQL Server 2025

Mit Optimized Locking und RCSI prüfen UPDATE und DELETE ihre Bedingung gegen den zuletzt bestätigten Stand, ohne zu sperren (Lock After Qualification). Ob sich der Schalter für Ihre Last lohnt, steht in Optimized Locking aktivieren. Für diese Seite heißt das:

  • Die drei Muster bleiben, wie sie unter RCSI sind. Verwaistes Kind, doppelte Laufnummer und verlorene Buchung traten mit Optimized Locking genauso auf. Die Gegenmittel wirken weiter, nur warten die Sitzungen jetzt auf LCK_M_S_XACT_READ oder LCK_M_S_XACT_MODIFY.
  • Kein neuer Fehler unter RCSI. Microsoft schreibt, Update-Konflikte unter RCSI würden mit Optimized Locking automatisch wiederholt. Im Lab heißt das: B wartet auf die Transaktion von A, prüft danach erneut und liefert dasselbe Ergebnis wie ohne Optimized Locking. Fehler 3960 gab es unter RCSI weder mit noch ohne Optimized Locking.
  • SNAPSHOT bleibt SNAPSHOT. Fehler 3960 trat mit Optimized Locking unverändert auf.
  • Eine Reihenfolge ändert sich. Das Beispiel der Dokumentation (UPDATE t4 SET b = 3 WHERE b = 2, während eine andere Transaktion b von 1 auf 2 setzt) endete mit Optimized Locking und RCSI bei b = 2, ohne Optimized Locking bei b = 3. Erklärung in der Vertiefung Optimized Locking.

Methode und Grenzen

Alle Messwerte stammen aus unserem Lab vom 10.11.2026: SQL Server 2017 CU31, 2019 CU32, 2022 CU27 und 2025 CU10 (17.0.5035.5), Developer Edition, Linux-Container mit vier CPUs auf einer Test-VM mit SATA-SSD, synthetische Tabellen, eigener Code. Die Zwei-Sitzungs-Muster liefen je zweimal mit festem Zeitversatz, die Lastversuche über mehrere Runden zu acht bis zwölf Sekunden. Was wir nicht selbst gemessen haben, stammt aus Microsoft Learn und den unten genannten Fachbeiträgen. Grenzen:

  • Die Häufigkeiten in den NOLOCK- und READ-COMMITTED-Versuchen gelten für eine absichtlich extreme Änderungsrate. Sie zeigen, dass es passiert, nicht wie oft bei Ihnen.
  • Fehler 601 haben wir nicht ausgelöst; andere Auslöser (Heaps mit Forwarded Records, Ghost Cleanup, TRUNCATE) nicht geprüft.
  • Muster 2 hängt am Plan. Wir haben zwei Formen gezeigt; andere Pläne können anders ausgehen.
  • Skalar- und Multi-Statement-Funktionen unter RCSI, Fremdschlüssel und indizierte Sichten haben wir nicht nachgestellt.
  • Warum NO_WAIT nicht scheiterte und warum ROLLBACK IMMEDIATE gleichbleibend rund drei Sekunden brauchte, haben wir nicht untersucht. Geprüft ist nur der Fall einer untätigen oder ruhenden Verbindung, nicht der einer laufenden Abfrage.
  • Warum der Schreiber im READ-COMMITTED-Versuch auf 2022 und 2025 so viel langsamer war, ist offen.
  • Dass eine untätige offene Transaktion Versionen festhält, haben wir auf 2022 und 2025 gemessen, auf 2019 nicht geprüft. Ein früher Vorversuch mit kleinerer Tabelle zeigte das nicht; den Widerspruch haben wir nicht aufgeklärt.
  • Azure SQL Database, Verfügbarkeitsgruppen und Windows haben wir nicht gemessen.

Weiterlesen

  • Microsoft Learn: SET TRANSACTION ISOLATION LEVEL; Transaction Locking and Row Versioning Guide; ALTER DATABASE SET options; Optimized locking.
  • Paul White: Serie „SQL Server Isolation Levels“ auf SQLPerformance.com (2014/2015), besonders „The Read Committed Isolation Level“, „Read Committed Snapshot Isolation“ und „The SNAPSHOT Isolation Level“.
  • Lubor Kollar (SQLCAT): Previously committed rows might be missed if NOLOCK hint is used (2007).
  • Brent Ozar: Using NOLOCK? Here’s how you’ll get the wrong query results (2018).
  • Erik Darling: A Little More About Isolation Levels In SQL Server (2024).
  • Bei PSG: Optimized Locking verständlich, Optimized Locking aktivieren, Version Store und Transaktionslog, Deadlockgraph lesen.

Sie überlegen, RCSI einzuschalten?

Oder Ihre Berichte zählen mal so, mal so? In einer Performance-Analyse lesen wir mit Ihnen die Kandidaten aus Abfrage 3, stellen die kritischen Muster an Ihrem Code nach und planen das Umschalten. Den Gesamtblick auf die Instanz gibt der SQL Server Health Check, die Grundlagen vermittelt die PTS-Schulung.

Weitere Locking-Themen