Transaktionsisolation:
Wie viel Konsistenz brauchst du — und was kostet sie?
Zwei Kassierer, eine Kassenschublade. Kassierer A zählt gerade nach — soll Kassierer B warten, oder darf er einfach anfangen, Geld rauszugeben? Genau das ist das Problem der Transaktionsisolation: Wie stark dürfen gleichzeitige Transaktionen sich gegenseitig beeinflussen?
SQL Server bearbeitet im Produktionsbetrieb hunderte, manchmal tausende gleichzeitiger Transaktionen. Das Isolation Level entscheidet, wie viel Konsistenz du bekommst — und was du dafür bezahlst. Die Währung ist Blocking, und Blocking kostet Wartezeit. ACID-Prinzip, das I steht für Isolation, und genau wie beim ersten Buchstaben A (Atomicity) gibt es hier kein kostenfreies Maximum.
In Kapitel 14 haben wir Blocking und Deadlocks aus der Administratoren-Perspektive analysiert. Hier schauen wir aus der Entwickler-Perspektive: Was wähle ich für welchen Anwendungsfall — und warum landet fast jedes Team beim falschen Level? Kapitel 28 vertieft Locking aus Entwicklersicht, Kapitel 29 erklärt Row Versioning im Detail.
Warum Isolation Levels existieren: Anomalien und ihre Kosten
Ohne jede Isolation passieren vier klassische Anomalien — und jede davon kann deine Applikation in unerwartete Zustände bringen:
Mit voller Isolation — SERIALIZABLE — sind alle vier Anomalien ausgeschlossen. Der Preis: maximales Blocking. Jeder hält Locks länger, Transaktionen warten aufeinander, und Deadlocks sind deutlich wahrscheinlicher. Der Kompromiss ist die Kernentscheidung: weniger Isolation ermöglicht mehr Parallelität, erhöht aber das Anomalie-Risiko.
SQL Server-Standard ist READ COMMITTED — verhindert Dirty Reads, erlaubt alles andere. Das ist ein vernünftiger Default für OLTP, aber kein Freifahrtschein. Und es erklärt, warum Read/Write-Blocking in vielen Applikationen das häufigste Latenz-Problem ist.
Die fünf Isolation Levels im Detail
READ UNCOMMITTED: Der Schnellste und der Riskanteste
READ UNCOMMITTED liest Daten ohne Shared Lock — also ohne zu warten, wenn eine andere Transaktion einen Exclusive Lock hält. Kein Read/Write-Blocking. Das klingt verlockend, ist aber in fast allen Fällen die falsche Wahl.
WITH (NOLOCK) ist syntaktischer Zucker für READ UNCOMMITTED auf Tabellenebene. Du wirst NOLOCK in sehr vielen Produktionsdatenbanken finden — meistens eingefügt von jemandem, der ein Blocking-Problem hatte und es mit dem Holzhammer gelöst hat. Das Blocking ist weg, aber die Korrektheit der Daten ist nicht mehr garantiert.
-- NOLOCK: Schnell, aber gefährlich
SELECT SUM(r.Betrag) AS GesamtUmsatz
FROM dbo.Rechnung AS r WITH (NOLOCK);
-- Dirty Reads: Rechnung wurde begonnen aber noch nicht committed.
-- SQL Server liest den uncommitteten Wert.
-- Transaktion rollt zurück — du hast Phantom-Umsatz summiert.
-- Zusätzliches Risiko: 605-Fehler
-- Bei NOLOCK kann SQL Server eine Seite lesen, die gerade verschoben wird
-- (z.B. durch Page Split). Ergebnis: Fehler 605 "Could not find row".
-- Das passiert selten, aber in Produktionssystemen mit hohem Schreibaufkommen
-- oft genug um schmerzhaft zu sein.
|
Warnung: NOLOCK ist keine Performance-Lösung |
|---|
|
NOLOCK verhindert kein Locking auf der Lese-Seite — es verhindert, dass Reads von Writes GEBLOCKT werden. Der Preis ist Korrektheitsverlust: Dirty Reads, Non-Repeatable Reads, Phantom Reads, und im Extremfall 605-Fehler. Wenn du NOLOCK siehst, ist das ein Zeichen, dass jemand ein Blocking-Problem hatte und es mit einem Korrektheitsproblem gelöst hat. Die eigentliche Lösung heißt RCSI — dazu gleich mehr. |
Wann ist READ UNCOMMITTED akzeptabel? Für Approximate Analytics: Dashboards die ungefähre Zählwerte anzeigen, Monitoring-Queries die Trends sehen wollen, nicht exakte Werte. Wenn eine Abweichung von ±1% tolerierbar ist und die Alternative 500 ms Blocking bedeutet: NOLOCK kann sinnvoll sein. Aber nur bewusst, nicht als Default.
READ COMMITTED: Der Standard — mit einer wichtigen Tücke
READ COMMITTED (der SQL Server-Default) verwendet Shared Locks für das Lesen. Shared Locks werden gehalten, bis das Lesen der Zeile abgeschlossen ist — dann freigegeben. Das verhindert Dirty Reads: Du liest keine uncommitteten Daten.
Die Tücke: Wenn eine andere Transaktion gerade einen Exclusive Lock auf eine Zeile hält (weil sie schreibt), muss der Leser warten. Das ist Read/Write-Blocking. Ein INSERT/UPDATE/DELETE hält einen X-Lock bis zum Commit — und jedes READ auf dieselben Zeilen wartet in dieser Zeit. Bei langen Schreib-Transaktionen blockiert das die gesamte Leserlast.
Genau das ist das Blocking-Muster, das bei Musterwerk GmbH (MWSQL01) aufgetaucht ist: Eine Batch-Verarbeitung die Bestellungen updated, hielt X-Locks auf der gesamten Bestelltabelle — und jede Reporting-Query wartete. LCK_M_S dominierte die Wait Statistics. Die Lösung war nicht NOLOCK, sondern RCSI.
REPEATABLE READ: Mehr Konsistenz, mehr Blocking
REPEATABLE READ hält Shared Locks bis zum Ende der Transaktion (statt bis zum Ende des einzelnen Lesevorgangs). Damit sind Non-Repeatable Reads ausgeschlossen: Du kannst dieselbe Zeile zweimal lesen und bekommst denselben Wert. Phantom Reads sind aber noch möglich — neue Zeilen können zwischen zwei Lesevorgängen auftauchen.
In der Praxis selten direkt genutzt. Wenn du REPEATABLE READ brauchst (weil du innerhalb einer Transaktion zweimal dieselbe Zeile liest und Konsistenz erwartest), gibt es meistens eine bessere Applikationsarchitektur die das Problem löst ohne aggressiveres Locking.
SERIALIZABLE: Die stärkste Garantie, das häufigste Deadlock-Risiko
SERIALIZABLE ist das stärkste Isolation Level. Es hält nicht nur Shared Locks bis Transaktionsende, sondern verwendet auch Range Locks: Sperren auf Bereiche von Werten, nicht nur auf einzelne Zeilen. Damit sind Phantom Reads ausgeschlossen.
Das Deadlock-Risiko ist bei SERIALIZABLE erheblich höher als bei anderen Levels. Zwei Transaktionen die denselben Datenbereich lesen und beide dann schreiben wollen, geraten in klassisches Deadlock-Muster. In Kapitel 14 haben wir Deadlock-Graphen analysiert — bei SERIALIZABLE sind sie häufiger und komplexer.
Wann ist SERIALIZABLE nötig? Bei kritischen Finanzoperationen die Phantom Reads absolut ausschließen müssen, und bei verteilten Transaktionen (Distributed Transactions zwischen mehreren SQL Server-Instanzen verwenden SERIALIZABLE als Standard). Im normalen OLTP-Betrieb ist SERIALIZABLE fast immer overkill.
SNAPSHOT: Optimistisches Locking mit Version Store
SNAPSHOT Isolation ist fundamental anders als die bisherigen vier Levels: Es verwendet kein pessimistisches Locking, sondern liest die letzte committete Version einer Zeile aus dem Version Store in TempDB. Kein Read/Write-Blocking — ein lesender Query wartet nie auf einen schreibenden.
Der Haken: Update Conflict Detection. Wenn zwei Transaktionen dieselbe Zeile basierend auf demselben Snapshot ändern wollen, erkennt SQL Server den Konflikt beim Commit — und bricht eine der Transaktionen mit Fehler 3960 ab. Die Applikation muss diesen Fehler behandeln und die Transaktion wiederholen. Das ist optimistisches Concurrency Control: Wir gehen davon aus, dass Konflikte selten sind, und behandeln sie, wenn sie auftreten.
SNAPSHOT ist per Transaktion aktiviert (SET TRANSACTION ISOLATION LEVEL SNAPSHOT). Ausführliches Row Versioning und den TempDB-Overhead haben wir in Kapitel 29 — dort auch der Vergleich mit RCSI.
READ COMMITTED SNAPSHOT ISOLATION: Die praktische Lösung
READ COMMITTED SNAPSHOT ISOLATION — kurz RCSI — ist keine neue Isolation Level, sondern eine datenbankweite Einstellung, die das Verhalten von READ COMMITTED ändert: Statt Shared Locks zu verwenden, liest READ COMMITTED unter RCSI aus dem Version Store. Die letzte committete Version — ohne zu warten.
Der entscheidende Vorteil: Kein Applikationscode muss geändert werden. Die Applikation verwendet weiterhin READ COMMITTED (den Standard), bekommt aber automatisch das Versioning-Verhalten. Read/Write-Blocking ist eliminiert. Reads blocken nie auf Writes, Writes blocken nie auf Reads.
-- RCSI aktivieren: Zwei Schritte, beide nötig
-- Schritt 1: ALLOW_SNAPSHOT_ISOLATION aktivieren
-- (Voraussetzung für den Version Store)
ALTER DATABASE MusterwerkDB
SET ALLOW_SNAPSHOT_ISOLATION ON;
-- Schritt 2: READ_COMMITTED_SNAPSHOT aktivieren
-- Bewirkt, dass READ COMMITTED aus dem Version Store liest
ALTER DATABASE MusterwerkDB
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
-- WITH ROLLBACK IMMEDIATE: Aktive Verbindungen werden unterbrochen.
-- Im Produktionsbetrieb besser: erst WITH NO_WAIT testen,
-- dann im Wartungsfenster mit ROLLBACK IMMEDIATE ausführen.
-- Prüfen ob RCSI aktiv ist:
SELECT
name,
is_read_committed_snapshot_on,
snapshot_isolation_state_desc
FROM sys.databases
WHERE name = DB_NAME();
Bei Musterwerk GmbH (MWSQL01) war LCK_M_S der dominante Wait Type — klassisches Read/Write-Blocking durch die Batch-Verarbeitung. Nach RCSI-Aktivierung verschwanden 80% dieser Waits. Die Batch-Laufzeit änderte sich kaum, aber die Reporting-Queries antworteten wieder ohne Wartezeiten. Das ist das typische Bild: RCSI löst Read/Write-Blocking ohne die Schreib-Performance zu verschlechtern.
|
Hinweis: RCSI-Kosten: TempDB Version Store |
|---|
|
RCSI ist nicht kostenlos: Der Version Store in TempDB wächst proportional zur Menge der laufenden Transaktionen und ihrer Änderungsrate. Bei großen Batch-Operationen kann der Version Store erheblich anwachsen — in Kapitel 13 (TempDB) haben wir die Dimensionierung besprochen, und Kapitel 29 erklärt den Version Store im Detail. Monitoring-Query: sys.dm_tran_version_store_space_usage. |
Isolation Level im Vergleich: Was verhindert was, was kostet es?
|
Isolation Level |
Dirty Read |
Non-Rep. Read |
Phantom Read |
Read/Write-Block |
Version Store |
Empfehlung |
|---|---|---|---|---|---|---|
|
READ UNCOMMITTED |
möglich |
möglich |
möglich |
nein |
nein |
Nur Approximate Analytics |
|
READ COMMITTED |
verhindert |
möglich |
möglich |
ja |
nein |
Default — aber RCSI bevorzugen |
|
READ COMMITTED SNAPSHOT (RCSI) |
verhindert |
möglich* |
möglich* |
nein |
ja (TempDB) |
Empfohlen für OLTP |
|
REPEATABLE READ |
verhindert |
verhindert |
möglich |
hoch |
nein |
Selten sinnvoll |
|
SERIALIZABLE |
verhindert |
verhindert |
verhindert |
sehr hoch |
nein |
Nur für kritische Finanzops |
|
SNAPSHOT |
verhindert |
verhindert |
verhindert |
nein |
ja (TempDB) |
Wenn explizite Konfliktbehandlung nötig |
* RCSI liest die letzte committete Version — Non-Repeatable Reads und Phantom Reads sind theoretisch möglich, in der OLTP-Praxis aber selten ein Problem
Praktische Empfehlungen: Was du wann nutzt
Die kurze Version: Aktiviere RCSI für alle OLTP-Datenbanken. Damit sind 90% der Isolation-Level-Diskussionen erledigt.
Die längere Version mit Ausnahmen:
Isolation Level in der Applikation setzen
Isolation Levels können auf drei Ebenen gesetzt werden — und alle drei gibt es in der Praxis:
-- 1. Session-weit: SET TRANSACTION ISOLATION LEVEL
-- Gilt für alle Statements der aktuellen Verbindung
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Standard
SET TRANSACTION ISOLATION LEVEL SNAPSHOT; -- Für diese Session
-- 2. Tabellenebene: Query-Hint WITH (...)
-- Überschreibt das Session-Level für diese eine Tabelle
SELECT r.RechnungsID, r.Betrag
FROM dbo.Rechnung AS r WITH (NOLOCK)
-- Nur für diese Tabelle READ UNCOMMITTED.
-- Andere Tabellen im gleichen Query behalten das Session-Level.
-- 3. Weitere nützliche Hints (nicht nur NOLOCK)
SELECT p.ProduktID, p.Lagerbestand
FROM dbo.Produkt AS p WITH (UPDLOCK, ROWLOCK)
-- UPDLOCK: Shared Lock wird sofort zu Update Lock — verhindert
-- Deadlock-Muster "zwei Sessions lesen, dann beide updaten".
-- ROWLOCK: Hint für Zeilen-statt-Seiten-Locking (kann helfen,
-- kann aber auch mehr Locks erzeugen — nur mit Messung einsetzen).
-- .NET: Isolation Level über SqlTransaction
-- using System.Data;
-- using (var conn = new SqlConnection(connString))
-- {
-- conn.Open();
-- var tx = conn.BeginTransaction(IsolationLevel.Snapshot);
-- // Alle Befehle dieser Transaktion laufen unter SNAPSHOT
-- }
-- Entity Framework: Explizite Transaktion mit Isolation Level
-- using (var tx = context.Database.BeginTransaction(
-- System.Data.IsolationLevel.ReadCommitted))
-- {
-- // EF übergibt READ COMMITTED — bei RCSI-Datenbank optimal.
-- // Kein Code-Change nötig, wenn RCSI datenbankweit aktiv ist.
-- }
-- Wichtig: ORM-Frameworks (Entity Framework, Dapper) nutzen
-- READ COMMITTED als Default. Bei RCSI-Datenbank ist das perfekt:
-- READ COMMITTED + RCSI = kein Blocking, keine Code-Änderung.
-- Verweis Kapitel 30: ORM & Applikationsdesign.
|
Tipp: UPDLOCK als Deadlock-Prävention |
|---|
|
Ein klassisches Deadlock-Muster: Session A liest Zeile X (Shared Lock), Session B liest Zeile X (Shared Lock). Jetzt will A die Zeile updaten — kann nicht, weil B noch den Shared Lock hält. B will auch updaten — Deadlock. Lösung: WITH (UPDLOCK) beim ersten Lesen. Der Update Lock ist kompatibel mit Shared Locks anderer Sessions, aber nicht mit weiteren Update Locks. Session B muss warten, bis A seinen Update Lock freigibt. Kein Deadlock. In Kapitel 28 vertiefen wir Locking-Strategien aus Entwicklersicht. |
Symptome, Diagnose und Gegenmaßnahmen
|
Hinweis: Symptome: Isolation-Level-Probleme |
|---|
|
Symptom 1: LCK_M_S dominiert die Wait Statistics (Kapitel 9). Read/Write-Blocking durch READ COMMITTED ohne RCSI. |
|
Symptom 2: Applikation hängt bei gleichzeitigen Reads und Writes — Nutzer berichten von "Seite lädt nicht" während eine Batch-Verarbeitung läuft. |
|
Symptom 3: NOLOCK-Hints in vielen Stored Procedures — jemand hat vor Jahren Blocking mit Korrektheitsverlust bekämpft. |
|
Symptom 4: Sporadische Fehler 3960 ("Snapshot isolation transaction aborted due to update conflict") — Applikation nutzt SNAPSHOT ohne Konfliktbehandlung. |
|
Symptom 5: Unerwartete NULL-Werte oder inkonsistente Berechnungen in Reports — mögliches Zeichen für Dirty Reads durch NOLOCK. |
|
Hinweis: So misst du das |
|---|
|
Aktive Isolation Levels aller Sessions prüfen: |
-- Aktive Sessions mit ihrem Isolation Level
SELECT
s.session_id,
s.login_name,
s.program_name,
s.transaction_isolation_level,
-- 0=Unspecified, 1=ReadUncommitted, 2=ReadCommitted,
-- 3=RepeatableRead, 4=Serializable, 5=Snapshot
CASE s.transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'READ UNCOMMITTED'
WHEN 2 THEN 'READ COMMITTED'
WHEN 3 THEN 'REPEATABLE READ'
WHEN 4 THEN 'SERIALIZABLE'
WHEN 5 THEN 'SNAPSHOT'
END AS IsolationLevelName,
t.transaction_begin_time,
t.transaction_type
FROM sys.dm_exec_sessions AS s
LEFT JOIN sys.dm_tran_session_transactions AS st
ON s.session_id = st.session_id
LEFT JOIN sys.dm_tran_active_transactions AS t
ON st.transaction_id = t.transaction_id
WHERE s.is_user_process = 1
ORDER BY s.session_id;
-- RCSI-Status der Datenbank prüfen:
SELECT name,
is_read_committed_snapshot_on,
snapshot_isolation_state_desc
FROM sys.databases
WHERE name = DB_NAME();
|
Warnung: Typische Fehlinterpretationen |
|---|
|
"NOLOCK macht Queries schneller": Falsch. NOLOCK verhindert Blocking — das ist nicht dasselbe. Die Query selbst läuft nicht schneller, sie wartet nur nicht. Auf Kosten von Korrektheit. |
|
"READ COMMITTED ist sicher genug": Stimmt — aber RCSI ist genauso sicher und eliminiert Read/Write-Blocking. Es gibt keinen guten Grund für READ COMMITTED ohne RCSI in einem OLTP-System. |
|
"SNAPSHOT und RCSI sind das Gleiche": Nein. SNAPSHOT ist ein explizites Isolation Level auf Transaktions-Ebene — die Applikation muss es setzen und Konflikte behandeln. RCSI ist eine datenbankweite Einstellung, die READ COMMITTED transparent auf Versioning umstellt. Unterschiedliche Abstraktionsebene, unterschiedliche Konfliktsemantik. |
|
"RCSI verursacht zu viel TempDB-Last": Muss gemessen werden. In den meisten OLTP-Systemen ist der Version Store-Overhead klein verglichen mit dem Gewinn durch eliminiertes Blocking. |
|
Tipp: Erste Gegenmaßnahmen |
|---|
|
1. RCSI-Status prüfen: sys.databases, Spalte is_read_committed_snapshot_on. Wenn 0: RCSI nicht aktiv. |
|
2. RCSI aktivieren (nach Testing im Staging): ALTER DATABASE … SET READ_COMMITTED_SNAPSHOT ON — im Wartungsfenster mit WITH ROLLBACK IMMEDIATE. |
|
3. NOLOCK-Hints aus dem Code entfernen: Nach RCSI-Aktivierung sind sie nicht mehr nötig für Blocking-Prävention. Wenn sie aus Korrektheitsgründen bewusst gesetzt wurden: Kommentar dazu schreiben. |
|
4. Isolation Level in sys.dm_exec_sessions prüfen: Welche Sessions laufen unter SNAPSHOT oder SERIALIZABLE? Sind die Applikationen darauf vorbereitet? |
|
5. Version Store-Größe monitoren: sys.dm_tran_version_store_space_usage — wenn der Version Store explodiert, gibt es lange laufende Transaktionen die ihn aufblähen (Kapitel 29). |
Zusammenfassung
Isolation Levels sind der häufig übersehene dritte Faktor neben Indizes und Abfrageoptimierung. Die wichtigsten Punkte:
Kapitel 28 vertieft Locking aus der Entwickler-Perspektive: Welche Lock-Typen gibt es, wie lange werden sie gehalten, und wie baut man Applikationslogik die Locks minimiert ohne Konsistenz zu opfern. Row Versioning und RCSI im technischen Detail folgen in Kapitel 29 — dort auch der TempDB-Overhead unter Last.

Abb. 1: Isolation Levels: Matrix der Nebenläufigkeitsphänomene
Kapitel 28
