Optimized Locking einschalten? Erst messen, dann entscheiden
Acht Sitzungen ändern dieselbe Tabelle, jede ihre eigene Gruppe von Zeilen. Ohne passenden Index blockieren sie sich trotzdem gegenseitig. Mit Optimized Locking verschwindet das Warten, der Durchsatz steigt in unserem Lab von 69 auf 129 Änderungen je Sekunde. Ein Erfolg. Dann geben wir derselben Tabelle den passenden Schlüssel, ganz ohne Optimized Locking: 386 Änderungen je Sekunde, bei einem Sechstel der CPU.
Optimized Locking löst einen bestimmten Konflikt sehr gut. Die Frage vor dem Einschalten lautet deshalb nicht „Ist es gut?“, sondern „Haben wir diesen Konflikt, und gibt es einen billigeren Weg?“. Diese Seite zeigt mit Messwerten von SQL Server 2025, wann sich der Schalter lohnt, was er kostet und wie Sie das an Ihrer eigenen Last prüfen.
In 60 Sekunden
- Optimized Locking braucht ADR, und seinen Hauptgewinn (Lock After Qualification) gibt es nur mit RCSI. Wer es einschaltet, schaltet also drei Dinge ein. Die Mechanik steht in Optimized Locking verständlich.
- Der Gewinn: Schreiber, die über fremde Zeilen laufen, blockieren sich nicht mehr. Im Lab beinahe doppelter Durchsatz in genau diesem Fall, dazu viel weniger gehaltene Sperren und kaum noch Lock Escalation.
- Die Kosten kommen von ADR: bei Änderungen vieler Zeilen ein Viertel bis zwei Fünftel weniger Durchsatz, das 2,3- bis 3,5-Fache an CPU und gut dreimal so viel Log. Bei Einzelzeilen-Änderungen war kein Unterschied messbar.
- Ein passender Index war besser: dreimal so viel Durchsatz wie Optimized Locking ohne Index, ganz ohne die ADR-Kosten.
- Heiße Zeilen: Bei vielen Schreibern auf derselben Zeile blieb der Durchsatz gleich, die langsamsten Schreiber wurden aber doppelt so langsam.
- Entscheiden Sie per Messung: Messfenster vorher, Schalter einzeln, Messfenster nachher. Die Abfrage dafür steht unten.
Was Sie kaufen und was Sie bezahlen
Wir haben drei Konfigurationen mit vier typischen Lasten verglichen, je 30 Sekunden, drei Wiederholungen, auf SQL Server 2025 CU10 mit acht CPUs. Gemessen wurden erfolgreiche Änderungen je Sekunde:
| Last | Schreiber | ohne ADR und RCSI | ADR + RCSI | ADR + RCSI + Optimized Locking |
|---|---|---|---|---|
| Mengen-UPDATE ohne passenden Index (1.000 Zeilen je Änderung) | 1 | 73,6 | 52,9 | 64,2 |
| 8 | 69,3 | 48,5 | 128,7 | |
| dieselbe Änderung mit passendem Schlüssel | 1 | 92,2 | 71,0 | 75,4 |
| 8 | 386,0 | 230,5 | 235,0 | |
| Einzelzeile per Primärschlüssel | 1 | 121,8 | 121,9 | 118,5 |
| 8 | 584,1 | 589,7 | 639,4 | |
| eine heiße Zeile, 4 Schreiber und 4 Leser | 4 | 129,3 | 135,2 | 134,2 |
Daraus lassen sich vier Dinge ablesen:
- Gewinn nur bei einem Muster. Optimized Locking holt seinen Vorteil dort, wo acht Schreiber ohne Index über alle Zeilen laufen und dabei aufeinander warten. Ohne Optimized Locking warteten sie zusammen rund 205 Sekunden je 30-Sekunden-Lauf auf
LCK_M_U, im Mittel also fast sieben Sitzungen gleichzeitig. Mit Optimized Locking wartete niemand mehr auf eine Sperre; der neue Engpass waren Seitenlatches und das Log. - Ein einzelner Schreiber zahlt nur. Ohne Konkurrenz gibt es nichts zu entsperren. Es bleiben die ADR-Kosten.
- ADR kostet bei Mengenänderungen. ADR + RCSI lag dort 23 bis 40 Prozent unter der Konfiguration ohne beides, brauchte je Änderung das 2,3- bis 3,5-Fache an CPU und schrieb gut dreimal so viel Log (bei acht Schreibern mit passendem Schlüssel 412 statt 129 KB je Änderung). Die Ursache: ADR schreibt für jede geänderte Zeile eine Version in die Datenbank, und die muss ins Log.
- Einzelzeilen-Änderungen merken davon nichts. Bei Änderungen über den Primärschlüssel lagen alle drei Konfigurationen innerhalb weniger Prozent, um eine Millisekunde CPU je Änderung, begrenzt vom Schreiben ins Log.
Dazu kommen die Gewinne, die keine Durchsatzzahl zeigt: Ein UPDATE über 1.000 Zeilen hielt in LAB-10 mit ADR und RCSI 1.060 Sperren, mit Optimized Locking 3. Weniger Sperren heißt weniger Lock-Speicher und kaum noch Lock Escalation. Microsoft nennt aus Telemetrie von rund 25.000 Azure-SQL-Datenbanken, dass bei Nonclustered-Indizes etwa 81 Prozent der Sperren übersprungen werden. Das sind Sperrzahlen, keine Zeitwerte; Durchsatz- oder Latenzzahlen veröffentlicht Microsoft nicht, und unabhängige Benchmarks haben wir nicht gefunden.
Profitiert Ihr Workload? Fünf Fragen
- Warten Schreiber auf Schreiber, die andere Zeilen ändern? In den Wartezeiten zeigt sich das als
LCK_M_U, oft zusammen mit Scans in den Plänen der wartendenUPDATE- undDELETE-Anweisungen. Das ist der Fall, den Optimized Locking löst. - Scannen diese Änderungen, weil ein Index fehlt? Dann prüfen Sie zuerst den Index (nächster Abschnitt).
- Steht im Code etwas, das den Gewinn verhindert? Sperrhinweise wie
UPDLOCK,HOLDLOCKoderREADCOMMITTEDLOCK, andere Isolationsstufen als READ COMMITTED,MERGE,OUTPUTan den Client, Columnstore-Indizes oder Pläne mit mehreren Zugriffen auf die Zieltabelle: Dort greift Lock After Qualification nicht. - Halten große Transaktionen viele Sperren, gibt es Lock Escalation? Dann hilft Optimized Locking auch ohne Wartezeiten, denn es reduziert Lock-Speicher und Eskalationen. Ausnahme: Direkt nach dem Laden oder einem Rebuild mit Füllfaktor 100 eskalierte im Lab auch mit Optimized Locking jeder erste große Lauf, weil sich die Seiten teilen (Details).
- Ist RCSI schon an oder fachlich vertretbar? Ohne RCSI bleibt vom Gewinn wenig. Was RCSI an Ihrem Code ändert, steht in NOLOCK, RCSI oder SNAPSHOT?.
Wer bei Frage 1 nichts findet und bei Frage 4 auch nicht, kauft mit Optimized Locking vor allem die ADR-Kosten.
Index oder Optimized Locking?
Im Lab war es dieselbe Tabelle, dieselben Daten und dieselbe Anweisung:
UPDATE dbo.Scan SET v = v + 1 WHERE g = @g; -- 8.000 Zeilen in acht Gruppen, jede Sitzung ändert ihre eigene GruppeEinmal mit dem Clustered Key nur auf id, sodass jede Änderung alle Zeilen lesen muss, einmal mit dem Clustered Key (g, id), sodass jede Änderung nur ihre eigene Gruppe liest.
Der passende Zugriffspfad beseitigt den Konflikt an der Wurzel: Keine Sitzung liest mehr die Zeilen der anderen, also wartet auch keine. Optimized Locking lässt die Sitzungen nur ohne Warten aneinander vorbeilesen; die Arbeit für das Lesen fremder Zeilen und die ADR-Kosten bleiben.
Daraus folgt die Reihenfolge: Erst den Zugriffspfad, dann den Schalter. Optimized Locking ist die richtige Wahl, wenn Sie den Code oder die Indizes nicht ändern dürfen, etwa bei Herstellersoftware, und der Konflikt „Schreiber über fremde Zeilen“ tatsächlich auftritt.
Die Grenze dieses Vergleichs: Wir haben den Clustered Key geändert. Ein zusätzlicher Nonclustered-Index auf g wurde vom Optimierer bei einem Achtel Selektivität nicht genutzt. Ein realer Zusatzindex kostet Platz, Pflege und manchmal Key Lookups; das haben wir nicht gemessen.
Heiße Zeilen: gleicher Durchsatz, längere Ausreißer
Vier Sitzungen erhöhen denselben Zähler, vier weitere lesen ihn:
Der Durchsatz der Schreiber bleibt gleich, ihre gesamte Wartezeit auch. Mit Optimized Locking ist sie aber ungleicher verteilt: Die langsamsten fünf Prozent der Schreiber brauchten doppelt so lange, das langsamste Prozent fast dreimal so lange. Für Anwendungen mit Antwortzeit-Zusagen auf solchen Zählern, Sequenztabellen oder Statuszeilen ist das ein Grund, vorher zu messen. Microsoft beschreibt das nicht; in den Quellen fanden wir dazu nur einen Erfahrungsbericht ohne Messwerte.
Unsere Vermutung zur Ursache, nicht geprüft: Mit Optimized Locking wartet ein Schreiber auf die Transaktion des anderen, nicht auf die Zeile. Nach deren Commit muss er die Zeile erneut anfordern, und die geordnete Warteschlange einer klassischen Zeilensperre gibt es dabei nicht.
Die Leser zeigen nebenbei, was RCSI leistet: 47-mal so viele Abfragen, mit und ohne Optimized Locking. Wer Leser entlasten will, braucht RCSI, nicht Optimized Locking.
Vorher messen: das Messrezept
Eine Umstellung beurteilen Sie am besten mit derselben Messung vorher und nachher, zur selben Lastzeit. Die folgende Abfrage misst ein Zeitfenster: Sie merkt sich die Zähler, wartet die eingestellte Zeit und zeigt dann, was in diesem Fenster passiert ist. Wartezeiten auf Sperren erscheinen einschließlich der neuen LCK_M_S_XACT_*-Typen, dazu Log und Versionsspeicher.
Abfrage: Messfenster vorher und nachher
/* PSG Stack · SQL Server Locking · Abfrage 4 Messfenster: Was hat sich in einem festen Zeitfenster getan? Wartezeiten auf Sperren (auch die neuen XACT-Wartetypen), Sperr- und Deadlock-Zähler, geschriebenes Log und Versionsspeicher. Einmal vor und einmal nach einer Umstellung zur gleichen Lastzeit ausführen und vergleichen. !!! WARNUNG — bitte vor dem Ausführen lesen !!! - Nur lesend. Die Abfrage ändert keine Daten und keine Einstellung. Sie legt nur kleine Tabellenvariablen in der eigenen Sitzung an. - Sie WARTET absichtlich: Die Sitzung bleibt @Sekunden lang offen (WAITFOR DELAY) und liefert erst danach Ergebnisse. Ein Abbruch ist jederzeit gefahrlos möglich. - Gemessen wird der ganze Server (Wartezeiten, Zähler), Log und Versionsspeicher aber nur für die Datenbank, in der Sie die Abfrage starten. Andere Last im selben Fenster zählt mit. - Rechte: VIEW SERVER STATE (ab SQL Server 2022 genügt VIEW SERVER PERFORMANCE STATE), dazu ein Benutzer in der Datenbank, die Sie messen. - MAXDOP 1: Jede Teilabfrage läuft seriell und belegt keine weiteren Kerne. - Ab SQL Server 2017. Was eine Version nicht kennt (Versionsspeicher-DMV vor 2019, Optimized Locking vor 2025), erscheint als n/a. Geprüft im PSG-Lab auf 2017, 2019, 2022 und 2025 (10.11.2026), unter Last auf 2025 CU10, auch ohne sysadmin. Ergebnisse: 1 Schalter der aktuellen Datenbank (RCSI, ADR, Optimized Locking) 2 Wartezeiten im Fenster, nur Sperren, Log und Seitenlatches, größte zuerst 3 Zähler im Fenster: Sperranforderungen, Sperrwartevorgänge, Timeouts, Deadlocks, Eskalationen 4 Log und Versionsspeicher der aktuellen Datenbank*/SET NOCOUNT ON; DECLARE @Sekunden int = 60; -- Länge des Messfensters in Sekunden (1 bis 3600) DECLARE @w0 TABLE (wait_type nvarchar(60) PRIMARY KEY, anzahl bigint, ms bigint, signal_ms bigint);DECLARE @z0 TABLE (zaehler nvarchar(128) PRIMARY KEY, wert bigint);DECLARE @w1 TABLE (wait_type nvarchar(60) PRIMARY KEY, anzahl bigint, ms bigint, signal_ms bigint);DECLARE @z1 TABLE (zaehler nvarchar(128) PRIMARY KEY, wert bigint);DECLARE @log0 bigint, @log1 bigint, @pvs0 bigint, @pvs1 bigint, @t0 datetime2(3), @t1 datetime2(3);DECLARE @hatPvs bit = CASE WHEN OBJECT_ID(N'sys.dm_tran_persistent_version_store_stats') IS NULL THEN 0 ELSE 1 END;DECLARE @pvsSql nvarchar(400) = N'SELECT @p = persistent_version_store_size_kb FROM sys.dm_tran_persistent_version_store_stats WHERE database_id = DB_ID() OPTION (MAXDOP 1);';DECLARE @warte char(8); IF @Sekunden NOT BETWEEN 1 AND 3600 BEGIN RAISERROR(N'@Sekunden muss zwischen 1 und 3600 liegen.', 16, 1); RETURN; END;SET @warte = CONVERT(char(8), DATEADD(second, @Sekunden, CONVERT(datetime, 0)), 108); /* Stand vorher */SET @t0 = SYSUTCDATETIME();INSERT @w0SELECT wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_msFROM sys.dm_os_wait_statsWHERE wait_type LIKE N'LCK[_]M[_]%' OR wait_type IN (N'WRITELOG', N'PAGELATCH_EX', N'PAGELATCH_SH')OPTION (MAXDOP 1);INSERT @z0SELECT RTRIM(counter_name), cntr_valueFROM sys.dm_os_performance_countersWHERE (object_name LIKE N'%:Locks%' AND instance_name = N'_Total' AND counter_name IN (N'Lock Requests/sec', N'Lock Waits/sec', N'Lock Timeouts (timeout > 0)/sec', N'Number of Deadlocks/sec')) OR (object_name LIKE N'%:Access Methods%' AND counter_name = N'Table Lock Escalations/sec')OPTION (MAXDOP 1);SELECT @log0 = SUM(v.num_of_bytes_written)FROM sys.dm_io_virtual_file_stats(DB_ID(), NULL) AS vJOIN sys.database_files AS f ON f.file_id = v.file_id AND f.type = 1OPTION (MAXDOP 1);IF @hatPvs = 1 EXEC sys.sp_executesql @pvsSql, N'@p bigint OUTPUT', @p = @pvs0 OUTPUT; WAITFOR DELAY @warte; /* Stand nachher */SET @t1 = SYSUTCDATETIME();INSERT @w1SELECT wait_type, waiting_tasks_count, wait_time_ms, signal_wait_time_msFROM sys.dm_os_wait_statsWHERE wait_type LIKE N'LCK[_]M[_]%' OR wait_type IN (N'WRITELOG', N'PAGELATCH_EX', N'PAGELATCH_SH')OPTION (MAXDOP 1);INSERT @z1SELECT RTRIM(counter_name), cntr_valueFROM sys.dm_os_performance_countersWHERE (object_name LIKE N'%:Locks%' AND instance_name = N'_Total' AND counter_name IN (N'Lock Requests/sec', N'Lock Waits/sec', N'Lock Timeouts (timeout > 0)/sec', N'Number of Deadlocks/sec')) OR (object_name LIKE N'%:Access Methods%' AND counter_name = N'Table Lock Escalations/sec')OPTION (MAXDOP 1);SELECT @log1 = SUM(v.num_of_bytes_written)FROM sys.dm_io_virtual_file_stats(DB_ID(), NULL) AS vJOIN sys.database_files AS f ON f.file_id = v.file_id AND f.type = 1OPTION (MAXDOP 1);IF @hatPvs = 1 EXEC sys.sp_executesql @pvsSql, N'@p bigint OUTPUT', @p = @pvs1 OUTPUT; /* 1 · Schalter der aktuellen Datenbank */DECLARE @schalter nvarchar(max) = N'SELECT d.name AS datenbank, d.is_read_committed_snapshot_on AS rcsi, d.snapshot_isolation_state_desc AS snapshot_isolation, ' + CASE WHEN COL_LENGTH(N'sys.databases', N'is_accelerated_database_recovery_on') IS NULL THEN N'N''n/a''' ELSE N'CONVERT(nvarchar(3), d.is_accelerated_database_recovery_on)' END + N' AS adr, ' + CASE WHEN COL_LENGTH(N'sys.databases', N'is_optimized_locking_on') IS NULL THEN N'N''n/a''' ELSE N'CONVERT(nvarchar(3), d.is_optimized_locking_on)' END + N' AS optimized_lockingFROM sys.databases AS dWHERE d.database_id = DB_ID()OPTION (MAXDOP 1);';EXEC sys.sp_executesql @schalter; /* 2 · Wartezeiten im Fenster */SELECT w1.wait_type, CASE WHEN w1.wait_type LIKE N'%XACT%' THEN N'Optimized Locking (XACT)' WHEN w1.wait_type LIKE N'LCK%' THEN N'Sperre' WHEN w1.wait_type = N'WRITELOG' THEN N'Log schreiben' ELSE N'Seitenlatch' END AS art, w1.anzahl - ISNULL(w0.anzahl, 0) AS anzahl, w1.ms - ISNULL(w0.ms, 0) AS wartezeit_ms, CONVERT(decimal(12, 2), (w1.ms - ISNULL(w0.ms, 0)) * 1.0 / NULLIF(w1.anzahl - ISNULL(w0.anzahl, 0), 0)) AS ms_je_warten, CONVERT(decimal(12, 2), (w1.ms - ISNULL(w0.ms, 0)) / 1000.0 / NULLIF(DATEDIFF(millisecond, @t0, @t1) / 1000.0, 0)) AS wartende_im_mittelFROM @w1 AS w1LEFT JOIN @w0 AS w0 ON w0.wait_type = w1.wait_typeWHERE w1.anzahl > ISNULL(w0.anzahl, 0)ORDER BY wartezeit_ms DESCOPTION (MAXDOP 1); /* 3 · Zähler im Fenster */SELECT z1.zaehler, z1.wert - ISNULL(z0.wert, 0) AS im_fenster, CONVERT(decimal(14, 2), (z1.wert - ISNULL(z0.wert, 0)) / NULLIF(DATEDIFF(millisecond, @t0, @t1) / 1000.0, 0)) AS je_sekundeFROM @z1 AS z1LEFT JOIN @z0 AS z0 ON z0.zaehler = z1.zaehlerORDER BY z1.zaehlerOPTION (MAXDOP 1); /* 4 · Log und Versionsspeicher der aktuellen Datenbank */SELECT DB_NAME() AS datenbank, DATEDIFF(millisecond, @t0, @t1) / 1000.0 AS fenster_s, CONVERT(decimal(14, 2), (@log1 - @log0) / 1048576.0) AS log_geschrieben_mb, CASE WHEN @hatPvs = 0 THEN N'n/a' ELSE CONVERT(nvarchar(20), @pvs0) END AS pvs_vorher_kb, CASE WHEN @hatPvs = 0 THEN N'n/a' ELSE CONVERT(nvarchar(20), @pvs1) END AS pvs_nachher_kbOPTION (MAXDOP 1);Beispielausgabe
Ein 20-Sekunden-Fenster aus dem Lab (SQL Server 2025 CU10, acht Schreiber ohne passenden Index, ADR, RCSI und Optimized Locking an, synthetische Daten, Spalten gekürzt):
1 · Schalter
datenbank rcsi snapshot_isolation adr optimized_locking
--------- ---- ------------------ --- -----------------
ol_full 1 OFF 1 1
2 · Wartezeiten im Fenster
wait_type art anzahl wartezeit_ms ms_je_warten wartende_im_mittel
------------ ------------- ------ ------------ ------------ ------------------
PAGELATCH_SH Seitenlatch 42762 32691 0.76 1.63
PAGELATCH_EX Seitenlatch 20838 18586 0.89 0.92
WRITELOG Log schreiben 2869 9595 3.34 0.48
3 · Zähler im Fenster
zaehler im_fenster je_sekunde
------------------------------- ---------- ----------
Lock Requests/sec 9459593 470696.77
Lock Timeouts (timeout > 0)/sec 0 0.00
Lock Waits/sec 0 0.00
Number of Deadlocks/sec 0 0.00
Table Lock Escalations/sec 0 0.00
4 · Log und Versionsspeicher
datenbank fenster_s log_geschrieben_mb pvs_vorher_kb pvs_nachher_kb
--------- --------- ------------------ ------------- --------------
ol_full 20.097000 1033.42 114056 572808So lesen Sie das:
- Ergebnis 2: Keine einzige Sperrwartezeit, dafür Seitenlatches und Log. In unseren Läufen derselben Last ohne Optimized Locking warteten an dieser Stelle im Mittel fast sieben Sitzungen auf
LCK_M_U. Der Engpass ist gewandert, das ist der Gewinn. Erscheinen nach dem Einschalten neueLCK_M_S_XACT_*-Zeilen, warten Sitzungen auf andere Transaktionen, etwa auf heißen Zeilen. - Ergebnis 3:
Lock WaitsundTable Lock Escalationssollten nach der Umstellung sinken. Die Zahl der Sperranforderungen sinkt nicht unbedingt, denn kurze Zeilensperren gibt es weiterhin. - Ergebnis 4: Das Log und der Versionsspeicher zeigen die ADR-Kosten. Steigt das Log je Fenster deutlich, prüfen Sie Platz, Sicherung und Replikation. Der Versionsspeicher darf schwanken, solange er wieder schrumpft; wächst er von Fenster zu Fenster, hält etwas die Bereinigung auf.
Ergänzend gehören in jede Vorher-nachher-Messung die Antwortzeiten der wichtigsten Vorgänge aus Sicht der Anwendung, auch die langsamsten fünf Prozent, und die CPU der Instanz. Das Extended Event locking_stats meldet etwa alle fünf Minuten je Datenbank Eskalationen und wie oft Lock After Qualification versucht wurde; es zeigt also auch, ob der Gewinn überhaupt greift.
Der PoC in fünf Schritten
- Testsystem mit echter Last. Ein Replay oder ein Lasttest mit den Vorgängen, die heute warten. Auf einem leeren System sehen Sie nur die Kosten. SQL Server 2025 mindestens mit CU9: Microsoft hat Abstürze von Optimized Locking im Zusammenspiel mit Change Tracking (CU3) und mit weitergeleiteten Zeilen in Heaps (CU9) behoben.
- Messfenster ohne alles. Abfrage 4 zu den Lastzeiten, dazu Antwortzeiten und CPU.
- Schalter einzeln. Erst ADR, messen. Dann RCSI, messen. Dann Optimized Locking, messen. Nur so wissen Sie, welcher Schritt was kostet und was bringt. In unserem Lab kostete ADR + RCSI bis zu 40 Prozent, und erst Optimized Locking holte es im passenden Fall mehr als zurück.
- Ergebnisse prüfen, nicht nur Zeiten. RCSI ändert, was Trigger, Prüflogik und Lesen-dann-Schreiben-Code sehen; Optimized Locking ändert in einem Grenzfall die Reihenfolge von Änderungen. Beides steht in NOLOCK, RCSI oder SNAPSHOT? und Optimized Locking verständlich. Testen Sie diese Stellen mit zwei Sitzungen.
- Monitoring und Deadlock-Auswertung prüfen. Erkennt Ihr Monitoring die Wartetypen
LCK_M_S_XACT_READ,LCK_M_S_XACT_MODIFYundLCK_M_S_XACT? Liest Ihre Deadlock-Auswertung das Element<xactlock>? Wenn nicht, sieht Blocking nach dem Einschalten so aus, als wäre es verschwunden.
Woran Sie eine Regression erkennen
| Anzeichen | Wahrscheinliche Ursache | Was Sie tun |
|---|---|---|
| Durchsatz sinkt, CPU und Log steigen bei Mengenänderungen | ADR-Kosten ohne passenden Konflikt | Messung aus Schritt 3 ansehen; ohne Gewinn zurücknehmen |
| Versionsspeicher wächst von Fenster zu Fenster | Lange Transaktion, lange Abfrage oder vergessene offene Transaktion hält die Bereinigung auf | Halter in sys.dm_tran_persistent_version_store_stats (oldest_active_transaction_id) suchen; Version Store und Transaktionslog |
| Die langsamsten Schreiber werden langsamer, Durchsatz gleich | Heiße Zeilen, neue LCK_M_S_XACT_MODIFY-Wartezeiten | Hotspot entschärfen (Verteilung, Sequenzen) oder Optimized Locking für diese Datenbank zurücknehmen |
| Lock Escalation trotz Optimized Locking | Erste Änderung frisch geladener Zeilen auf vollen Seiten | Nach großen Ladevorgängen erwartbar; Füllfaktor prüfen (Details) |
lock_after_qual_stmt_abort häufig, Laufzeit einzelner Änderungen schwankt | Lock After Qualification startet Anweisungen neu | Die Heuristik schaltet LAQ bei zu viel verschwendeter Arbeit selbst ab, mit Query Store je Plan, sonst für die ganze Datenbank; Query Store eingeschaltet lassen |
| Blocking-Dashboard bleibt leer, Anwender klagen | Monitoring kennt die XACT-Wartetypen nicht | Filter um LCK_M_S_XACT* erweitern |
Umschalten in der Produktion
Alle drei Schalter brauchen einen Moment, in dem keine andere Verbindung in der Datenbank aktiv ist:
ALTER DATABASE [MeineDB] SET ACCELERATED_DATABASE_RECOVERY = ON WITH ROLLBACK IMMEDIATE;ALTER DATABASE [MeineDB] SET READ_COMMITTED_SNAPSHOT = ON WITH ROLLBACK IMMEDIATE;ALTER DATABASE [MeineDB] SET OPTIMIZED_LOCKING = ON WITH ROLLBACK IMMEDIATE;Was wir im Lab auf SQL Server 2017 bis 2025 gesehen haben:
- Ohne Zusatz wartet die Anweisung, und zwar auf jede andere Verbindung in der Datenbank, auch eine untätige ohne Transaktion. Hatte die andere Sitzung eine Transaktion, wartete
ALTER DATABASEüber deren Commit hinaus, bis die Verbindung getrennt war. WITH NO_WAIThalf nicht. Laut Dokumentation soll die Anweisung damit sofort scheitern. Im Lab wartete sie auf allen Versionen und bei allen drei Schaltern genauso lange wie ohne Zusatz, ohne Meldung. Die Ursache haben wir nicht geklärt.WITH ROLLBACK IMMEDIATEtrennt die anderen Verbindungen und rollt offene Transaktionen zurück. Im Lab war der Schalter nach rund drei Sekunden umgelegt.
Planen Sie also ein Wartungsfenster, in dem Abbrüche akzeptabel sind. Zurück geht es in umgekehrter Reihenfolge mit OFF: erst Optimized Locking, dann RCSI nach Bedarf, dann ADR. Andersherum lehnt SQL Server ab (Msg 12134, siehe Optimized Locking verständlich). Optimized Locking gibt es in SQL Server 2025 Enterprise und Standard, nicht in Express; in Azure SQL Database ist es immer an.
Was Ihre Werkzeuge noch nicht kennen
Optimized Locking verändert, was Diagnosewerkzeuge zu sehen bekommen: neue Wartetypen, Sperren auf XACT statt auf Zeilen, ein neues Element im Deadlock-Report. Manche Community-Werkzeuge kennen das schon, andere noch nicht, und der Stand ändert sich mit jeder Version. Prüfen Sie deshalb Ihr eigenes Monitoring, bevor Sie umstellen: Zeigt es eine Sitzung, die auf LCK_M_S_XACT_MODIFY wartet, als blockiert an? Löst es den Blocker auf? Wertet es einen Deadlock mit <xactlock> aus? Am einfachsten testen Sie das mit dem Lagebild aus der Übersicht und zwei Sitzungen auf einem Testsystem.
Methode und Grenzen
Die Workload-Messung lief am 10.11.2026 auf SQL Server 2025 CU10 (17.0.5035.5), Developer Edition im Linux-Container mit acht CPUs, 12 GB Arbeitsspeicher für SQL Server und MAXDOP 1, Daten und Log auf einer gemeinsamen SATA-SSD. 63 Läufe zu je 30 Sekunden, drei Wiederholungen je Punkt in zufälliger Reihenfolge, Mediane; alle Läufe ohne Fehler, Deadlocks oder Tabelleneskalationen, und nach jedem Lauf stimmten die Summen mit den erfolgreichen Änderungen überein. Acht Läufe überschnitten sich mit einem anderen Versuch auf demselben Host; allein wiederholt lagen sie 3 Prozent darunter bis 14 Prozent darüber, an den Aussagen änderte das nichts. Das Umschalten haben wir in einem eigenen Versuch auf SQL Server 2017 CU31, 2019 CU32, 2022 CU27 und 2025 CU10 gemessen. Grenzen:
- Synthetische Lasten, eine geschlossene Lastschleife ohne Ankunftsrate, ein Host. Das Log war in mehreren Fällen der Engpass und begrenzt alle Konfigurationen gleich; auf schnellerem Speicher können die Abstände anders ausfallen.
- Keine Langzeitmessung: Wie sich Versionsspeicher und Bereinigung über Stunden mit einer langen Lesetransaktion verhalten, haben wir nicht gemessen.
- Die Ursache der längeren Ausreißer auf heißen Zeilen ist eine Vermutung.
- Den Indexvergleich haben wir mit einem anderen Clustered Key gemacht; die Kosten eines realen Zusatzindex sind nicht gemessen.
- Azure SQL Database, Managed Instance und Windows haben wir nicht gemessen.
- Prozentwerte gelten für diese Lasten. Sie sind keine Prognose für Ihr System.
Weiterlesen
- Microsoft Learn: Optimized locking; Accelerated database recovery management und troubleshooting; ALTER DATABASE SET options; Deadlocks Guide.
- Dimitri Furman (Microsoft): Introducing optimized locking v2 (18.11.2025).
- Louis Tochon (dbi services): SQL Server 2025 – is optimized locking really working? Ask the Extended Events (2026).
- Bei PSG: Optimized Locking verständlich, ADR: schneller Rollback und was er kostet, NOLOCK, RCSI oder SNAPSHOT?, Upgrade auf SQL Server 2025.
Sie planen das Upgrade auf SQL Server 2025?
Dann stellt sich die Frage nach Optimized Locking ohnehin. In einer Performance-Analyse messen wir an Ihrer Last, ob der Konflikt auftritt, den Optimized Locking löst, ob ein Index ihn billiger beseitigt und was ADR bei Ihnen kostet. Den Gesamtblick auf die Instanz gibt der SQL Server Health Check.