Mert Özen Her Satırda Daha İleri
Veritabanı SQL Server Derinlemesine — #19

Eski SQL Server'ları Okumak: 2016'dan 2025'e Motor Evrimi ve Compatibility Level Yükseltme Stratejisi

Mert Özen 8 Kas 2026 10 dk 6 görüntülenme
Eski SQL Server'ları Okumak: 2016'dan 2025'e Motor Evrimi ve Compatibility Level Yükseltme Stratejisi

Motor sürümü ile compatibility level ayrı şeylerdir: SQL Server 2025 üzerinde 130 seviyesinde çalışan bir veritabanında table variable hâlâ 1 satır, MSTVF 100 satır tahmin edilir, scalar UDF 8.916 kez çağrılır ve REGEXP_LIKE tanınmaz. 2016'dan 2025'e özellik tablosu, Microsoft Lifecycle tarihleri ve motoru eski seviyede yükselt, Query Store'la topla, seviyeyi artır, regresyonda planı zorla akışının gerçek bir denemesi.

Sürüm notu: Bu yazıdaki SQL (Structured Query Language) örnekleri SQL Server 2025 (17.0.5035.5, Cumulative Update 10; Microsoft'un güncelleme listesinde Ekim 2026 itibarıyla en son CU (Cumulative Update)) üzerinde çalıştırılmıştır; karşılaştırmalar için aynı veritabanının compatibility level'ı 130 ile 170 arasında değiştirilmiştir. Yaşam döngüsü tarihleri Microsoft Lifecycle sayfalarından alınmıştır.

Önce Tahmin Et: Aynı Sorgu, Beş Compatibility Level

Elinde SQL Server 2025 kurulu bir sunucu var. Üzerinde eski bir uygulamanın veritabanı çalışıyor ve compatibility level'ı hiç değiştirilmemiş. Aynı motorda, aynı veriyle, yalnız veritabanının compatibility level'ını değiştirerek beş şeyi ölçtük: bir table variable'ın satır tahmini, bir multi-statement TVF'nin (MSTVF, Table-Valued Function) satır tahmini, 2,5 milyon satırlık bir aggregate'in execution mode'u, bir scalar function'ın kaç kez çağrıldığı ve REGEXP_LIKE'ın çalışıp çalışmadığı:

DECLARE @CustomerOrders TABLE (OrderId int PRIMARY KEY);
INSERT @CustomerOrders (OrderId) SELECT OrderId FROM dbo.Orders WHERE CustomerId = 2;
SELECT COUNT(*) AS lines FROM @CustomerOrders AS co JOIN dbo.OrderItems AS i ON i.OrderId = co.OrderId;
SELECT COUNT(*) AS lines FROM dbo.CustomerOrdersMulti(2) AS co JOIN dbo.OrderItems AS i ON i.OrderId = co.OrderId;
SELECT BookId, SUM(Quantity) AS sold FROM dbo.OrderItems GROUP BY BookId ORDER BY sold DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY;
SELECT SUM(dbo.OrderTotal(OrderId)) AS revenue FROM dbo.Orders WHERE CustomerId = 3;
SELECT COUNT(*) AS regexp_matches FROM dbo.Customers WHERE REGEXP_LIKE(Email, '^customer4[0-9]@');

Sunucu 2025 olduğuna göre hepsi 2025 davranışı göstermeli, değil mi?

compat  table variable est  MSTVF est  aggregate mode  UDF calls  REGEXP_LIKE
130     1                   100        Row             8916       Msg 195
140     1                   17359      Row             8916       Msg 195
150     17359               17359      Batch           0          Msg 195
160     17359               17359      Batch           0          Msg 195
170     17359               17359      Batch           0          10
Msg 195, Level 15, State 10, Server b8be943222fa, Line 1
'REGEXP_LIKE' is not a recognized built-in function name.

Hayır. Motor 2025, ama sorgu işleme davranışı veritabanının compatibility level'ına bağlı. 130'da table variable 1 satır, MSTVF 100 satır tahmin ediliyor, aggregate row mode'da çalışıyor, scalar function 8.916 kez çağrılıyor ve yeni fonksiyon hiç tanınmıyor. Sonuçlar (satır sayıları, toplam tutar) her seviyede aynı; değişen, oraya giden yol. Bu yazı serinin son yazısı: 2016'dan 2025'e motorun nasıl değiştiğini, compatibility level'ın tam olarak neyi kontrol ettiğini ve bir yükseltmeyi Query Store ile güvenle yapmanın planını anlatıyor.

Motor Sürümü ve Compatibility Level

İki ayrı kavram var. Motor sürümü, kurulu SQL Server'ın kendisidir: 2025 = 17.x, 2022 = 16.x, 2019 = 15.x, 2017 = 14.x, 2016 = 13.x. Compatibility level ise veritabanı bazında bir ayardır ve o veritabanındaki sorguların hangi sürümün davranışıyla derleneceğini belirler: 170, 160, 150, 140, 130. Bir motor, kendi seviyesinin yanında birkaç eski seviyeyi de destekler; böylece 2016'dan gelen bir veritabanı 2025'te 130 seviyesinde çalışmaya devam edebilir.

Eski bir sunucuyu ya da yeni taşınmış bir veritabanını okurken ilk bakılacak yerler:

SELECT SERVERPROPERTY('ProductVersion') AS product_version,
       SERVERPROPERTY('ProductMajorVersion') AS major,
       SERVERPROPERTY('ProductUpdateLevel') AS update_level,
       SERVERPROPERTY('Edition') AS edition;
SELECT name, compatibility_level, is_query_store_on
FROM sys.databases WHERE name = DB_NAME();
SELECT name, value
FROM sys.database_scoped_configurations
WHERE name IN (N'LEGACY_CARDINALITY_ESTIMATION', N'QUERY_OPTIMIZER_HOTFIXES',
               N'TSQL_SCALAR_UDF_INLINING', N'BATCH_MODE_ON_ROWSTORE', N'DEFERRED_COMPILATION_TV',
               N'PARAMETER_SENSITIVE_PLAN_OPTIMIZATION', N'OPTIONAL_PARAMETER_OPTIMIZATION');
DBCC TRACESTATUS(-1) WITH NO_INFOMSGS;
product_version|major|update_level|edition
---------------|-----|------------|-------
17.0.5035.5|17|CU10|Enterprise Developer Edition (64-bit)
name|compatibility_level|is_query_store_on
----|-------------------|-----------------
sqls_19|170|1
name|value
----|-----
LEGACY_CARDINALITY_ESTIMATION|0
QUERY_OPTIMIZER_HOTFIXES|0
TSQL_SCALAR_UDF_INLINING|1
BATCH_MODE_ON_ROWSTORE|1
DEFERRED_COMPILATION_TV|1
PARAMETER_SENSITIVE_PLAN_OPTIMIZATION|1
OPTIONAL_PARAMETER_OPTIMIZATION|1

Database scoped configuration'lar compatibility level'ın üstünde ince ayar sağlar: compatibility level yeni özelliği açar, ayar o özelliği tek tek kapatabilir. LEGACY_CARDINALITY_ESTIMATION = 1, compatibility level yüksek olsa bile 2014 öncesi tahmin modelini kullandırır; eski bir sistemde bunu görürsen, birinin bir regresyonu bu şekilde "çözdüğünü" anlarsın. DBCC TRACESTATUS(-1) boş döndü: bu sunucuda global trace flag yok. Eski sunucularda 4199 (optimizer düzeltmeleri) ya da 9481 (eski tahmin modeli) gibi flag'ler sık görülür; bugün çoğunun yerini database scoped configuration aldı.

2016'dan 2025'e: Sürüm Sürüm Motor

Serinin önceki yazılarında tek tek ölçtüğümüz özellikleri sürümlerine göre dizince tablo şöyle:

Sürüm (compat)Sorgu işleme ve teşhisDiğer motor yenilikleri
2016 (130)Query Store, Live Query StatisticsTemporal table, Row-Level Security, Dynamic Data Masking, Always Encrypted, JSON (JavaScript Object Notation) fonksiyonları
2017 (140)Adaptive query processing: interleaved execution, batch mode adaptive join ve memory grant feedback; automatic tuning (FORCE_LAST_GOOD_PLAN)Linux desteği, resumable online index rebuild, graph tabloları
2019 (150)Scalar UDF (User-Defined Function) inlining, batch mode on rowstore, table variable deferred compilation, row mode memory grant feedbackADR (Accelerated Database Recovery), Unicode Transformation Format 8 (UTF-8) collation'lar, memory-optimized tempdb metadata
2022 (160)Parameter Sensitive Plan optimization, CE (Cardinality Estimation) ve DOP (Degree of Parallelism) feedback, Query Store hints, yeni veritabanlarında Query Store varsayılan açıkLedger, TDS (Tabular Data Stream) 8.0 strict encryption, GAM (Global Allocation Map) ve SGAM (Shared Global Allocation Map) latch iyileştirmeleri, GENERATE_SERIES
2025 (170)Optional parameter plan optimization, CE feedback for expressions, DOP feedback varsayılan açıkOptimized locking, REGEXP_* fonksiyonları, json veri tipi, tempdb space resource governance, PBKDF2 (Password-Based Key Derivation Function 2) parola hash'leri, time-bound Extended Events

Tabloyu okurken bir ayrım önemli. Soldaki sütundaki özelliklerin çoğu compatibility level'a bağlıdır: motoru yükseltmek yetmez, veritabanının seviyesini de yükseltmen gerekir; açılıştaki deney bunu gösteriyor. Sağdaki sütundakilerin çoğu ise motor özelliğidir ve compatibility level'dan bağımsız çalışır: ADR'yi, optimized locking'i ya da TDS 8.0'ı açmak için seviye değiştirmen gerekmez. İstisnalar var: REGEXP_LIKE gibi yeni T-SQL fonksiyonları ve GENERATE_SERIES belirli bir seviye ister; açılıştaki Msg 195 bunun sonucu.

Yaşam Döngüsü: Hangi Sürüm Ne Zamana Kadar?

Microsoft'un sabit yaşam döngüsü politikasında her sürüm beş yıl mainstream support (güvenlik dışı düzeltmeler de dahil) ve beş yıl extended support (yalnız güvenlik güncellemeleri) alır. Bugün itibarıyla tablo:

  • SQL Server 2016: extended support 14 Temmuz 2026'da bitti. Bundan sonra güvenlik güncellemesi yalnız ücretli ESU (Extended Security Updates) ile, en fazla üç yıl alınabilir.
  • SQL Server 2017: extended support 12 Ekim 2027'de bitiyor. Bir yıldan az kaldı.
  • SQL Server 2019: mainstream support 28 Şubat 2025'te bitti; extended support 8 Ocak 2030'a kadar.
  • SQL Server 2022: mainstream support 11 Ocak 2028'e, extended support 11 Ocak 2033'e kadar.
  • SQL Server 2025: 18 Kasım 2025'te çıktı; mainstream support 6 Ocak 2031'e, extended support 6 Ocak 2036'ya kadar.

2016 ya da 2017 üzerinde çalışan bir sistemin varsa, yükseltme artık bir tercih değil, bir takvim meselesi. İyi haber şu: hedef sürümün motoruna geçmek, compatibility level'ı hemen değiştirmeyi gerektirmez; risk iki ayrı adıma bölünebilir.

Yükseltme Oyun Planı

Microsoft'un Query Store ile önerdiği akış, motor değişikliğini ve sorgu davranışı değişikliğini birbirinden ayırır:

1. Motoru yükselt, compatibility level'ı eski değerinde bırak. Veritabanı 2025'te ama 130 ya da 140 seviyesinde çalışır; planlar büyük ölçüde aynı kalır. 2. Query Store'u aç ve iş yükünü bir süre topla; ay sonu gibi seyrek işleri kapsayacak kadar. 3. Compatibility level'ı yükselt. 4. Query Store'da regresyona uğrayan sorguları bul ve gerekirse eski planı zorla. 5. Kalıcı çözümü (index, sorgu, statistics) uygula ve zorlamayı kaldır.

Akışı laboratuvarda kısaltılmış hâliyle çalıştırdık. Raporlama sorgusunu önce 140'ta, sonra 170'te üçer kez çalıştırıp Query Store'a bakıyoruz:

ALTER DATABASE sqls_19 SET QUERY_STORE = ON (OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = ALL);
ALTER DATABASE sqls_19 SET COMPATIBILITY_LEVEL = 140;
/* rapor 3 kez */
ALTER DATABASE sqls_19 SET COMPATIBILITY_LEVEL = 170;
/* rapor 3 kez */
EXEC sys.sp_query_store_flush_db;

SELECT q.query_id, p.plan_id, p.compatibility_level, p.is_forced_plan,
       CASE WHEN CAST(p.query_plan AS nvarchar(max)) LIKE N'%EstimatedExecutionMode="Batch"%'
            THEN 'Batch' ELSE 'Row' END AS execution_mode,
       SUM(rs.count_executions) AS executions
FROM sys.query_store_query AS q
JOIN sys.query_store_query_text AS t ON t.query_text_id = q.query_text_id
JOIN sys.query_store_plan AS p ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id = p.plan_id
WHERE t.query_sql_text LIKE N'SELECT BookId, SUM(Quantity)%'
GROUP BY q.query_id, p.plan_id, p.compatibility_level, p.is_forced_plan, CAST(p.query_plan AS nvarchar(max))
ORDER BY p.plan_id;
query_id|plan_id|compatibility_level|is_forced_plan|execution_mode|executions
--------|-------|-------------------|--------------|--------------|----------
1|1|140|0|Row|3
1|2|170|0|Batch|3

Aynı sorgu, iki plan: 140'ta derlenen row mode, 170'te derlenen batch mode. Query Store her planın hangi compatibility level'da derlendiğini ve çalışma istatistiklerini ayrı ayrı tutuyor; Regressed Queries raporu tam olarak bu karşılaştırmayı yapar. Burada iki plan da 9.964 logical read ile OrderItems'ı tarıyor, farkı CPU (Central Processing Unit) tarafında. Diyelim ki yeni plan bir sorguda kötüleşti. Eski planı zorlamak tek satır:

EXEC sys.sp_query_store_force_plan @query_id = 1, @plan_id = 1;
/* rapor 3 kez, compatibility level 170'teyken */
query_id|plan_id|compatibility_level|is_forced_plan|execution_mode|executions
--------|-------|-------------------|--------------|--------------|----------
1|1|170|1|Row|6
1|2|170|0|Batch|3

plan_id|force_failure_count|last_force_failure_reason_desc
-------|-------------------|------------------------------
1|0|NONE
2|0|NONE

Veritabanı 170'te, ama sorgu zorlanan planın şekliyle, row mode'da çalıştı: plan 1'in çalışma sayısı 3'ten 6'ya çıktı ve artık 170 altında derlenmiş olarak görünüyor. force_failure_count 0; zorlama başarılı. Plan forcing'in bir şeyi unutma: zorlanan plan bir index'e dayanıyorsa ve o index silinirse zorlama başarısız olur ve last_force_failure_reason_desc nedenini söyler. Zorlama geçici bir sigortadır; kalıcı çözüm değil. Query Store'un ayrıntılarını ve automatic tuning'i serinin 8. yazısında (Query Store ve Intelligent Query Processing) görmüştük.

SQL Server 2019/2022'ye Yükseltiyorsan

Hedefin 2025 değil de 2019 ya da 2022 ise aynı plan geçerli, ama kazancın tablodaki satırlarla sınırlı. 2019'a geçip 150'ye yükseldiğinde açılıştaki beş ölçümden dördünü (table variable, MSTVF, batch mode, UDF inlining) alırsın; optimized locking, REGEXP_* ve OPPO (Optional Parameter Plan Optimization) yok. 2022 ve 160, Parameter Sensitive Plan optimization'ı ve feedback ailesini ekler; Query Store yeni veritabanlarında varsayılan açık gelir, ama taşınan eski veritabanlarında kapalı kalabilir, kontrol et. 2019'un mainstream support'u bittiği için yeni bir yükseltmenin hedefi olarak 2022 ya da 2025 daha mantıklı; 2022'nin extended support'u 2033'e, 2025'inki 2036'ya kadar.

Üretimde Dikkat

Motor ve compatibility level'ı aynı gün değiştirme. Aynı anda değiştirirsen, bir regresyonun motordan mı, yeni tahmin modelinden mi, yoksa yeni bir IQP (Intelligent Query Processing) özelliğinden mi geldiğini ayıramazsın. İki adım arasında, Query Store'un baseline toplaması için yeterli zaman bırak.

Uygulamanın kendi sürüm kontrollerini test et. Entity Framework Core (EF Core), SQL Server provider'ında hedeflenen sürüme göre SQL üretir; örneğin EF Core 8 ve sonrası, Contains çevirisinde OPENJSON kullanır ve bu compatibility level 130 ister. Çok eski bir seviyede çalışan veritabanında bu sorgular hata verir; EF Core'a UseCompatibilityLevel(...) ile seviyeyi bildirebilirsin. EF Core serisinin sürüm yazılarında bu değişiklikleri görmüştük.

Deprecated özellikleri tara. Yükseltmeden önce sys.dm_os_performance_counters'taki "Deprecated Features" sayaçları ya da Extended Events'in deprecation_announcement ve deprecation_final_support event'leri, hedef sürümde kaldırılan ya da kaldırılacak özelliklerin kullanılıp kullanılmadığını gösterir.

Edition'ı unutma. 2025'te Standard edition 32 core ve 256 GB belleğe çıktı ve Express edition'ın veritabanı sınırı 50 GB oldu (serinin 1. yazısı); eski bir sunucudan taşırken lisans planını da bu sınırlara göre gözden geçir.

Sık Düşülen Tuzaklar

Motoru yükseltip compatibility level'ı unutmak. Açılıştaki tablo bu tuzağın kendisi: 2025 lisansı için ödeme yapılmış, ama veritabanı 2016 davranışıyla çalışıyor. Yeni özelliklerin çoğu hiç devreye girmez.

Regresyonu trace flag ile "çözmek". 9481 ya da LEGACY_CARDINALITY_ESTIMATION, sorunlu birkaç sorgu için bütün veritabanını eski modele geri döndürür. Önce sorgu bazında araçları dene: Query Store'da plan forcing, Query Store hints ya da sorguya özel USE HINT.

Query Store'u yükseltmeden sonra açmak. Baseline'ı yükseltmeden önce toplamazsan, "yeni plan daha mı kötü?" sorusunu karşılaştıracak bir şeyin olmaz.

Restore ile taşınan veritabanının seviyesini kontrol etmemek. Bir backup yeni sürüme restore edildiğinde compatibility level, hedef sürüm destekliyorsa olduğu gibi kalır; desteklemiyorsa desteklenen en düşük seviyeye yükseltilir. Her iki durumda da sonucu sys.databases'tan doğrula.

Küçük Bir Deneme

★☆☆ Kendi bir veritabanını (ya da serinin laboratuvar veritabanını) sys.databases ve sys.database_scoped_configurations ile incele. Compatibility level kaç? Varsayılandan farklı bir ayar var mı?

★★☆ Açılıştaki beş ölçümü compatibility level 170'te ALTER DATABASE SCOPED CONFIGURATION SET BATCH_MODE_ON_ROWSTORE = OFF ile tekrarla. Hangi sütun değişiyor, hangisi değişmiyor?

★★★ Serinin 7. yazısındaki parameter sniffing demosunu 150, 160 ve 170'te çalıştır ve Query Store'da her seviyede kaç plan oluştuğunu karşılaştır. Parameter Sensitive Plan optimization'ın planları sys.query_store_query_variant'ta nasıl görünüyor?

SQL Server Derinlemesine serisini burada bitiriyoruz. Veriyi page'lerde nasıl tuttuğundan, index'lere, optimizer'ın tahminlerine, lock'lara, kodun veritabanındaki hâline ve bir şikâyeti ölçerek çözmeye kadar motorun içinden geçtik. Buradan sonraki doğal adım, tek bir sunucunun ötesine bakmak: ilerideki Veritabanı Performansı ve Scaling serisinde partitioning, read replica'lar, High Availability/Disaster Recovery (HA/DR) ve sharding'i ele alacağız.