TekinAli ⎇ main

NOLOCK'un ötesi: row versioning, RCSI ve SQL Server 2025 optimized locking

NOLOCK'un gerçekte nasıl davrandığı (dirty read, atlanan ve iki kez okunan satırlar, 601 hatası), RCSI ve SNAPSHOT isolation'ın onun yerini nasıl aldığı ve SQL Server 2025 optimized locking'in üstüne neler kattığı, sınırlarıyla birlikte.

WITH (NOLOCK) genellikle yavaş bir raporun yazan transaction'ları beklemesini ya da onları bloklamasını önlemek için eklenir. İlk bakışta işe yarar, sorgu artık beklemez. Ama bedeli göründüğünden yüksek. NOLOCK altında bir sorgu sonradan rollback edilecek veriyi döndürebilir, var olan satırları atlayabilir, bazılarını iki kez sayabilir ya da tarama sırasında veri yer değiştirdiğinde 601 hatasıyla düşebilir.

SQL Server'ın okuyan/yazan blocking'ine uzun süredir daha iyi bir cevabı var, SQL Server 2025 ise bunun üstüne yeni bir katman ekliyor. Bu yazıda NOLOCK'un gerçekte ne yaptığını, RCSI ve SNAPSHOT ile row versioning'in onun yerini nasıl aldığını ve optimized locking'in bu tabloda nereye oturduğunu anlatıyorum.

NOLOCK'un gerçek davranışı

NOLOCK, READUNCOMMITTED ile aynı şey. Sorgu shared lock almaz ve diğer transaction'ların tuttuğu exclusive lock'lar tarafından bloklanmaz.

Dirty read kısmı iyi bilinir. Başka bir transaction'ın değiştirdiği satırı okursun, o transaction rollback olur ve sen resmen hiç var olmamış bir değeri göstermiş olursun.

Daha az bilinen sorun yapısal. Microsoft'un table hint dokümantasyonu bunu açıkça yazıyor: dirty read'lere izin vermek kullanıcıların "see records twice (or not at all)" durumuna, yani kayıtları iki kez görmesine ya da hiç görmemesine yol açabilir. Sebep fiziksel. NOLOCK altında motor allocation-order scan seçebilir. Bu tarama sayfaları index sırasına göre değil, ayrıldıkları sıraya göre okur. Sen tararken eşzamanlı bir insert ya da update page split'e yol açarsa, yeni sayfaya taşınan satırlar taramanın gerisine düşebilir (ve kaçırılır) ya da önüne geçebilir (ve tekrar okunur). Sistemdeki her transaction sonunda commit olsa bile SUM(amount) sonucun yanlış olabilir.

Bir de doğrudan hata durumu var. Veri bir NOLOCK taramasının altında yer değiştirirse sorgu 601 hatası ile düşebilir: "Could not continue scan with NOLOCK due to data movement." Dokümantasyon bunu deadlock'u (1205 hatası) ele aldığın gibi ele almanı, yani statement'ı tekrar denemeni öneriyor. Beklememek için eklediğin hint, artık retry mantığı yazmanı gerektiriyor.

Hint'in yapmadığı iki şey daha var:

  • Bütün lock'lardan kaçınmaz. NOLOCK'lu sorgular da Sch-S (schema stability) lock alır. Bu yüzden online olmayan bir index rebuild gibi Sch-M lock tutan DDL işlemleri tarafından bloklanırlar.
  • Yazma işlemlerine etkisi yoktur. NOLOCK, INSERT, UPDATE ya da DELETE'in hedef tablosuna uygulanamaz. Optimizer UPDATE/DELETE'in hedef tablosundaki hint'i yok sayar ve Microsoft bu kullanımın desteğinin kaldırılacağını duyurmuş durumda.

NOLOCK'un eski gerekçesi

NOLOCK, ana alternatifin lock tabanlı READ COMMITTED olduğu dönemden kalma. O dönemde uzun bir rapor gerçekten OLTP yazmalarını bloklar ya da onlar tarafından bloklanırdı. Row versioning SQL Server 2005'ten beri var. Bugün Azure SQL Database'de varsayılan davranış bu, SQL Server 2025 ise üstüne daha fazlasını inşa ediyor. NOLOCK'u haklı çıkaran ödünleşim artık geçerli değil.

RCSI ile row versioning

ALTER DATABASE Sales SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;

SELECT name, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = N'Sales';

RCSI açıkken READ COMMITTED altında çalışan her statement, her satırın statement başladığı andaki son commit edilmiş versiyonunu okur. Okuyanlar shared lock almaz, dolayısıyla yazanları bloklamaz. Yazanlar da okuyanları bloklamaz. NOLOCK'un aksine commit edilmemiş veri görmezsin. Tarama sırasında yer değiştiren veriden kaynaklanan kayıp ya da çift satırlar da olmaz.

Bu geçiş veritabanına exclusive erişim ister, bu yüzden bir bakım penceresi planla. WITH ROLLBACK IMMEDIATE diğer oturumları sonlandırır.

RCSI'nin bedeli

  • Version store. Güncellenen ve silinen satırların önceki versiyonları, onlara ihtiyaç duyan aktif bir statement kalmayana kadar saklanır. Accelerated database recovery (ADR) kapalıysa bu versiyonlar tempdb'de tutulur. ADR açıksa (SQL Server 2019 ve sonrası) kullanıcı veritabanının içindeki persistent version store'a yazılır. Her iki durumda da uzun süren bir transaction temizliği durdurur, bunu izlemen gerekir.
  • Satır başına ek yük. Versiyonlanan satırlar versiyon bilgisi için fazladan byte taşır. Ayarı açtıktan hemen sonra bir miktar page split görebilirsin.
  • Uygulamanın varsayımları. Asıl önemli kısım bu. Bir değeri okuyup ona göre yazan ve okumanın eşzamanlı yazan bitene kadar bekleyeceğini varsayan kod, artık bir önceki commit edilmiş versiyonu okuyor. Tipik örnekler: bakiyeyi kontrol edip sonra düşmek, ya da "bir sonraki boş numarayı SELECT et, sonra INSERT et". Bu kalıplar açık locking ister (UPDLOCK ya da atomik bir UPDATE ... OUTPUT). READPAST kullanan kuyruk tabloları da READCOMMITTEDLOCK eklemezsen RCSI altında hata verir.

SNAPSHOT isolation başka bir şey

ALTER DATABASE Sales SET ALLOW_SNAPSHOT_ISOLATION ON;

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
    -- buradaki her statement aynı ana ait görüntüyü görür
COMMIT;

RCSI her statement'a tutarlı bir görüntü verir. SNAPSHOT ise transaction'ın tamamına tek bir tutarlı görüntü verir. Birden çok sorgudan oluşan ve sonuçlarının birbiriyle tutarlı olması gereken raporlarda işe yarar. Oturumların SET TRANSACTION ISOLATION LEVEL SNAPSHOT ile bunu açıkça istemesi gerekir. Snapshot başladıktan sonra başka bir transaction'ın değiştirdiği bir satırı güncellemeye çalışan snapshot transaction'ı update conflict (3960 hatası) ile düşer. Bu yüzden onu kullanan yazma kodunun retry mantığına ihtiyacı var. Benim varsayılanım şu: veritabanı geneli için RCSI, SNAPSHOT ise yalnızca gerçekten tek bir tutarlı görüntüye ihtiyaç duyan transaction'larda.

SQL Server 2025: optimized locking

RCSI okuyan/yazan arasındaki blocking'i çözer. Yazan/yazan blocking'i ve lock belleği ise optimized locking'in konusu. Aşağıdakilerin hepsi Microsoft Learn'deki "Optimized locking" makalesinden.

Hangi platformda var?

  • SQL Server 2025 (17.x): mevcut, varsayılan olarak kapalı, veritabanı bazında açılıyor.
  • Azure SQL Database, Microsoft Fabric'teki SQL database ve Azure SQL Managed Instance (Always-up-to-date ya da SQL Server 2025 update policy ile): her zaman açık.
  • SQL Server 2022 ve öncesi, ayrıca 2022 update policy ile çalışan Managed Instance: mevcut değil.
ALTER DATABASE Sales SET ACCELERATED_DATABASE_RECOVERY = ON;
ALTER DATABASE Sales SET OPTIMIZED_LOCKING = ON;

SELECT DATABASEPROPERTYEX(DB_NAME(), 'IsOptimizedLockingOn') AS is_optimized_locking_on;

ADR ön koşul. Önce onu açman gerekiyor ve optimized locking açıkken ADR'yi kapatamıyorsun.

Optimized locking iki parçadan oluşuyor:

  • Transaction ID (TID) locking. Değişiklik sırasında row ve page lock'lar hâlâ alınıyor, ama her biri satır değiştirilir değiştirilmez bırakılıyor. Commit'e kadar tutulan tek lock, transaction ID'si üzerindeki tek bir X lock. Lock belleği ciddi şekilde düşüyor ve lock escalation ihtimali çok azalıyor.
  • Lock after qualification (LAQ). Predicate'ler, U lock alınmadan satırın son commit edilmiş versiyonu üzerinde değerlendiriliyor ve lock yalnızca koşulu sağlayan satırlara alınıyor. Farklı satırlara dokunan iki update artık birbirini bloklamıyor. LAQ yalnızca RCSI açıkken çalışıyor. Önce RCSI'yi açman için bir sebep daha.

Sınırlamalar ve davranış değişiklikleri

  • UPDLOCK, HOLDLOCK, XLOCK ve READCOMMITTEDLOCK gibi locking hint'lere uyuluyor, ama kazancı azaltıyorlar. Bu hint'leri taşıyan statement'larda LAQ kullanılmıyor.
  • LAQ şu durumlarda da kullanılmıyor: MERGE statement'ları, READ COMMITTED dışındaki isolation level'lar, columnstore index'i olan tablolar, değişken ataması içeren DML ve sonuç kümesi döndüren ya da table variable'a yazan OUTPUT clause'ları.
  • Dokümantasyon bir davranış değişikliğini açıkça anlatıyor. RCSI ile birlikte LAQ açıkken, UPDATE ... WHERE b = 2 son commit edilmiş değere bakabilir ve eşzamanlı, henüz commit edilmemiş bir transaction'ın 2 yapmak üzere olduğu satırı atlayabilir. LAQ olmasaydı update bekler ve sonra o satırı değiştirirdi. İş yükün eşzamanlı transaction'lar arasında katı bir sıralamaya dayanıyorsa, Microsoft o transaction'lar için REPEATABLE READ ya da SERIALIZABLE öneriyor.
  • tempdb ve temporary tablolar için, ayrıca read-only replica'larda kullanılmıyor.

Tuzaklar

  • RCSI açmadan NOLOCK'u kaldırmak. Blocking geri gelir. Önce RCSI'yi aç ve test et, sonra hint'leri kaldır.
  • Uzun transaction'ları gözden kaçırmak. Açık unutulmuş tek bir transaction, version store'un saatlerce büyümesine yol açabilir.
  • View'ların içine gizlenmiş hint'ler. Bir view'ın içine yazılmış NOLOCK, o view'ı kullanan her sorguya uygulanır. Bir view'a verilen lock hint de altındaki tablolara yayılır. Yalnızca uygulama kodunda değil, veritabanı nesnelerinde de ara.
  • Optimized locking'in açık olduğunu varsaymak. SQL Server 2025'te sen açana kadar kapalı. RCSI olmadan da LAQ devreye girmiyor.

Özet ve kontrol listesi

NOLOCK beklememek için doğruluktan vazgeçer. Row versioning ise bu ödünleşimi gereksiz kılar. Asıl derdin okuyanların yazanları bloklamasıysa RCSI'yi aç ve hint'leri kaldır. Tek bir tutarlı görüntüye ihtiyaç duyan transaction'larda SNAPSHOT kullan. SQL Server 2025'te ise yazanlar arasındaki blocking'i de azaltmak için optimized locking'i ekle.

  1. Kodda, view'larda ve procedure'larda geçen bütün NOLOCK ve READUNCOMMITTED'ları bul.
  2. Önce okuyup sonra yazan kalıpları ve READPAST kullanan kuyrukları RCSI uyumluluğu açısından gözden geçir.
  3. Bir bakım penceresinde READ_COMMITTED_SNAPSHOT'ı aç ve version store'u izle.
  4. Hint'leri kaldır ve blocking'i öncesi ve sonrası için ölç.
  5. Birden çok statement boyunca tek bir tutarlı görüntüye ihtiyaç duyan transaction'larda SNAPSHOT kullan.
  6. SQL Server 2025'te önce ADR'yi, ardından OPTIMIZED_LOCKING'i aç. Sık çalışan kod yollarını gereksiz locking hint'lerden arındır.
tekinali.db · salt okunur · ● bağlı

SQLite 3.53.3 · 8 tablo, 2 view.

Soldaki bir tabloya tıkla, aşağıdan bir sorgu seç ya da kendin yaz. Yardım: \?

↑↓ seç · Enter aç · Esc kapat