Themenseite · SQL Server Locking

Sperren in SQL Server verstehen: von Lock Escalation bis Optimized Locking

SQL Server sperrt, damit zwei Transaktionen sich nicht gegenseitig die Daten unter den Händen ändern. Das ist kein Fehler, sondern der Preis für korrekte Ergebnisse. Ärgerlich wird es erst, wenn Sperren länger gehalten werden als nötig, zu grob werden oder Leser aufhalten, die gar nichts ändern wollen. Genau an diesen drei Stellen hat Microsoft seit SQL Server 2019 gedreht: mit der beschleunigten Wiederherstellung (ADR), mit Optimized Locking in SQL Server 2025 und mit Zeilenversionen, die es seit 2005 gibt und die viele immer noch nicht einschalten.

Stand 08.11.2026 · ca. 7 Minuten

Vertiefungen 3SQL-Abfrage 1Linie SQL Server 2017–2025
KlassischSperre je Zeile und SeiteOptimized Lockingeine Sperre je TransaktionOBJECT · IXOBJECT · IXPAGE · IXPAGE · IXKEY · X je geänderte ZeileXACT · X (TID 4711)Zeilen tragen die TID 4711Schema, keine Messung

Diese Seite erklärt die Mechanik und die Schalter. Sie zeigt mit einer lesenden Abfrage, welche Schalter in Ihren Datenbanken stehen und wer gerade welche Sperren hält, und führt zu den Vertiefungen. Für den akuten Fall, „gerade hängt etwas, wer blockiert?“, bleibt der Ratgeber Blocking und Timeouts der Einstieg; diese Seite wiederholt seine Headblocker-Abfrage nicht.

Warum SQL Server sperrt

Eine Sperre sagt: Diese Ressource gehört gerade mir, und zwar in diesem Modus. Die Ressource kann eine Zeile (KEY oder RID), eine Seite (PAGE), eine ganze Tabelle (OBJECT) oder seit SQL Server 2025 eine Transaktion (XACT) sein. Der Modus sagt, was andere noch dürfen: Mit einer geteilten Sperre (S) dürfen andere lesen, mit einer exklusiven (X) nicht einmal das; Absichtssperren (IS, IX) auf Seite und Tabelle kündigen an, dass darunter etwas gesperrt ist, damit niemand die ganze Tabelle sperren kann, während eine Zeile in Arbeit ist.

Drei Regeln bestimmen, wie weh das tut:

  1. Schreibsperren bleiben bis zum Commit. Eine Transaktion, die 10.000 Zeilen ändert und dann wartet, hält 10.000 Zeilensperren, bis sie fertig ist. Je länger die Transaktion, desto länger warten andere.
  2. Ab etwa 5.000 Sperren eines Statements auf einer Tabelle eskaliert SQL Server auf eine Tabellensperre. Das spart Speicher, sperrt aber alles, auch Zeilen, die niemand angefasst hat. Wie ein harmloses SELECT so die ganze Tabelle sperren kann, zeigt Der Leser, der die Tabelle sperrt.
  3. Leser warten auf Schreiber, solange die Datenbank unter dem klassischen READ COMMITTED läuft. Mit Read Committed Snapshot (RCSI) lesen sie stattdessen die zuletzt bestätigte Version der Zeile und warten nicht. Im Lab brach ein Leser unter READ COMMITTED nach 503,53 ms am Lock-Timeout ab; unter RCSI lieferte derselbe Leser in 1,64 ms den zuletzt bestätigten Wert. RCSI ändert, was ein Leser sieht, und braucht Platz für Versionen; es ist eine Entscheidung, keine Standardempfehlung.

Schreiber gegen Schreiber löst RCSI nicht. Dafür gibt es seit SQL Server 2025 Optimized Locking.

Die Linie: 2017, 2019, 2022, 2025

SchalterSQL Server2017SQL Server2019SQL Server2022SQL Server2025RCSIRead Committed SnapshotADRAccelerated Database RecoveryOptimized LockingTID-Sperren und LAQnur mit ADRADR für TempDBeigener Schalter der Instanzlaut Dokuim PSG-Lab geprüftlaut Microsoft, nicht selbst gemessennicht vorhandenOptimized Locking lässt sich auf 2025 nur bei eingeschaltetem ADR aktivieren (Msg 12133). ADR auf 2017und Optimized Locking auf 2017 bis 2022 meldeten im Lab einen Syntaxfehler (Msg 102).
SQL Server2017201920222025RCSIADROptimized Lockingnur mit ADRADR für TempDBlaut Dokuim PSG-Lab geprüftlaut Microsoft, nicht selbst gemessennicht vorhandenOptimized Locking lässt sich auf 2025 nur beieingeschaltetem ADR aktivieren (Msg 12133). ADRauf 2017 und Optimized Locking auf 2017 bis 2022meldeten im Lab einen Syntaxfehler (Msg 102).
Welche Schalter es ab welcher Version gibt. Geprüft im PSG-Lab auf SQL Server 2017 CU31, 2019 CU32, 2022 CU27, 2025 CU9; ADR für TempDB laut Microsoft-Dokumentation.
VersionWas dazukamWas es für Sperren bedeutet
2017das klassische Bild: Sperre je Zeile, Eskalation ab etwa 5.000, Rollback dauert so lange wie die ArbeitReferenzpunkt
2019Accelerated Database Recovery (ADR): Zeilenversionen liegen im Persistent Version Store in der Datenbank, Rollback ist logisch und sofort fertig, Recovery nach Neustart hängt nicht mehr an der längsten Transaktionan den Sperren ändert sich nichts; ADR ist die Voraussetzung für alles Weitere
2022ADR räumt mit mehreren Threads auf; kein Optimized Lockingwie 2019
2025Optimized Locking (nur mit ADR): die Transaktion hält eine Sperre auf ihre eigene ID statt tausender Zeilensperren; mit RCSI prüft ein UPDATE seine Bedingung ohne Sperre gegen die letzte bestätigte Version (Lock After Qualification)zwei Sperren statt tausender, keine Eskalation im Lab, Scan-Schreiber auf verschiedenen Zeilen blockieren sich nicht mehr; dafür ändert sich in Wettlaufsituationen das Ergebnis

Drei Schalter, die zusammenspielen und einzeln geschaltet werden: RCSI (seit 2005), ADR (seit 2019) und Optimized Locking (seit 2025, setzt ADR voraus, nicht RCSI; Lock After Qualification wirkt aber nur mit RCSI). In Azure SQL Database ist Optimized Locking laut Microsoft standardmäßig an, in SQL Server 2025 aus. Deshalb zeigt dieselbe Demo in der Cloud etwas anderes als auf einem frisch installierten Server.

Die Vertiefungen

Welche Beobachtung passt zu Ihnen?

Ihre BeobachtungErster BlickVertiefung
Gerade hängt etwas, Timeouts in der AnwendungHeadblocker findenBlocking und Timeouts im Ratgeber
Ein SELECT sperrt die ganze TabelleLock Escalation durch PrefetchDer Leser, der die Tabelle sperrt
Sehr viele Sperren, hoher Lock-Speicher, Eskalationen bei großen ÄnderungenLagebild, Ergebnis 2: wer hält wie viele SperrenOptimized Locking verständlich
Ein Rollback dauert Minuten, die Recovery nach dem Neustart auchLagebild, Ergebnis 1: steht ADR?ADR: schneller Rollback und was er kostet
TempDB oder das Log wachsen, obwohl kaum etwas läufteine offene Transaktion hält Versionen festVersion Store und Transaktionslog
Fehler 1205, zwei Transaktionen warten über Kreuzden Graphen lesenDeadlockgraph lesen
Nach dem Upgrade auf 2025 fehlen die gewohnten Sperren in der DiagnoseRessourcentyp XACT, Wartetypen LCK_M_S_XACT_*Optimized Locking verständlich

Lagebild: Welche Schalter stehen, und wer hält gerade Sperren?

Die Abfrage liefert drei Ergebnisse: den Schalterstand jeder Datenbank (RCSI, Snapshot Isolation, ADR, Optimized Locking, Wiederherstellungsmodell, Kompatibilitätsgrad), die gehaltenen Sperren je Sitzung, verdichtet nach Objekt, Ressourcentyp und Modus, und die Anfragen, die gerade auf eine Sperre warten, mit ihrem Blocker. Auf SQL Server 2025 erscheinen dabei die neuen XACT-Ressourcen und LCK_M_S_XACT_*-Wartetypen. Ältere Versionen zeigen bei den fehlenden Schaltern n/a statt abzubrechen.

Abfrage: Lagebild Sperren und Schalter

Abfrage 1: Lagebild Sperren und Schalter · nur lesend · im PSG-Lab geprüft, Versionen im Kopf
/* PSG Stack · SQL Server Locking · Abfrage 1   Lagebild: Welche Schalter sind an, wer hält welche Sperren, wer wartet?    !!! WARNUNG — bitte vor dem Ausführen lesen !!!   - Nur lesend. Die Abfrage ändert keine Daten und keine Einstellung und beendet keine Sitzung.     Sie legt nur zwei kleine Tabellenvariablen an (in der TempDB der eigenen Sitzung).   - Auf Produktion mit Bedacht: Teil 2 liest sys.dm_tran_locks einmal vollständig. Bei sehr vielen     Sperren (Hunderttausende, z. B. während großer Änderungen ohne Lock Escalation) kostet das     selbst CPU und Speicher. Nicht in einer Schleife oder im Sekundentakt ausführen.   - Momentaufnahme: Sperren und Wartezustände ändern sich laufend. Ein leeres Ergebnis heißt     nur „in diesem Augenblick nichts“, nicht „nie“.   - Rechte: VIEW SERVER STATE (ab SQL Server 2022 genügt VIEW SERVER PERFORMANCE STATE).     VIEW DATABASE STATE allein genügt nicht. Ohne das Serverrecht liefert nur Teil 1 Zeilen:     Teil 2 bricht mit Fehler 300 ab, Teil 3 ebenso (2017–2022) oder bleibt ohne Meldung leer (2025).     Objektnamen erscheinen nur für Datenbanken, auf die das Login zugreifen darf und deren     Metadaten es sehen darf (z. B. Benutzer in der Datenbank plus VIEW DEFINITION oder     VIEW ANY DEFINITION); sonst steht dort object_id bzw. hobt_id.   - MAXDOP 1: Jede Teilabfrage läuft seriell und belegt keine weiteren Kerne. So bleibt die     Diagnose klein, auch wenn der Server gerade unter Last steht.   - Ab SQL Server 2017. Geprüft im PSG-Lab auf 2017, 2019, 2022 und 2025 (08.11.2026).     Spalten, die eine Version nicht kennt (ADR vor 2019, Optimized Locking vor 2025),     erscheinen als n/a; die Abfrage prüft dazu, ob die Spalte existiert.    Ergebnisse:     1  Schalter je Datenbank (RCSI, Snapshot Isolation, ADR, Optimized Locking, ...)     2a Sitzungen mit Sperren oder offener Transaktion     2b Gehaltene und angeforderte Sperren, verdichtet     3  Wartende Anfragen mit Blocker   Den Headblocker einer Blocking-Kette sucht diese Abfrage nicht; dafür gibt es die eigene   Abfrage auf der Ratgeber-Seite „Blocking und Timeouts“. Hinweis zu 2025: Warten mehrere   Sitzungen auf dieselbe Transaktion (XACT), kann blockiert_von auf eine andere wartende   Sitzung zeigen statt auf den Halter.*/SET NOCOUNT ON; /* 1 · Schalter je Datenbank   laq_moeglich: Lock after Qualification wirkt nur mit Optimized Locking UND RCSI.   Optimized Locking setzt ADR voraus; ohne RCSI bleibt nur das TID-Locking. */DECLARE @adr nvarchar(300) =    CASE WHEN COL_LENGTH(N'sys.databases', N'is_accelerated_database_recovery_on') IS NOT NULL         THEN N'CASE d.is_accelerated_database_recovery_on WHEN 1 THEN N''an'' ELSE N''aus'' END'         ELSE N'N''n/a''' END;DECLARE @ol nvarchar(300) =    CASE WHEN COL_LENGTH(N'sys.databases', N'is_optimized_locking_on') IS NOT NULL         THEN N'CASE d.is_optimized_locking_on WHEN 1 THEN N''an'' ELSE N''aus'' END'         ELSE N'N''n/a''' END;DECLARE @sql1 nvarchar(max) = N'SELECT d.name                                                            AS datenbank,       CASE d.is_read_committed_snapshot_on WHEN 1 THEN N''an'' ELSE N''aus'' END AS rcsi,       d.snapshot_isolation_state_desc                                   AS snapshot_isolation,       ' + @adr + N'                                                     AS adr,       ' + @ol + N'                                                      AS optimized_locking,       CASE WHEN ' + @ol + N' = N''n/a'' THEN N''n/a''            WHEN ' + @ol + N' = N''an'' AND d.is_read_committed_snapshot_on = 1 THEN N''ja''            ELSE N''nein'' END                                           AS laq_moeglich,       d.recovery_model_desc                                             AS wiederherstellungsmodell,       d.compatibility_level                                             AS kompatibilitaet,       d.log_reuse_wait_desc                                             AS log_wartet_aufFROM sys.databases AS dWHERE d.state = 0          -- nur ONLINE  AND d.database_id <> 3   -- modelORDER BY CASE WHEN d.database_id <= 4 THEN 1 ELSE 0 END, d.nameOPTION (MAXDOP 1);';EXEC sys.sp_executesql @sql1; /* 2 · Sperren: einmal lesen, dann verdichten   - Eigene Sitzung und die gemeinsame DATABASE-S-Sperre jeder Verbindung bleiben draußen.   - ressourcen = verschiedene gesperrte Ressourcen; eintraege = Zeilen in sys.dm_tran_locks.     Auf Servern mit 16 oder mehr logischen CPUs teilt SQL Server Tabellensperren auf     (Lock Partitioning): Eine Tabellensperre S oder X, etwa nach einer Lock Escalation,     steht dann einmal je Partition in der Sicht. Sie zählt hier als eine Ressource;     partitionen nennt die Zahl der Teile.   - XACT (ab SQL Server 2025 mit Optimized Locking): Sperre auf eine Transaktion. Der Schreiber     hält X auf seine eigene Transaktion (objekt = „(Transaktion)“). Wartende fordern S an     (Status WAIT); bei ihnen nennt objekt die Tabelle der Zeile, auf die sie warten. */DECLARE @sperren TABLE (    database_id    int           NOT NULL,    session_id     int           NOT NULL,    resource_type  nvarchar(60)  NOT NULL,    entity_id      bigint        NULL,    request_mode   nvarchar(60)  NOT NULL,    request_status nvarchar(60)  NOT NULL,    ressourcen     int           NOT NULL,    eintraege      int           NOT NULL,    partitionen    int           NOT NULL); INSERT INTO @sperrenSELECT l.resource_database_id,       l.request_session_id,       l.resource_type,       CASE WHEN l.resource_type IN (N'OBJECT', N'KEY', N'PAGE', N'RID', N'HOBT', N'XACT')            THEN NULLIF(l.resource_associated_entity_id, 0) END,       l.request_mode,       l.request_status,       COUNT(DISTINCT CONCAT(l.resource_associated_entity_id, N'|', l.resource_description,                             N'|', l.resource_subtype)),       COUNT(*),       COUNT(DISTINCT COALESCE(l.resource_lock_partition, 0))FROM sys.dm_tran_locks AS lWHERE l.request_session_id <> @@SPID  AND l.request_session_id > 0  AND NOT (l.resource_type = N'DATABASE' AND l.request_mode = N'S')GROUP BY l.resource_database_id, l.request_session_id, l.resource_type,         CASE WHEN l.resource_type IN (N'OBJECT', N'KEY', N'PAGE', N'RID', N'HOBT', N'XACT')              THEN NULLIF(l.resource_associated_entity_id, 0) END,         l.request_mode, l.request_statusOPTION (MAXDOP 1); /* KEY-, PAGE-, RID- und HOBT-Sperren nennen eine hobt_id. Den Tabellennamen kennt nur die   jeweilige Datenbank; deshalb je Datenbank ein kurzer Blick in sys.partitions. Datenbanken   ohne Zugriff werden übersprungen, dort bleibt die ID stehen. */DECLARE @namen TABLE (database_id int NOT NULL, hobt_id bigint NOT NULL,                      objekt nvarchar(300) NULL, index_name sysname NULL);DECLARE @db int, @liste nvarchar(max), @sql2 nvarchar(max);DECLARE dbs CURSOR LOCAL FAST_FORWARD FOR    SELECT database_id,           STRING_AGG(CAST(entity_id AS nvarchar(max)), N',')    FROM (SELECT DISTINCT database_id, entity_id FROM @sperren          WHERE resource_type IN (N'KEY', N'PAGE', N'RID', N'HOBT', N'XACT') AND entity_id IS NOT NULL) AS x    WHERE HAS_DBACCESS(DB_NAME(database_id)) = 1    GROUP BY database_id;OPEN dbs;FETCH NEXT FROM dbs INTO @db, @liste;WHILE @@FETCH_STATUS = 0BEGIN    SET @sql2 = N'SELECT ' + CAST(@db AS nvarchar(10)) + N', p.hobt_id,       QUOTENAME(OBJECT_SCHEMA_NAME(p.object_id, ' + CAST(@db AS nvarchar(10)) + N')) + N''.''         + QUOTENAME(OBJECT_NAME(p.object_id, ' + CAST(@db AS nvarchar(10)) + N')), i.nameFROM ' + QUOTENAME(DB_NAME(@db)) + N'.sys.partitions AS pLEFT JOIN ' + QUOTENAME(DB_NAME(@db)) + N'.sys.indexes AS i       ON i.object_id = p.object_id AND i.index_id = p.index_idWHERE p.hobt_id IN (' + @liste + N')OPTION (MAXDOP 1);';    BEGIN TRY        INSERT INTO @namen EXEC sys.sp_executesql @sql2;    END TRY    BEGIN CATCH        -- kein Zugriff auf diese Datenbank: IDs bleiben stehen    END CATCH;    FETCH NEXT FROM dbs INTO @db, @liste;END;CLOSE dbs;DEALLOCATE dbs; /* 2a · Sitzungen mit Sperren oder offener Transaktion   isolationsstufe: Einstellung der Sitzung. READ COMMITTED liest in einer Datenbank mit RCSI   trotzdem Zeilenversionen, ohne zu warten.   letzter_batch: zuletzt gesendeter Batch der Verbindung (gekürzt), bei schlafenden Sitzungen   oft der Befehl, der die Transaktion geöffnet hat. Kein Hostname, um die Ausgabe teilen zu können. */SELECT es.session_id,       es.login_name                                                    AS login,       es.program_name                                                  AS programm,       es.status,       es.open_transaction_count                                        AS offene_transaktionen,       CASE es.transaction_isolation_level            WHEN 1 THEN N'READ UNCOMMITTED' WHEN 2 THEN N'READ COMMITTED'            WHEN 3 THEN N'REPEATABLE READ'  WHEN 4 THEN N'SERIALIZABLE'            WHEN 5 THEN N'SNAPSHOT'         ELSE N'unbekannt' END        AS isolationsstufe,       tx.beginn                                                        AS transaktion_seit,       DATEDIFF(SECOND, tx.beginn, SYSDATETIME())                       AS transaktion_s,       COALESCE(s.ressourcen, 0)                                        AS sperren,       COALESCE(s.wartend, 0)                                           AS davon_wartend,       COALESCE(s.xact, 0)                                              AS xact_sperren,       r.wait_type                                                      AS wartet_auf,       NULLIF(r.blocking_session_id, 0)                                 AS blockiert_von,       LEFT(REPLACE(REPLACE(t.text, CHAR(13), N' '), CHAR(10), N' '), 200) AS letzter_batchFROM sys.dm_exec_sessions AS esLEFT JOIN (SELECT session_id,                  SUM(ressourcen)                                                   AS ressourcen,                  SUM(CASE WHEN request_status <> N'GRANT' THEN ressourcen ELSE 0 END) AS wartend,                  SUM(CASE WHEN resource_type = N'XACT' THEN ressourcen ELSE 0 END)    AS xact           FROM @sperren GROUP BY session_id) AS s       ON s.session_id = es.session_idLEFT JOIN sys.dm_exec_requests AS r       ON r.session_id = es.session_idLEFT JOIN sys.dm_exec_connections AS c       ON c.session_id = es.session_idOUTER APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) AS tOUTER APPLY (SELECT MIN(at.transaction_begin_time) AS beginn             FROM sys.dm_tran_session_transactions AS st             JOIN sys.dm_tran_active_transactions AS at               ON at.transaction_id = st.transaction_id             WHERE st.session_id = es.session_id) AS txWHERE es.session_id <> @@SPID  AND es.is_user_process = 1  AND (s.session_id IS NOT NULL OR es.open_transaction_count > 0)ORDER BY sperren DESC, es.session_idOPTION (MAXDOP 1); /* 2b · Sperren je Sitzung, Datenbank, Objekt, Ressource, Modus und Status   Ressourcentypen: OBJECT = Tabelle, PAGE = Seite, KEY = Indexzeile, RID = Heap-Zeile,   XACT = Transaktion (Optimized Locking). Status GRANT = gehalten, WAIT/CONVERT = angefordert. */SELECT TOP (500)       s.session_id,       DB_NAME(s.database_id)                                           AS datenbank,       CASE WHEN s.resource_type = N'OBJECT'                 THEN COALESCE(QUOTENAME(OBJECT_SCHEMA_NAME(s.entity_id, s.database_id)) + N'.'                               + QUOTENAME(OBJECT_NAME(s.entity_id, s.database_id)),                               N'object_id ' + CAST(s.entity_id AS nvarchar(20)))            WHEN s.resource_type IN (N'KEY', N'PAGE', N'RID', N'HOBT')                 THEN COALESCE(n.objekt, N'hobt_id ' + CAST(s.entity_id AS nvarchar(20)))            WHEN s.resource_type = N'XACT' THEN COALESCE(n.objekt, N'(Transaktion)')            ELSE N'' END                                                AS objekt,       n.index_name                                                     AS index_name,       s.resource_type                                                  AS ressource,       s.request_mode                                                   AS modus,       s.request_status                                                 AS status,       s.ressourcen,       s.eintraege,       s.partitionenFROM @sperren AS sLEFT JOIN @namen AS n       ON n.database_id = s.database_id      AND n.hobt_id = s.entity_id      AND s.resource_type IN (N'KEY', N'PAGE', N'RID', N'HOBT', N'XACT')ORDER BY s.session_id, datenbank, objekt, index_name,         CASE s.resource_type WHEN N'OBJECT' THEN 1 WHEN N'HOBT' THEN 2 WHEN N'PAGE' THEN 3                              WHEN N'KEY' THEN 4 WHEN N'RID' THEN 4 WHEN N'XACT' THEN 5 ELSE 6 END,         s.request_modeOPTION (MAXDOP 1); /* 3 · Wartende Anfragen mit Blocker   LCK_M_S_XACT_MODIFY / LCK_M_S_XACT_READ / LCK_M_S_XACT (ab 2025): Die Anfrage wartet auf das   Ende einer anderen Transaktion, nicht auf eine Zeilensperre. wait_resource nennt dann die   XACT-Ressource und dahinter die betroffene Zeile. Wer Wartezeiten nur nach LCK_M_S, LCK_M_U   und LCK_M_X filtert, übersieht diese Fälle. */SELECT r.session_id,       NULLIF(r.blocking_session_id, 0)                                 AS blockiert_von,       r.wait_type,       CASE WHEN r.wait_type LIKE N'LCK[_]M[_]%XACT%' THEN N'wartet auf Transaktionsende (XACT)'            WHEN r.wait_type LIKE N'LCK[_]M[_]%'      THEN N'wartet auf Sperre'            ELSE N'anderer Wartegrund' END                              AS art,       r.wait_time                                                      AS wartet_ms,       r.wait_resource,       DB_NAME(r.database_id)                                           AS datenbank,       CASE r.transaction_isolation_level            WHEN 1 THEN N'READ UNCOMMITTED' WHEN 2 THEN N'READ COMMITTED'            WHEN 3 THEN N'REPEATABLE READ'  WHEN 4 THEN N'SERIALIZABLE'            WHEN 5 THEN N'SNAPSHOT'         ELSE N'unbekannt' END        AS isolationsstufe,       LEFT(REPLACE(REPLACE(SUBSTRING(t.text, r.statement_start_offset / 2 + 1,            (CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(t.text)                  ELSE r.statement_end_offset END - r.statement_start_offset) / 2 + 1),            CHAR(13), N' '), CHAR(10), N' '), 200)                      AS anweisungFROM sys.dm_exec_requests AS rOUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS tWHERE r.session_id <> @@SPID  AND (r.blocking_session_id <> 0 OR r.wait_type LIKE N'LCK[_]M[_]%')ORDER BY r.wait_time DESCOPTION (MAXDOP 1);

Beispielausgabe

Beispielausgabe aus einem nachgestellten Lab-Lauf (SQL Server 2025 CU10, synthetische Daten, Namen frei gewählt, Spalten gekürzt). Drei Datenbanken mit drei Schalterständen: In Verkauf (klassisches READ COMMITTED) hat Sitzung 57 3.000 Zeilen geändert und eine kleine Tabelle komplett gesperrt, zwei Leser warten. In Lager (RCSI mit ADR) hat Sitzung 56 500 Zeilen geändert; ein Schreiber wartet, ein Leser nicht. In Kasse (ADR, RCSI, Optimized Locking) hat Sitzung 55 3.000 Zeilen geändert und hält dafür zwei Sperren; ein Schreiber und ein Leser mit READCOMMITTEDLOCK warten auf die Transaktion:

Beispielausgabe
1 · Schalter je Datenbank

datenbank rcsi snapshot_isolation adr optimized_locking laq_moeglich wiederherstellungsmodell kompatibilitaet log_wartet_auf
--------- ---- ------------------ --- ----------------- ------------ ------------------------ --------------- --------------
Kasse     an   OFF                an  an                ja           SIMPLE                   170             NOTHING
Lager     an   ON                 an  aus               nein         FULL                     170             NOTHING
Verkauf   aus  OFF                aus aus               nein         SIMPLE                   170             NOTHING
master    aus  ON                 aus aus               nein         SIMPLE                   170             CHECKPOINT
msdb      aus  ON                 aus aus               nein         SIMPLE                   170             NOTHING
tempdb    aus  OFF                aus aus               nein         SIMPLE                   170             NOTHING

(Auf SQL Server 2017 stehen adr, optimized_locking und laq_moeglich auf n/a, auf 2019 und 2022
 optimized_locking und laq_moeglich.)


2a · Sitzungen mit Sperren oder offener Transaktion (gekürzt)

session_id login       programm        status  offene_transaktionen isolationsstufe transaktion_s sperren davon_wartend xact_sperren wartet_auf          blockiert_von letzter_batch
---------- ----------- --------------- ------- -------------------- --------------- ------------- ------- ------------- ------------ ------------------- ------------- ------------------------------
57         app_buchung Buchungsdienst  running 1                    READ COMMITTED  7             3086    0             0            WAITFOR             NULL          BEGIN TRAN; UPDATE dbo.Konto ...
56         app_buchung Buchungsdienst  running 1                    READ COMMITTED  7             531     0             0            WAITFOR             NULL          BEGIN TRAN; UPDATE dbo.Konto ...
58         app_buchung Buchungsdienst  running 2                    READ COMMITTED  4             3       1             0            LCK_M_X             56            (@1 int,@2 tinyint)UPDATE [dbo].[Konto] ...
62         app_bericht Berichte        running 0                    READ COMMITTED  NULL          3       1             0            LCK_M_S             57            SELECT saldo FROM dbo.Konto WHERE id = 10
55         app_kasse   Kassendienst    running 1                    READ COMMITTED  7             2       0             1            WAITFOR             NULL          BEGIN TRAN; UPDATE dbo.Konto ...
60         app_kasse   Kassendienst    running 2                    READ COMMITTED  4             2       1             1            LCK_M_S_XACT_MODIFY 55            (@1 int,@2 tinyint)UPDATE [dbo].[Konto] ...
63         app_bericht Berichte        running 0                    READ COMMITTED  NULL          2       1             1            LCK_M_S_XACT_READ   60            SELECT ... WITH (READCOMMITTEDLOCK) ...
61         app_bericht Berichte        running 0                    READ COMMITTED  NULL          1       1             0            LCK_M_IS            57            SELECT COUNT(*) FROM dbo.Klein

(Im Lab stand bei Sitzung 55/57/56 status = running, weil sqlcmd die Transaktion mit WAITFOR
 offen hielt; eine Anwendung, die nach dem UPDATE nichts mehr sendet, erscheint als sleeping.)


2b · Sperren je Sitzung, Datenbank, Objekt, Ressource, Modus und Status

session_id datenbank objekt         index_name ressource modus status ressourcen eintraege partitionen
---------- --------- -------------- ---------- --------- ----- ------ ---------- --------- -----------
55         Kasse     (Transaktion)  NULL       XACT      X     GRANT  1          1         1
55         Kasse     [dbo].[Konto]  NULL       OBJECT    IX    GRANT  1          1         1
56         Lager     [dbo].[Konto]  NULL       OBJECT    IX    GRANT  1          1         1
56         Lager     [dbo].[Konto]  PK_Konto   PAGE      IX    GRANT  30         30        1
56         Lager     [dbo].[Konto]  PK_Konto   KEY       X     GRANT  500        500       1
57         Verkauf   [dbo].[Klein]  NULL       OBJECT    X     GRANT  1          1         1
57         Verkauf   [dbo].[Konto]  NULL       OBJECT    IX    GRANT  1          1         1
57         Verkauf   [dbo].[Konto]  PK_Konto   PAGE      IX    GRANT  84         84        1
57         Verkauf   [dbo].[Konto]  PK_Konto   KEY       X     GRANT  3000       3000      1
58         Lager     [dbo].[Konto]  PK_Konto   KEY       X     WAIT   1          1         1
60         Kasse     [dbo].[Konto]  PK_Konto   XACT      S     WAIT   1          1         1
61         Verkauf   [dbo].[Klein]  NULL       OBJECT    IS    WAIT   1          1         1
62         Verkauf   [dbo].[Konto]  PK_Konto   KEY       S     WAIT   1          1         1
63         Kasse     [dbo].[Konto]  PK_Konto   XACT      S     WAIT   1          1         1
(gekürzt: Absichtssperren IS/IX der Wartenden auf Tabelle und Seite)

Lesart: Ohne Optimized Locking hält Sitzung 57 bis zum Commit 3.000 Zeilensperren und 84
Seitensperren. Mit Optimized Locking hält Sitzung 55 für ebenso viele geänderte Zeilen nur die
Sperre auf ihre Transaktion (XACT X) und die Absichtssperre auf die Tabelle.

Auf einem Server mit 24 logischen CPUs (Lab: SQL Server 2017 und 2019) sah die Tabellensperre auf
dbo.Klein so aus (Lock Partitioning: eine Ressource, 24 Einträge):
57         Verkauf   [dbo].[Klein]  NULL       OBJECT    X     GRANT  1          24        24


3 · Wartende Anfragen mit Blocker

session_id blockiert_von wait_type           art                                 wartet_ms wait_resource                                              datenbank anweisung
---------- ------------- ------------------- ----------------------------------- --------- ---------------------------------------------------------- --------- ------------------------------------
58         56            LCK_M_X             wartet auf Sperre                   4436      KEY: 6:72057594047234048 (d08358b1108f)                    Lager     UPDATE [dbo].[Konto] set [saldo] = ...
61         57            LCK_M_IS            wartet auf Sperre                   4421      OBJECT: 5:1269579561:0                                     Verkauf   SELECT @x = COUNT(*) FROM dbo.Klein
62         57            LCK_M_S             wartet auf Sperre                   4171      KEY: 5:72057594047234048 (d08358b1108f)                    Verkauf   SELECT @x = saldo FROM dbo.Konto ...
60         55            LCK_M_S_XACT_MODIFY wartet auf Transaktionsende (XACT)  4170      XACT: 7:1107:0 KEY: 7:72057594047234048 (d08358b1108f)     Kasse     UPDATE [dbo].[Konto] set [saldo] = ...
63         60            LCK_M_S_XACT_READ   wartet auf Transaktionsende (XACT)  4166      XACT: 7:1107:0 KEY: 7:72057594047234048 (286fc18d83ea)     Kasse     SELECT @x = saldo FROM dbo.Konto WITH (READCOMMITTEDLOCK) ...

Lesart: Sitzung 63 wartet auf dieselbe Transaktion (XACT: 7:1107:0) wie Sitzung 60, aber auf eine
andere Zeile. blockiert_von zeigt dennoch 60, also eine selbst wartende Sitzung, nicht den
Halter 55. Das kam in allen fünf 2025-Läufen vor (je zwei auf CU9 und CU10, ein Pilot auf CU10): blockiert_von zeigte auf die
Sitzung, die zuerst auf die Transaktion zu warten begann.

So lesen Sie das Ergebnis: Ergebnis 1 sagt, welche Mechanik in der Datenbank überhaupt greifen kann; steht dort is_optimized_locking_on = 0, sind Zeilensperren und Eskalation das erwartete Bild. Ergebnis 2 zeigt, wer die Sperren hält: Eine Sitzung mit tausenden KEY-X-Sperren und offener Transaktion ist der Kandidat für Eskalation und für Blocking. Auf einer Instanz mit vielen CPUs erscheint eine eskalierte Tabellensperre wegen Lock Partitioning mehrfach; die Abfrage fasst das zusammen. Ergebnis 3 ist nur das Lagebild der Wartenden; die Blocking-Kette und den schlafenden Headblocker untersucht der Ratgeber.

Rechte für die Abfrage

Ergebnis 1 braucht kein besonderes Recht; sys.databases ist mit dem Standardrecht VIEW ANY DATABASE lesbar. Für die Ergebnisse 2 und 3 genügt VIEW SERVER STATE, ab SQL Server 2022 auch VIEW SERVER PERFORMANCE STATE; VIEW DATABASE STATE reicht nicht. Ohne das Serverrecht bricht Ergebnis 2 mit Fehler 300 ab („VIEW SERVER STATE permission was denied on object 'server', database 'master'.“); Ergebnis 3 bricht auf 2017 bis 2022 ebenfalls ab, bleibt auf SQL Server 2025 aber ohne Meldung leer. Das ist tückisch: Ein leeres drittes Ergebnis kann „niemand wartet“ heißen oder „kein Recht“. Objektnamen erscheinen nur, wenn das Login die Metadaten der Datenbank sehen darf, etwa als Benutzer mit VIEW DEFINITION oder mit VIEW ANY DEFINITION; sonst steht dort die object_id oder hobt_id. Geprüft im Lab mit sechs Testlogins auf SQL Server 2017, 2019, 2022 und 2025 CU9 und CU10.

Ein Hinweis zu SQL Server 2025: Warten mehrere Sitzungen auf dieselbe Transaktion, zeigt blockiert_von bei der später wartenden Sitzung auf die zuerst wartende, nicht auf den Halter. Das sieht nach einer Kette aus, ist aber keine. Der Halter ist die Sitzung mit XACT X in Ergebnis 2. Wir haben das in fünf Läufen auf zwei Builds beobachtet; eine Microsoft-Beschreibung dazu haben wir nicht gefunden.

Häufige Fragen

Löst RCSI mein Blocking? Nur das Blocking von Lesern durch Schreiber. Schreiber gegen Schreiber warten weiter, und eine offene Transaktion hält weiter ihre Sperren.

Brauche ich für Optimized Locking RCSI? Nein, ADR. Ohne RCSI hält die Transaktion trotzdem nur noch eine Sperre auf ihre ID. Aber erst mit RCSI greift Lock After Qualification, und erst dann blockieren sich Scan-Schreiber auf verschiedenen Zeilen nicht mehr.

Ist Lock Escalation mit SQL Server 2025 Geschichte? Nein. Die Schwellen gelten weiter; mit Optimized Locking werden sie nur selten erreicht. Unter REPEATABLE READ, SERIALIZABLE und mit Sperrhinweisen bleibt alles beim Alten.

Kostet ADR etwas? Ja: Platz für den Persistent Version Store in der Datenbank, mehr Logvolumen und in unserem Extremfall ein langsameres UPDATE. Was genau, steht in der Vertiefung.

Wenn Sperren Ihr Tagesgeschäft stören

Die drei Schalter sind schnell umgelegt. Ob sie Ihre Anwendung schneller machen oder in Wettlaufsituationen anders rechnen lassen, zeigt erst ein Test an Ihrer Last. Im Rahmen der Performance-Analyse messen wir, wo Sperren Zeit kosten, welche Abfragen von Optimized Locking profitieren würden und welche Sperrhinweise das verhindern. Wer die Mechanik mit dem eigenen Team durcharbeiten will, findet sie in der PTS-Schulung.