SQL Server Deadlocks: einen Deadlockgraph Schritt für Schritt lesen
Fehler 1205 können Sie sich leicht merken: fünf nach zwölf. Wenn die Meldung kommt, ist es für diese Transaktion schon zu spät. SQL Server hat sie bereits zurückgerollt, um einen Kreis aufzulösen, in dem sich zwei oder mehr Ausführungen gegenseitig blockiert haben. Was jetzt noch hilft, ist der Deadlockgraph: Er hält fest, wer welche Sperre hielt und worauf wartete. Dieser Beitrag liest solche Graphen von der Meldung bis zur Gegenmaßnahme, an Beispielen, die wir auf SQL Server 2017 bis 2025 nachgestellt haben.
Kurz gesagt
Sichern Sie zuerst den passenden Graph mit Zeitpunkt und Herkunft. Auf SQL Server liegt er meist schon in der Ereignissitzung system_health. Lesen Sie dann die drei Bereiche: wer das Opfer wurde (victim-list), was die Beteiligten ausgeführt haben (process-list) und wer welche Sperre hält und wer auf sie wartet (resource-list). Verfolgen Sie daraus den Kreis. Erst danach beurteilen Sie Zugriffsreihenfolge, Transaktionsdauer, Indizes und Isolation. Das Opfer ist dabei nicht der Schuldige: SQL Server wählt nach Priorität und bei gleicher Priorität die Ausführung, deren Rücknahme am wenigsten kostet.
Was Fehler 1205 über den Vorfall sagt
Die Anwendung erhält diese Meldung, im Original englisch:
Msg 1205, Level 13, State 51, Server …, Procedure dbo.Auftrag_stornieren, Line 10
Transaction (Process ID 58) was deadlocked on lock resources with another process
and has been chosen as the deadlock victim. Rerun the transaction.Daraus lassen sich drei Dinge ablesen:
- Die ganze Transaktion ist zurückgerollt, nicht nur die Anweisung in Zeile 10. Alles, was diese Transaktion vorher geändert hatte, ist rückgängig gemacht. Fünf nach zwölf eben.
- Die andere Seite läuft weiter. Sie erhält keinen Fehler. Ihre Sperren bleiben bestehen, sie arbeitet ihre Transaktion zu Ende, und nur in ihrem Protokoll sieht niemand etwas von dem Vorfall.
- Die Meldung nennt nicht die Ursache. Sie zeigt, welche Ausführung ausgewählt wurde, und mit Prozedur und Zeile, an welcher Anweisung sie gerade wartete. Mit wem sie im Kreis stand und welche Sperren beteiligt waren, steht nur im Graph.
SQL Server sucht solche Kreise selbstständig. Der Lock Monitor prüft standardmäßig alle fünf Sekunden, bei häufigen Deadlocks auch öfter, bis herunter auf 100 Millisekunden. Findet er einen Kreis, wählt er ein Opfer, rollt es zurück und schreibt das Ereignis xml_deadlock_report. Ein Deadlock ist damit etwas anderes als eine lange Blockierung: Bei einer Blockierungskette wartet jemand auf einen anderen, der irgendwann fertig wird. Bei einem Deadlock wird ohne Eingriff niemand mehr fertig. Wenn gerade etwas hängt und Sie den Verursacher suchen, hilft der Ratgeber Blocking und Timeouts. Brechen im Tagesgeschäft laufend Transaktionen ab, hilft unsere Akut-Hilfe beim Einordnen und Stabilisieren.
Den passenden Deadlockgraph finden
system_health zeichnet bereits auf
Auf SQL Server und Azure SQL Managed Instance läuft die Ereignissitzung system_health ab Werk. Sie erfasst unter anderem jeden Deadlockgraph und schreibt ihn in Dateien im Log-Verzeichnis der Instanz. Sie müssen dafür nichts einrichten und sollten die Sitzung für eine Untersuchung auch nicht verändern.
Zwei Eigenschaften sollten Sie kennen:
- Die Aufbewahrung ist begrenzt. In allen geprüften Versionen (Developer Edition) hatte
system_health10 Dateien zu je 100 MB, die reihum überschrieben werden. Microsoft nennt das für Standard und Enterprise; kleinere Editionen behalten laut Dokumentation deutlich weniger. Wie weit das zurückreicht, hängt davon ab, wie viel sonst protokolliert wird. Ein Graph, der nicht mehr da ist, beweist nicht, dass es keinen Deadlock gab. - Neue Ereignisse kommen verzögert an. Die Sitzung schreibt mit einer Verzögerung von bis zu 120 Sekunden. Im Lab stand ein Deadlock 12 Sekunden nach dem Fehler noch nicht in der Datei. Wer unmittelbar nach einem 1205 nachsieht, wartet besser zwei Minuten.
Abfrage 1 listet die neuesten 50 Deadlocks mit Zeitpunkt, Datenbank, Zahl der Beteiligten, Opfer und den Typen der beteiligten Ressourcen. Die letzte Spalte enthält den vollständigen Graph. In SQL Server Management Studio öffnet ein Klick darauf das XML. Speichern Sie es mit der Endung .xdl, zeigt SSMS den Graph beim nächsten Öffnen grafisch an.
/* PSG · Deadlock · Abfrage 1 Welche Deadlocks hat system_health noch gespeichert? Die neuesten 50, mit Graph. Nur lesend. Liest die Ereignisdateien der Session system_health, ändert nichts. Berechtigung: VIEW SERVER STATE, ab SQL Server 2022 genügt VIEW SERVER PERFORMANCE STATE. Die Dateien sind in Größe und Anzahl begrenzt: Ein fehlender alter Graph beweist nicht, dass es keinen Deadlock gab. Neue Deadlocks erscheinen mit bis zu 120 Sekunden Verzögerung. Zeiten in UTC. Bewusst ohne Zeitfilter: Ein Filter direkt auf timestamp_utc lieferte auf SQL Server 2017 bis 2022 im Test keine einzige Zeile. Große system_health-Dateien zu lesen kann einige Sekunden dauern. MAXDOP 1: Die Abfrage läuft seriell und belegt keine weiteren Kerne. */SELECT TOP (50) d.zeit_utc, DB_NAME(d.graph.value('(/deadlock/process-list/process/@currentdb)[1]', 'int')) AS datenbank, d.graph.value('count(/deadlock/process-list/process)', 'int') AS prozesse, d.graph.value('(/deadlock/victim-list/victimProcess/@id)[1]', 'varchar(64)') AS opfer_prozess, d.graph.query('for $r in /deadlock/resource-list/* return local-name($r)') .value('.', 'nvarchar(400)') AS ressourcen, d.graph AS deadlockgraphFROM ( SELECT f.timestamp_utc AS zeit_utc, CAST(f.event_data AS xml) .query('/event/data[@name="xml_report"]/value/deadlock') AS graph FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) AS f WHERE f.object_name = N'xml_deadlock_report') AS dORDER BY d.zeit_utc DESCOPTION (MAXDOP 1);So sah das Ergebnis im Prüflauf auf SQL Server 2022 aus, gekürzt um die Graphspalte. Die letzten Deadlocks stammen aus dem Fall „Eine Tabelle, die sich selbst im Weg steht“, der in kurzen Abständen auftrat:
zeit_utc datenbank prozesse opfer_prozess ressourcen
2026-11-07 06:32:32.3210000 DeadlockLab 2 processf10413848 keylock keylock
2026-11-07 06:32:32.2100000 DeadlockLab 2 processf10413848 keylock keylock
2026-11-07 06:32:32.1030000 DeadlockLab 2 processf10413848 keylock keylockAbfrage 1 lief auf SQL Server 2017, 2019, 2022 und 2025 ohne Fehler.
Ein Login ohne Berechtigung erhält Fehler 300 (VIEW SERVER STATE permission was denied …, ab 2022 VIEW SERVER PERFORMANCE STATE …). Auf SQL Server 2017 und 2019 genügt VIEW SERVER STATE, ab 2022 auch das engere VIEW SERVER PERFORMANCE STATE.
Vorsicht beim Zeitfilter auf SQL Server 2017 bis 2022
Naheliegend wäre, Abfrage 1 auf die letzten 24 Stunden einzuschränken, etwa mit WHERE f.timestamp_utc > DATEADD(day, -1, SYSUTCDATETIME()). Auf SQL Server 2017 und 2022 lieferte genau dieser Filter im Lab keine einzige Zeile, obwohl die Datei den Deadlock enthielt. Dasselbe galt für einen festen Zeitpunkt und für eine Variable. SQL Server 2019 verhielt sich in einer weiteren Prüfung gleich. Auf SQL Server 2025 funktionierte derselbe Filter.
Wer sich darauf verlässt, schließt aus einer leeren Liste womöglich, dass es keine Deadlocks gab. Abfrage 1 verzichtet deshalb auf den Filter und sortiert nur. Brauchen Sie einen Zeitraum, wandeln Sie den Zeitstempel zuerst um und filtern danach. Das lieferte auf allen vier Versionen das richtige Ergebnis, und auch das Beispiel in der Microsoft-Dokumentation zu sys.fn_xe_file_target_read_file wandelt den Zeitstempel so um:
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) AS fCROSS APPLY (SELECT CAST(f.timestamp_utc AS datetime2(7)) AS ts) AS xWHERE f.object_name = N'xml_deadlock_report' AND x.ts > DATEADD(day, -1, SYSUTCDATETIME())Geprüft haben wir das auf Linux mit SQL Server 2017 CU31, 2019 CU32, 2022 CU27 und 2025 CU9. Die Ursache haben wir nicht geklärt, Windows-Installationen nicht geprüft.
Welche Angaben zum Graph gehören
Ein Graph allein ist schwer einzuordnen. Notieren Sie dazu:
- Zeitpunkt mit Zeitzone. Die Ereignisdateien speichern UTC, die Anwendungsprotokolle oft Ortszeit.
- Version und Build der Instanz, die Datenbank und, ab SQL Server 2025, ob Optimized Locking eingeschaltet ist.
- Das Symptom in der Anwendung: welche Funktion den Fehler meldete und wie oft.
- Das unveränderte XML. Für die eigene Untersuchung bleibt es, wie es ist. Bevor Sie es weitergeben, entfernen Sie Login-, Rechner- und Anwendungsnamen sowie Parameterwerte im Eingabepuffer, die Kundendaten enthalten können.
Was auf Azure anders ist
Azure SQL Managed Instance verhält sich wie SQL Server und hat system_health. Azure SQL Database hat keine solche Sitzung. Microsoft beschreibt dort eine eigene Ereignissitzung auf Datenbankebene für Deadlocks. Diesen Weg haben wir für diesen Beitrag nicht nachgestellt; die Microsoft-Anleitung für Azure SQL Database beschreibt ihn.
Ein Beispiel: zwei Transaktionen warten im Kreis
Ein Bestellsystem hat zwei Codewege. Der eine bucht einen Auftrag und reserviert dafür zuerst Ware im Lager, dann vermerkt er die Buchung am Auftrag. Der andere storniert einen Auftrag und geht umgekehrt vor: erst der Auftrag, dann das Lager. Jeder für sich ist korrekt. Zwei Entwickler haben sie zu verschiedenen Zeiten geschrieben.
CREATE PROCEDURE dbo.Auftrag_buchen @AuftragID int, @ArtikelID int, @Menge intASBEGIN TRANSACTION; UPDATE dbo.Lager SET Reserviert = Reserviert + @Menge -- zuerst Lager WHERE ArtikelID = @ArtikelID; -- weitere Arbeit in derselben Transaktion UPDATE dbo.Auftrag SET Gebucht = Gebucht + @Menge WHERE AuftragID = @AuftragID;COMMIT TRANSACTION;GOCREATE PROCEDURE dbo.Auftrag_stornieren @AuftragID int, @ArtikelID int, @Menge intASBEGIN TRANSACTION; UPDATE dbo.Auftrag SET Gebucht = Gebucht - @Menge -- zuerst Auftrag WHERE AuftragID = @AuftragID; -- weitere Arbeit in derselben Transaktion UPDATE dbo.Lager SET Reserviert = Reserviert - @Menge WHERE ArtikelID = @ArtikelID;COMMIT TRANSACTION;Laufen beide gleichzeitig für denselben Auftrag, entsteht der Kreis:
| Zeitpunkt | Sitzung A: buchen | Sitzung B: stornieren |
|---|---|---|
| 0,0 s | sperrt die Lagerzeile (X) | |
| 0,5 s | sperrt die Auftragszeile (X) | |
| 2,0 s | will die Auftragszeile, wartet auf B | |
| 2,5 s | will die Lagerzeile, wartet auf A | |
| bis 5 s | Der Lock Monitor erkennt den Kreis und wählt B als Opfer |
A hält das Lager und braucht den Auftrag. B hält den Auftrag und braucht das Lager. Keine der beiden kann allein weiterlaufen. Die Pause zwischen den beiden Anweisungen haben wir im Lab mit WAITFOR erzeugt, damit der Fall jedes Mal eintritt. In einer Anwendung steht dort Rechenarbeit, ein Aufruf eines anderen Dienstes oder schlicht die Zeit zwischen zwei Datenbankaufrufen. Je länger eine Transaktion offen ist, desto größer ist das Zeitfenster für den Kreis.
Die drei Bereiche des Graphs lesen
So sieht der Graph dieses Falls auf SQL Server 2022 aus, gekürzt auf die Angaben, die für das Lesen zählen. Klicken Sie eine Prozess-ID an: Der Graph markiert sie in allen drei Bereichen.
victim-list: Welche Ausführung wurde ausgewählt?
Die Opferliste enthält nur eine Prozess-ID, hier processf077ab848. Suchen Sie diese ID in der Prozessliste: Es ist Sitzung 58, die stornieren wollte, also B. Die IDs process… sind interne Adressen. Sie gelten nur innerhalb dieses einen Graphs und verbinden seine drei Bereiche miteinander.
Wen SQL Server auswählt, folgt zwei Regeln. Zuerst zählt die Priorität, die eine Sitzung mit SET DEADLOCK_PRIORITY festlegen kann. Bei gleicher Priorität wählt SQL Server die Ausführung, deren Rücknahme weniger kostet. Im Graph zeigt das Attribut logused, wie viele Byte Log jede Transaktion bis dahin geschrieben hat. Im Lab hat sich das auf allen Versionen so bestätigt:
| Ausgangslage | Opfer |
|---|---|
| gleiche Priorität, gleich viel Log (je 240 bis 6.204 Byte) | B in sechs von sieben Läufen, einmal A (SQL Server 2019) |
B mit SET DEADLOCK_PRIORITY HIGH (im Graph priority="5") | A in sieben von sieben |
| B hatte vorher 1.500 andere Zeilen geändert (180 bis 495 KB Log) | A in sieben von sieben |
Bei Gleichstand ist das Opfer also nicht vorhersagbar; Microsoft beschreibt die Auswahl in diesem Fall als zufällig. Und das Opfer ist kein Schuldspruch: Im dritten Fall „gewinnt“ B, nur weil B mehr Arbeit verloren hätte. Beide Codewege sind gleich falsch sortiert. Eine niedrige Priorität für unwichtige Hintergrundarbeit kann sinnvoll sein. Sie entscheidet aber nur, wer verliert, nicht ob es einen Verlierer gibt.
process-list: Was haben die Beteiligten ausgeführt?
Jeder process beschreibt eine beteiligte Ausführung zum Zeitpunkt des Deadlocks:
spidist die Sitzungs-ID, dieselbe wie in der Fehlermeldung (Process ID 58).isolationlevelundlogusedbeschreiben die Transaktion: unter welcher Isolationsstufe sie lief und wie viel Log sie bis dahin geschrieben hatte.waitresourceundlockModesagen, worauf die Ausführung wartete und in welchem Modus sie sperren wollte.waittimeist die Wartezeit bis zur Auflösung in Millisekunden.executionStackzeigt die Aufrufkette: Prozedur und Zeile der wartenden Anweisung, darunter der Aufrufer. Der Anweisungstext im Frame ist auf manchen Versionen abgeschnitten, wie hier bei@ArtikelI….inputbufenthält den Stapel, den der Client zuletzt geschickt hat. Bei einer Prozedur ist das der Aufruf mit Parametern, bei einer Anwendung mit Abfrageparametern oft nur die parametrisierte Anweisung.
Lesen Sie die Prozessliste als Momentaufnahme: Sie zeigt die Anweisung, die wartete, nicht die, die die Sperre genommen hat. Bei B wartet Zeile 10 auf das Lager. Die Sperre auf den Auftrag, mit der B den Kreis schließt, stammt aus einer früheren Anweisung derselben Transaktion, die im Graph nicht vorkommt. Dafür brauchen Sie den Code.
resource-list: Wer hält, und wer wartet?
Die Ressourcenliste ist der eigentliche Kreis. Jede Ressource nennt ihren Typ, hier keylock (eine Zeile in einem Index), das Objekt und den Index. Darunter stehen in owner-list die Prozesse, die eine Sperre halten, und in waiter-list die, die warten, jeweils mit Modus.
Eine Eigenheit lohnt einen zweiten Blick. Die Lagerzeile und die Auftragszeile haben im waitresource denselben Wert in Klammern, (8194443284a0). Dieser Hashwert wird aus dem Schlüssel berechnet, und beide Zeilen haben den Schlüssel 1. Erst die Zahl davor, die hobtid des Index, unterscheidet sie. Der Hash allein benennt also keine Zeile, und die hobtid ist eine interne Nummer, die von Instanz zu Instanz verschieden ist. Lesen Sie Objekt und Index deshalb aus objectname und indexname, nicht aus dem waitresource.
Abfrage 2 nimmt einen Graph aus system_health und zerlegt ihn in diese beiden Sichten: je Beteiligtem eine Zeile und je Sperre die Halter und Wartenden.
/* PSG · Deadlock · Abfrage 2 Einen Deadlockgraph in Beteiligte und Ressourcen zerlegen. Nur lesend, liest system_health wie Abfrage 1. @nr = 1 ist der neueste gespeicherte Deadlock. Ergebnis 1: je Prozess eine Zeile mit Opfer, Sitzung, Isolation, worauf er wartet, Modul und Eingabepuffer. Ergebnis 2: je Sperre und Beteiligtem eine Zeile, erst wer sie hält, dann wer wartet. Berechtigung wie Abfrage 1. MAXDOP 1: Die Abfrage läuft seriell und belegt keine weiteren Kerne. */DECLARE @nr int = 1; -- 1 = neuester, 2 = zweitneuester …DECLARE @graph xml; SELECT @graph = g.graphFROM ( SELECT ROW_NUMBER() OVER (ORDER BY f.timestamp_utc DESC) AS nr, CAST(f.event_data AS xml) .query('/event/data[@name="xml_report"]/value/deadlock') AS graph FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) AS f WHERE f.object_name = N'xml_deadlock_report') AS gWHERE g.nr = @nrOPTION (MAXDOP 1); -- Ergebnis 1: die BeteiligtenSELECT x.prozess, CASE WHEN @graph.exist( '/deadlock/victim-list/victimProcess[@id = sql:column("x.prozess")]') = 1 THEN 'ja' ELSE '' END AS opfer, p.n.value('@spid', 'int') AS session_id, p.n.value('@isolationlevel', 'varchar(40)') AS isolation, p.n.value('@waitresource', 'varchar(200)') AS wartet_auf, p.n.value('@lockMode', 'varchar(10)') AS modus, p.n.value('@waittime', 'int') AS wartezeit_ms, p.n.value('@logused', 'bigint') AS log_bytes, p.n.value('@trancount', 'int') AS trancount, p.n.value('(executionStack/frame/@procname)[1]', 'nvarchar(400)') AS modul, p.n.value('(executionStack/frame/@line)[1]', 'int') AS zeile, LTRIM(RTRIM(p.n.value('(inputbuf)[1]', 'nvarchar(4000)'))) AS eingabepuffer, p.n.value('@clientapp', 'nvarchar(200)') AS anwendung, p.n.value('@hostname', 'nvarchar(200)') AS clientFROM @graph.nodes('/deadlock/process-list/process') AS p(n)CROSS APPLY (SELECT p.n.value('@id', 'varchar(64)') AS prozess) AS xOPTION (MAXDOP 1); -- Ergebnis 2: die Ressourcen mit Haltern und WartendenSELECT r.n.value('@id', 'varchar(64)') AS sperre, r.n.value('local-name(.)', 'varchar(40)') AS ressource, COALESCE(r.n.value('@objectname', 'nvarchar(400)'), r.n.value('(UnderlyingResource/*/@objectname)[1]', 'nvarchar(400)')) AS objekt, COALESCE(r.n.value('@indexname', 'nvarchar(200)'), r.n.value('(UnderlyingResource/*/@indexname)[1]', 'nvarchar(200)')) AS index_name, b.rolle, b.prozess, b.modusFROM @graph.nodes('/deadlock/resource-list/*') AS r(n)CROSS APPLY ( SELECT N'hält', o.n.value('@id', 'varchar(64)'), o.n.value('@mode', 'varchar(10)') FROM r.n.nodes('owner-list/owner') AS o(n) UNION ALL SELECT N'wartet', w.n.value('@id', 'varchar(64)'), w.n.value('@mode', 'varchar(10)') FROM r.n.nodes('waiter-list/waiter') AS w(n)) AS b(rolle, prozess, modus)ORDER BY sperre, b.rolle -- je Sperre: erst „hält“, dann „wartet“OPTION (MAXDOP 1);Für den neuesten Deadlock im Prüflauf, den Fall aus dem Abschnitt „Eine Tabelle, die sich selbst im Weg steht“, lieferte Abfrage 2 auf SQL Server 2022 diese beiden Ergebnisse, gekürzt um Eingabepuffer, Anwendung und Client:
prozess opfer session_id wartet_auf modus log_bytes modul zeile
processf10413848 ja 54 KEY: 5:72057594045923328 (40fd182c0dd9) S 0 adhoc 9
processf10411848 64 KEY: 5:72057594045988864 (1e3e71606df0) X 244 adhoc 10
sperre ressource objekt index_name rolle prozess modus
lockf17d5dd00 keylock DeadlockLab.dbo.Konto IX_Konto_Status hält processf10413848 S
lockf17d5dd00 keylock DeadlockLab.dbo.Konto IX_Konto_Status wartet processf10411848 X
lockf17d5e500 keylock DeadlockLab.dbo.Konto PK_Konto hält processf10411848 X
lockf17d5e500 keylock DeadlockLab.dbo.Konto PK_Konto wartet processf10413848 SJe Sperre stehen Halter und Wartender untereinander. Daraus lässt sich der Kreis direkt ablesen: Der Leser (Sitzung 54) hält IX_Konto_Status und wartet auf PK_Konto, der Schreiber (Sitzung 64) umgekehrt. Bei Ad-hoc-Anweisungen steht in modul nur adhoc; welche Anweisung es war, zeigt dann der Eingabepuffer. Abfrage 2 lief auf SQL Server 2017, 2019, 2022 und 2025 ohne Fehler, auch mit xactlock-Ressourcen unter Optimized Locking.
Den Kreis zeichnen und eine Hypothese bilden
Mit der Ressourcenliste können Sie den Kreis aufzeichnen. Schreiben Sie jede Sperre als Kasten, jeden Prozess als Kreis. Ein Pfeil vom Kasten zum Prozess heißt „wird gehalten von“, ein Pfeil vom Prozess zum Kasten „wartet auf“. Im Beispiel:
B wartet auf
PK_Lager, das A hält. A wartet aufPK_Auftrag, das B hält.
Aus dem Bild folgt die kleinste Erklärung, die der Graph trägt: Zwei Transaktionen ändern dieselben zwei Zeilen in entgegengesetzter Reihenfolge und halten die erste Sperre, während sie auf die zweite warten. Was der Graph nicht zeigt, schreiben Sie als offene Frage dazu: Welche frühere Anweisung hat die gehaltene Sperre genommen? Wie lange war die Transaktion offen? Mit welchen Parametern lief sie? Kommt der Fall regelmäßig vor oder nur bei einer bestimmten Datenkonstellation? Die Antworten stehen im Code, in den Anwendungsprotokollen und, für Häufigkeit und Pläne, im Query Store.
Oft gehört der Code nicht Ihnen, sondern dem Hersteller der Anwendung. Dann sind Graph, Hypothese und offene Fragen zusammen der Beleg, mit dem der Hersteller ansetzen kann. Besser als „wir haben Deadlocks“ ist: „Diese zwei Prozeduren ändern Lager und Auftrag in entgegengesetzter Reihenfolge.“ In unserer Performance-Betreuung PSG MX gehört genau das zur Arbeit: Befunde belegen und Änderungen mit Ihrem Team und dem Hersteller abstimmen.
Eine Tabelle, die sich selbst im Weg steht
Ein Deadlock braucht nicht zwei Tabellen. Auch eine einzige Tabelle kann sich über ihre eigenen Indizes in einen Kreis bringen. Der Fall überrascht, weil im Code nur eine harmlose Abfrage und eine einfache Änderung stehen.
Die Tabelle dbo.Konto hat einen gruppierten Primärschlüssel auf KontoID und einen zusätzlichen Index auf Status. Ein Leser sucht über diesen Index und holt den Saldo, der nur im gruppierten Index steht, per Key Lookup nach. Ein Schreiber ändert dieselben Konten über den Primärschlüssel und setzt dabei einen neuen Status. Damit muss er nach der Zeile im gruppierten Index auch den Eintrag in IX_Konto_Status ändern.
Beide greifen auf dieselben zwei Indizes zu, aber in umgekehrter Reihenfolge:
- Der Leser hält eine S-Sperre auf dem Eintrag in
IX_Konto_Statusund will die Zeile inPK_Kontolesen. - Der Schreiber hält eine X-Sperre auf der Zeile in
PK_Kontound will den Eintrag inIX_Konto_Statusändern.
So sieht das im Graph aus, gekürzt auf die Ressourcenliste, SQL Server 2025 ohne Optimized Locking:
<keylock objectname="DeadlockLab.dbo.Konto" indexname="PK_Konto" mode="X">
<owner-list><owner id="process…478" mode="X"/></owner-list> <!-- Schreiber -->
<waiter-list><waiter id="process…088" mode="S"/></waiter-list> <!-- Leser -->
</keylock>
<keylock objectname="DeadlockLab.dbo.Konto" indexname="IX_Konto_Status" mode="S">
<owner-list><owner id="process…088" mode="S"/></owner-list> <!-- Leser -->
<waiter-list><waiter id="process…478" mode="X"/></waiter-list> <!-- Schreiber -->
</keylock>Ein Objekt, zwei Indizes, ein Kreis. In der Werkstatt können Sie den Ablauf nachspielen und die beiden Gegenmittel zuschalten:
Im Lab haben wir Leser und Schreiber je 15 Sekunden in einer Schleife laufen lassen:
Leser und Schreiber auf dbo.Konto, je 15 Sekunden
CREATE INDEX IX_Konto_Status ON dbo.Konto (Status); -- LeserSELECT @s = SUM(Saldo) FROM dbo.Konto WHERE Status = 7; -- Saldo per Key Lookup -- SchreiberUPDATE dbo.KontoSET Status = CASE WHEN Status = 7 THEN 8 ELSE 7 END, Saldo = Saldo + 1WHERE KontoID IN (7, 1007, 2007, 3007); -- erst PK_Konto, dann IX_Konto_Status31 bis 82 Deadlocks in 15 s, Opfer immer der Leser · Leser 462 bis 936 Durchläufe
CREATE INDEX IX_Konto_Status ON dbo.Konto (Status) INCLUDE (Saldo) -- kein Lookup mehr WITH (DROP_EXISTING = ON); -- Leser und Schreiber unverändert0 Deadlocks · Leser 2.958 bis 27.569 Durchläufe
ALTER DATABASE DeadlockLab SET READ_COMMITTED_SNAPSHOT ON; -- Leser liest Versionen -- Index ohne INCLUDE, Leser und Schreiber unverändert0 Deadlocks · Leser 272.126 bis 314.783 Durchläufe (2022, 2025 mit Optimized Locking)
Ohne RCSI: SQL Server 2017 bis 2025, auf 2017 bis 2022 je zwei Läufe. Mit RCSI: nur 2022 und 2025. Der Plan des Lesers wurde je Lauf im Plancache geprüft.
Was die Zahlen zeigen:
- Der Kreis entsteht auf allen Versionen, von SQL Server 2017 bis 2025, mit 31 bis 82 Deadlocks in 15 Sekunden. Das Opfer war in jedem Lauf der Leser. Er hatte kein Log geschrieben, seine Rücknahme kostete nichts.
- Der abdeckende Index beseitigt ihn, weil der Leser den gruppierten Index nicht mehr anfasst. Nebenbei schaffte der Leser in sieben von acht Läufen 17- bis 42-mal so viele Durchläufe, weil er auch nicht mehr blockiert wurde. In einem Lauf auf SQL Server 2019 waren es nur dreimal so viele; den Grund haben wir nicht geklärt.
- RCSI beseitigt ihn ebenfalls, weil der Leser unter Read Committed Snapshot keine Sperren nimmt, sondern die letzte bestätigte Version liest.
- Optimized Locking allein hilft hier nicht. Auf SQL Server 2025 mit Optimized Locking, aber ohne RCSI gab es 80 Deadlocks in 15 Sekunden. Der Leser sperrt unter Read Committed weiterhin.
Welcher Weg passt, ist eine Abwägung. Ein abdeckender Index löst diesen Kreis für diese Abfrage, macht den Index aber größer und muss bei jeder Änderung der eingeschlossenen Spalten mitgepflegt werden. Er ist außerdem für alle Abfragen der Tabelle sichtbar. Wie Sie das prüfen, beschreibt der Beitrag zur Indexoptimierung. RCSI verändert die Lesesemantik der ganzen Datenbank und braucht Platz für Zeilenversionen in der TempDB. Es ist eine Entscheidung für die Anwendung, nicht für eine Abfrage.
Wenn mehr als zwei beteiligt sind
Ein Kreis kann beliebig viele Glieder haben. Im Lab haben wir drei und vier Sitzungen hintereinandergeschaltet: Jede ändert ihre eigene Lagerzeile, wartet und will dann die Zeile der nächsten. Die letzte will die Zeile der ersten.
Auf allen Versionen entstand ein einziger Graph mit drei beziehungsweise vier Prozessen und ebenso vielen Ressourcen. SQL Server wählte genau ein Opfer. Damit war der Kreis offen, und die übrigen Sitzungen liefen nacheinander durch.
Bei solchen Graphen hilft die Prozessliste allein nicht weiter, denn alle Beteiligten führen dieselbe Prozedur aus und warten auf dasselbe Objekt. Im Lab lagen alle vier Ressourcen auf PK_Lager und unterschieden sich nur im Hashwert. Den Kreis finden Sie über die Ressourcenliste: Beginnen Sie beim Opfer, suchen Sie die Sperre, auf die es wartet, deren Halter, dann die Sperre, auf die dieser wartet, und so fort, bis Sie wieder beim Opfer ankommen. Ergebnis 2 von Abfrage 2 liefert dafür je Sperre den Halter und den Wartenden in aufeinanderfolgenden Zeilen.
Ein Kreis mit vielen Gliedern bedeutet meist dasselbe wie einer mit zwei: Mehrere Abläufe ändern dieselbe Art von Zeilen in unterschiedlicher Reihenfolge. Er sieht nur bedrohlicher aus.
Welche Gegenmaßnahme zu diesem Fall passt
Für jeden Kreis gibt es mehrere mögliche Gegenmaßnahmen. Welche passt, hängt davon ab, wie er entsteht.
Dieselbe Zugriffsreihenfolge in allen Codewegen
Für den Kreis aus dem Beispiel ist das die direkte Lösung: Auch das Stornieren ändert zuerst das Lager und dann den Auftrag. Wir haben beide Codewege je 300-mal gleichzeitig laufen lassen, mit 10 Millisekunden Arbeit zwischen den beiden Anweisungen, auf SQL Server 2022 und 2025 in fünf Konfigurationen, je einmal:
| Deadlocks | Laufzeit | |
|---|---|---|
| umgekehrte Reihenfolge | 1 bis 50 je Lauf | 5,6 bis 22,7 s |
| gleiche Reihenfolge | 0 in allen fünf Konfigurationen (zusammen 3.000 Vorgänge) | 5,3 bis 6,1 s |
Die Deadlockzahl bei umgekehrter Reihenfolge streute von Lauf zu Lauf stark. Sie taugt nicht für einen Vergleich der Konfigurationen, wohl aber für den Befund: Mit gleicher Reihenfolge entstand kein einziger Kreis. Die Abläufe warteten nur aufeinander, und das kostete weniger Zeit als die Deadlocks.
In der Praxis heißt das: eine feste Reihenfolge der Tabellen für schreibende Transaktionen festlegen, etwa vom Kopf- zum Positionsdatensatz oder alphabetisch, und bei mehreren Zeilen derselben Tabelle nach dem Schlüssel sortiert ändern.
Transaktionen kurz halten
Je kürzer eine Transaktion ihre Sperren hält, desto kleiner ist das Zeitfenster, in dem ein anderer Ablauf dazwischenkommen kann. Gehören Benutzerinteraktion, Dienstaufrufe oder aufwendige Berechnungen nicht zwingend in die Transaktion, ziehen Sie sie davor oder danach.
Den Zugriffsweg prüfen: wenn ein Scan den Kreis schließt
Manche Kreise entstehen nicht durch die Reihenfolge im Code, sondern durch den Plan. Im Lab ändert Sitzung A die Rechnung 10, Sitzung B die Rechnung 20. Danach ändert A alle Rechnungen von Kunde 111, B alle von Kunde 222. Es gibt keine Zeile, die beide ändern wollen. Trotzdem entstand auf allen Versionen ein Deadlock.
Der Grund: Für Kundennr gab es keinen Index. Jede Sitzung musste die ganze Tabelle durchsuchen und dabei jede Zeile kurz sperren, um den Kunden zu prüfen. A stieß dabei auf die Zeile 20 von B, B auf die Zeile 10 von A. Im Graph erscheint das als Warten im Modus U auf eine fremde Zeile, die eigentlich gar nicht gemeint war.
| Konfiguration | ohne Index auf Kundennr | mit Index |
|---|---|---|
| SQL Server 2017 bis 2025, Read Committed | Deadlock | kein Deadlock |
| SQL Server 2022 mit RCSI | Deadlock | kein Deadlock |
| SQL Server 2025 mit Optimized Locking | Deadlock | kein Deadlock |
| SQL Server 2025 mit Optimized Locking und RCSI | kein Deadlock | kein Deadlock |
Ein passender Index löst diesen Kreis auf allen Versionen, weil jede Sitzung nur noch ihre eigenen Zeilen anfasst. RCSI allein hilft hier nicht, denn die Suche nach den zu ändernden Zeilen sperrt auch unter RCSI. Erst auf SQL Server 2025 mit Optimized Locking und RCSI prüft SQL Server die Bedingung, ohne die fremde Zeile zu sperren (Lock After Qualification). Das gilt laut Microsoft nicht für alle Pläne und Anweisungen, etwa nicht mit Sperrhinweisen wie UPDLOCK; geprüft haben wir nur dieses einfache UPDATE.
Wie Sie für eine solche Abfrage einen geeigneten Index ableiten, beschreibt der Beitrag zur Indexoptimierung. Ein Graph mit Sperren auf Zeilen, die fachlich nichts miteinander zu tun haben, ist ein guter Anlass, die Pläne der beteiligten Anweisungen anzusehen.
Welche Abfragen eine Tabelle durchsuchen, statt gezielt zu suchen, zeigt in unseren Analysen das Zugriffsprofil in PSG QX, gewichtet nach Rechenzeit und aus der vorhandenen Ausführungshistorie im Query Store. Den Deadlock selbst sieht QX nicht, wohl aber die Zugriffswege wie hier den Scan, die einen solchen Kreis erst möglich machen.
Isolation und Hinweise nur mit Blick auf den konkreten Kreis
RCSI, Optimized Locking und Sperrhinweise sind keine allgemeinen Schalter gegen Deadlocks. Im Lab half RCSI beim Lesen über zwei Indizes, nicht aber beim Kreis aus umgekehrter Reihenfolge und nicht beim Scan. Optimized Locking allein beseitigte keinen der drei Fälle. Ändern Sie Isolation oder Hinweise deshalb nur, wenn Sie wissen, welche Sperre im Graph Sie damit vermeiden, und wenn die Anwendung die geänderte Lesesemantik verträgt. Wer solche Abwägungen im Team sicher treffen will: Nebenläufigkeit gehört neben Plänen und Statistiken zu unserem Training Performance Troubleshooting für DBAs und technischen Support.
Warum die Fehlerbehandlung ebenfalls dazugehört
Auch nach einer guten Gegenmaßnahme bleibt ein Restrisiko. Microsoft empfiehlt deshalb im Deadlock-Leitfaden, 1205 in der Anwendung zu behandeln und die Transaktion nach einer kurzen, zufällig gewählten Pause erneut zu versuchen. Entscheidend ist, was wiederholt wird. Wir haben die Varianten unter Last verglichen, je 300 Buchungen und Stornierungen mit umgekehrter Reihenfolge, auf SQL Server 2022 und 2025 in fünf Konfigurationen:
| Variante | Ergebnis |
|---|---|
| ohne Wiederholung | Bestand stimmig, aber bis zu 47 Buchungen fehlten (253 statt 300) |
| ganze Transaktion bis zu dreimal wiederholen, 10 bis 49 ms Pause | alle 600 Vorgänge erfolgreich, Bestand stimmig |
| nur den letzten Befehl erneut schicken | Bestand falsch, Abweichung genau so groß wie die Zahl der Wiederholungen, im ungünstigsten Lauf 83 |
Die dritte Variante ist der typische Fehler. Die Anwendung bekommt 1205 beim zweiten UPDATE und schickt dieses UPDATE noch einmal. Das erste UPDATE hat SQL Server aber mit der ganzen Transaktion zurückgerollt. Danach steht die Buchung am Auftrag, aber nicht im Lager. Fünf nach zwölf heißt: Alles seit BEGIN TRANSACTION ist weg.
Für einen Retry gilt deshalb:
- Die ganze Transaktion wiederholen, von
BEGIN TRANSACTIONan, mit frisch gelesenen Daten. - Wenige Versuche mit kurzer, zufällig gestreuter Pause, damit die Beteiligten nicht im selben Takt wieder aufeinandertreffen.
- Nebenwirkungen außerhalb der Datenbank beachten: Eine bereits verschickte Mail oder ein Aufruf an ein anderes System wird nicht zurückgerollt.
- Wiederholungen protokollieren. Ein Retry macht den Fehler unsichtbar, nicht die Ursache. Wenn die Zahl der Wiederholungen steigt, ist das ein Signal.
Deadlocks mit Optimized Locking erkennen
Ab SQL Server 2025 kann eine Datenbank Optimized Locking verwenden. Es setzt beschleunigte Datenbankwiederherstellung (ADR) voraus. Eine geänderte Zeile bleibt dann nicht bis zum Ende der Transaktion gesperrt. Stattdessen hält die Transaktion eine Sperre auf ihre eigene Transaktions-ID, und wer die Zeile ändern will, wartet auf diese.
In Azure SQL Database ist Optimized Locking laut Microsoft immer aktiv, dort sehen Sie die neue Form also ohne eigenes Zutun. Den Kreis aus umgekehrter Reihenfolge verhindert das nicht. Im Lab entstand er mit Optimized Locking genauso, mit und ohne RCSI. Der Graph sieht aber anders aus:
Drei Unterschiede zum klassischen Graph:
- Die Ressource heißt
xactlockstattkeylock. Gemeint ist die Transaktion, die die Zeile geändert hat. Welche Zeile das ist, steht darunter inUnderlyingResource. - Der Wartende fordert
San, nichtX. Er wartet darauf, dass die andere Transaktion endet. - Das
waitresourcebeginnt mitXACT:und nennt danach den Schlüssel.
Gemischte Graphen kommen ebenfalls vor. Beim Lesen über zwei Indizes stand unter Optimized Locking ohne RCSI ein xactlock auf PK_Konto neben einem klassischen keylock auf IX_Konto_Status. Abfrage 2 liest beide Formen: Bei xactlock holt sie Objekt und Index aus UnderlyingResource. Die Meldung 1205 hatte unter Optimized Locking im Lab übrigens den State 72 statt 51, der Wortlaut blieb gleich.
Häufige Fragen
Ist das Deadlockopfer der Verursacher? Nein. Das Opfer ist die Ausführung, die SQL Server zum Auflösen des Kreises ausgewählt hat, bei gleicher Priorität die mit der billigeren Rücknahme. Für die Ursache betrachten Sie alle Beteiligten, ihre Sperren und die zugehörigen Transaktionen.
Kann ich einen Deadlock aus dem Query Store rekonstruieren? Nicht den Kreis. Der Query Store liefert Abfragen, Pläne und zusammengefasste Laufzeit- und Wartedaten. Ein Deadlockopfer erschien in einem früheren PSG-Lab dort als Ausführung vom Typ Exception, und Sperrwartezeiten sind als Kategorie sichtbar. Wer wen blockiert hat, steht nur im Deadlockgraph. Für die Pläne der beteiligten Anweisungen ist der Query Store aber eine gute Ergänzung.
Soll die Anwendung nach 1205 den Befehl einfach wiederholen? Nicht den Befehl, sondern die ganze Transaktion. Wiederholt die Anwendung nur die letzte Anweisung, fehlt alles, was die Transaktion vorher geändert hatte. Im Lab passten Lager und Aufträge danach nicht mehr zusammen.
Hilft NOLOCK gegen Deadlocks? NOLOCK ändert, was ein Leser sieht: Er kann nicht bestätigte und doppelte Zeilen lesen oder Zeilen übersehen. Gegen Kreise zwischen Schreibern hilft es nicht. Wenn ein Leser beteiligt ist, wie beim Lesen über zwei Indizes, sind ein abdeckender Index oder RCSI die sauberen Wege.
Verhindern RCSI oder Optimized Locking Deadlocks? Manche, nicht alle. Im Lab beseitigte RCSI den Kreis zwischen Leser und Schreiber, Optimized Locking mit RCSI zusätzlich den Scan-Fall. Den Kreis aus umgekehrter Zugriffsreihenfolge verhinderte keiner der Schalter.
Warum finde ich einen bekannten Deadlock nicht mehr? Weil system_health nur eine begrenzte Menge Ereignisse aufbewahrt und ältere überschreibt, weil ein neuer Deadlock erst nach bis zu zwei Minuten in der Datei steht, oder, auf SQL Server 2017 bis 2022, weil ein Zeitfilter direkt auf timestamp_utc die Zeilen verschluckt.
Wenn der Graph die Transaktion noch nicht erklärt
Der Graph zeigt den Moment, in dem der Kreis geschlossen war. Wie die Transaktionen dorthin gekommen sind, welche Anweisungen die gehaltenen Sperren genommen haben und warum der Plan eine fremde Zeile angefasst hat, steht im Code und in den Plänen. Für eine gemeinsame Untersuchung helfen:
- der unveränderte Graph als
.xdloder XML, mit Zeitpunkt, Datenbank und Version, - die beteiligten Prozeduren oder Anweisungen,
- die tatsächlichen Ausführungspläne der wartenden Anweisungen,
- die Häufigkeit: einmalig, täglich zur selben Zeit oder ständig.
Mit diesen Angaben steigen wir in einer gemeinsamen Analyse direkt beim Kreis ein, statt erst Daten zu sammeln.
Quellen
- Microsoft: Deadlocks guide
- Microsoft: Use the system_health session
- Microsoft:
sys.fn_xe_file_target_read_file - Microsoft:
SET DEADLOCK_PRIORITY - Microsoft: Optimized locking
- Microsoft: Analyze and prevent deadlocks in Azure SQL Database
Weiterlesen: Blocking und Timeouts · Der Leser, der die Tabelle sperrt · SQL Server Indexoptimierung · SQL Server Performance Optimierung