Abdulaziz Akyol

SQL Server performans ayarı: indeks, istatistik ve sorgu planı

Microsoft SQL Server · Yazılım geliştirme
25 Eylül 2026 · 7 dk okuma · Abdulaziz Akyol

SQL Server performans ayarı (performance tuning), yavaş bir iş yükünün darboğazını ölçümle bulup en küçük ve en güvenli değişiklikle gidermektir: çoğu zaman bir indeks, güncel bir istatistik ya da yeniden yazılmış bir WHERE koşulu. Sıra hep aynıdır: sunucu neyi bekliyor, bu beklemeyi hangi sorgu üretiyor, o sorgunun planında ne yanlış?

Bu yazı, SQL Server 2014 Performance Tuning eğitiminde öğrendiklerimi ve Civil Mağazacılık’ta Nebim V3 ERP veri tabanlarını ve yaklaşık 4,5 TB’lık veri ambarını yönetirken edindiğim alışkanlıkları bir araya getiriyor. Yöntemin özü 2014’ten beri değişmedi; değişen, SQL Server’ın bazı sorunları artık kendiliğinden düzeltebilmesi. Güncel sürüm özelliklerini sonda ele alıyorum.

Performans ayarına nereden başlanır?

Sahada en sık gördüğüm hata, ölçmeden indeks eklemek ya da “sunucu yavaş” deyip donanımı büyütmektir. Önerdiğim döngü:

  1. Belirtiyi tanımlayın: hangi ekran, hangi rapor, hangi saat aralığı.
  2. Bekleme istatistikleriyle sunucunun genel darboğazını bulun.
  3. Query Store ile bu darboğazı üreten sorguları sıralayın.
  4. Gerçek yürütme planını okuyun ve tek bir değişiklik yapın.
  5. Aynı ölçümü tekrarlayın; iyileşme yoksa değişikliği geri alın.

Örneklerde aşağıdaki satış tablosunu kullanıyorum; betikleri bir deneme veri tabanında çalıştırabilirsiniz. Tablo tasarımının temelleri için SQL Server’da tablo oluşturma yazısına bakabilirsiniz.

CREATE TABLE dbo.SalesLine (
    SalesLineID bigint IDENTITY(1,1) NOT NULL
        CONSTRAINT PK_SalesLine PRIMARY KEY CLUSTERED,
    StoreCode   varchar(10)   NOT NULL,
    ItemCode    varchar(30)   NOT NULL,
    SaleDate    date          NOT NULL,
    Qty         int           NOT NULL,
    Amount      decimal(18,2) NOT NULL,
    IsReturn    bit           NOT NULL CONSTRAINT DF_SalesLine_IsReturn DEFAULT (0)
);
CREATE NONCLUSTERED INDEX IX_SalesLine_StoreCode ON dbo.SalesLine (StoreCode);

-- Deneme verisi: 1 milyon satır; satırların %60'ı tek mağazada (M0001), gerisi yüzlerce mağazaya dağılmış
INSERT INTO dbo.SalesLine (StoreCode, ItemCode, SaleDate, Qty, Amount, IsReturn)
SELECT TOP (1000000)
       CASE WHEN n % 10 < 6 THEN 'M0001' ELSE CONCAT('M', RIGHT(CONCAT('000', n % 2000), 4)) END,
       CONCAT('ITM', n % 5000),
       DATEADD(DAY, CAST(n % 730 AS int), CAST('20240101' AS date)),
       CAST(1 + n % 5 AS int),
       CAST(10 + n % 990 AS decimal(18,2)),
       CASE WHEN n % 50 = 0 THEN 1 ELSE 0 END
FROM (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
      FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b) AS x;

Adım 1: Sunucu neyi bekliyor?

SQL Server’da bir iş parçacığı çalışamadığı her an bir bekleme türüne yazılır: diskten sayfa okuma, kilit, CPU sırası, log yazma. sys.dm_os_wait_stats bu süreleri sunucu açıldığından ya da istatistikler sıfırlandığından beri birikimli olarak tutar. Bu yüzden tek bir anlık görüntü yerine, sonucu bir tabloya kaydedip iki ölçüm arasındaki farka bakmak daha anlamlıdır. wait_time_ms içinde signal_wait_time_ms de vardır: kaynak hazır olduktan sonra CPU’ya sıra gelmesini bekleme süresi. Sinyal beklemesinin payı yüksekse CPU baskısı düşünülür.

WITH w AS (
    SELECT wait_type,
           wait_time_ms / 1000.0                         AS wait_s,
           (wait_time_ms - signal_wait_time_ms) / 1000.0 AS resource_s,
           signal_wait_time_ms / 1000.0                  AS signal_s,
           waiting_tasks_count
    FROM sys.dm_os_wait_stats
    WHERE waiting_tasks_count > 0
      AND wait_type NOT IN (  -- zararsız arka plan beklemeleri (liste tam değildir)
          N'BROKER_EVENTHANDLER', N'BROKER_RECEIVE_WAITFOR', N'BROKER_TASK_STOP', N'BROKER_TO_FLUSH',
          N'CHECKPOINT_QUEUE', N'CLR_AUTO_EVENT', N'CLR_MANUAL_EVENT', N'DIRTY_PAGE_POLL',
          N'FT_IFTS_SCHEDULER_IDLE_WAIT', N'HADR_FILESTREAM_IOMGR_IOCOMPLETION', N'LAZYWRITER_SLEEP',
          N'LOGMGR_QUEUE', N'ONDEMAND_TASK_QUEUE', N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',
          N'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP', N'REQUEST_FOR_DEADLOCK_SEARCH',
          N'SLEEP_SYSTEMTASK', N'SLEEP_TASK', N'SOS_WORK_DISPATCHER', N'SP_SERVER_DIAGNOSTICS_SLEEP',
          N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', N'WAITFOR', N'XE_DISPATCHER_WAIT', N'XE_TIMER_EVENT')
)
SELECT TOP (10)
       wait_type,
       CAST(wait_s     AS decimal(18,1)) AS wait_s,
       CAST(resource_s AS decimal(18,1)) AS resource_s,
       CAST(signal_s   AS decimal(18,1)) AS signal_s,
       waiting_tasks_count,
       CAST(100.0 * wait_s / SUM(wait_s) OVER () AS decimal(5,1)) AS pct
FROM w
ORDER BY wait_s DESC;

En sık karşılaşılan türler ve ilk bakılacak yerler:

Bekleme türüGenellikle ne anlatırİlk bakılacak yer
PAGEIOLATCH_SH / _EXVeri sayfaları diskten okunuyorMantıksal okuması yüksek sorgular, tarama yapan planlar, bellek
LCK_M_*Kilit bekleme (blocking)Uzun işlemler, izolasyon düzeyi, aynı satırlara yazan işler
CXPACKET / CXCONSUMERParalel planlar; tek başına sorun değildirPahalı planlar, MAXDOP ve paralellik eşiği
SOS_SCHEDULER_YIELDCPU baskısıEn çok CPU tüketen sorgular
WRITELOGTransaction log yazma gecikmesiLog diski, satır satır commit eden işler
RESOURCE_SEMAPHOREBellek izni (memory grant) beklemeBüyük sıralama/hash işlemleri, yanlış satır tahminleri
ASYNC_NETWORK_IOİstemci sonucu yeterince hızlı almıyorUygulamanın satır satır işlemesi, gereksiz büyük sonuç kümeleri

İstatistikleri DBCC SQLPERF ('sys.dm_os_wait_stats', CLEAR); ile sıfırlayabilirsiniz, ama bu aynı sunucuyu izleyen başka araçları da etkiler; fark almak için kayıt tablosu daha güvenlidir.

Adım 2: Query Store ile pahalı sorguları bulun

Query Store, sorguları, planlarını ve çalışma istatistiklerini veri tabanının içinde zaman aralıklarına bölerek saklar; plan önbelleğinin aksine yeniden başlatmada kaybolmaz. SQL Server 2022’den itibaren yeni veri tabanlarında varsayılan olarak açıktır; 2016, 2017 ve 2019’da elle açılır. Yükseltilmiş veri tabanlarında durumu mutlaka kontrol edin.

ALTER DATABASE CURRENT
SET QUERY_STORE = ON (
    OPERATION_MODE = READ_WRITE,
    QUERY_CAPTURE_MODE = AUTO,
    WAIT_STATS_CAPTURE_MODE = ON   -- SQL Server 2017 ve sonrası
);

-- Son 24 saatte toplam CPU'ya göre en pahalı 20 sorgu/plan
SELECT TOP (20)
       q.query_id,
       p.plan_id,
       SUM(rs.count_executions)                                  AS executions,
       SUM(rs.avg_cpu_time  * rs.count_executions) / 1000.0      AS total_cpu_ms,
       SUM(rs.avg_duration  * rs.count_executions) / 1000.0      AS total_duration_ms,
       SUM(rs.avg_logical_io_reads * rs.count_executions)        AS total_logical_reads,
       MAX(qt.query_sql_text)                                    AS query_text
FROM sys.query_store_runtime_stats          AS rs
JOIN sys.query_store_runtime_stats_interval AS i  ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id
JOIN sys.query_store_plan                   AS p  ON p.plan_id = rs.plan_id
JOIN sys.query_store_query                  AS q  ON q.query_id = p.query_id
JOIN sys.query_store_query_text             AS qt ON qt.query_text_id = q.query_text_id
WHERE i.start_time >= DATEADD(HOUR, -24, SYSDATETIMEOFFSET())
GROUP BY q.query_id, p.plan_id
ORDER BY total_cpu_ms DESC;

Dün hızlı olup bugün yavaşlayan bir sorgu için SSMS’teki Regressed Queries raporu aynı sorgunun eski ve yeni planını yan yana gösterir. Eski plan iyiyse EXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 7; ile zorlanabilir. Bunu geçici bir yama sayın: asıl nedeni (istatistik, indeks, veri büyümesi) düzeltip zorlamayı kaldırın.

Adım 3: Gerçek yürütme planını okuyun

SSMS’te Include Actual Execution Plan (Ctrl+M) açıkken sorguyu IO ve zaman istatistikleriyle çalıştırın. logical reads, okunan 8 KB’lık sayfa sayısıdır ve önce/sonra karşılaştırmasında süreden daha kararlı bir ölçüdür, çünkü önbellekten etkilenmez.

SET STATISTICS IO, TIME ON;

SELECT SaleDate, ItemCode, Qty, Amount
FROM dbo.SalesLine
WHERE StoreCode = 'M0017'
  AND SaleDate >= '20250101' AND SaleDate < '20250201';

Planda üç şeye bakın:

  • Seek mi, scan mi? Index Seek, indeks ağacında yalnızca ilgili aralığa iner; Scan tüm indeksi ya da tabloyu okur. Scan her zaman kötü değildir: tablonun büyük kısmını döndüren sorguda en ucuz yol odur. Kötü olan, birkaç yüz satır için milyonlarca satırı taramaktır.
  • Key Lookup: IX_SalesLine_StoreCode yalnızca StoreCode’u içerdiği için bulunan her satırın diğer sütunları clustered indeksten tek tek okunur. Planda Index Seek, Key Lookup ve Nested Loops birlikte görünür. Az satırda ucuzdur; binlerce satırda planın en pahalı adımı olur.
  • Tahmini ve gerçek satır sayısı: Operatör özelliklerinde “Estimated Number of Rows Per Execution” ile “Actual Number of Rows for All Executions” değerlerini karşılaştırın; ikincisi tüm çalıştırmaların toplamıdır, iç döngülerde çalıştırma sayısına bölün. Kat kat fark varsa optimizer yanlış bilgiyle karar vermiştir: eski istatistik, sargable olmayan koşul ya da parameter sniffing.

Sarı ünlemli uyarılar da önemli: tempdb’ye taşan sıralama/hash (spill) ve tür dönüşümü (CONVERT_IMPLICIT) en sık görülenler. Bu sorgu için çözüm, ihtiyaç duyulan sütunları kapsayan bir indeks:

CREATE NONCLUSTERED INDEX IX_SalesLine_Store_Date
    ON dbo.SalesLine (StoreCode, SaleDate)
    INCLUDE (ItemCode, Qty, Amount);
-- IX_SalesLine_StoreCode artık bu indeksin ön ekidir; kullanımını kontrol edip kaldırabilirsiniz.

Adım 4: İndeksleri tasarlayın

  • Clustered indeks tablonun kendisidir ve tabloda bir tane olabilir. Dar, benzersiz, değişmeyen ve artan bir anahtar (IDENTITY gibi) iyi bir varsayılandır, çünkü her nonclustered indeks satıra bu anahtarla ulaşır. Rastgele GUID anahtar sayfa bölünmelerini artırır. Clustered indeksi olmayan tablo (heap) çoğu zaman bilinçli bir karar değil, unutulmuş bir ayrıntıdır.
  • Sütun sırası: Eşitlikle aranan sütunlar önce, aralıkla (>, <, BETWEEN) aranan sütun sonra gelir; eşitlik sütunları arasında seçici olan başa yazılır. (StoreCode, SaleDate) bu kuralın örneği.
  • INCLUDE sütunları yalnızca yaprak düzeyde durur; aramaya katılmaz ama key lookup’ı ortadan kaldırır. Sıraları önemsizdir.
  • Filtreli indeks tablonun yalnızca bir alt kümesini indeksler; küçük ve bakımı ucuzdur:
CREATE NONCLUSTERED INDEX IX_SalesLine_Returns
    ON dbo.SalesLine (SaleDate)
    INCLUDE (StoreCode, Amount)
    WHERE IsReturn = 1;

Filtreli indeksin iki tuzağı var: WHERE IsReturn = @p gibi parametreli bir koşulda optimizer bu indeksi kullanmayabilir, çünkü önbellekteki plan her parametre değeri için doğru olmak zorundadır. Ayrıca tabloya yazan oturumlarda ANSI_NULLS ve QUOTED_IDENTIFIER gibi SET seçeneklerinin açık olması gerekir.

Her indeks INSERT, UPDATE ve DELETE için ek maliyet ve disk demektir. Kullanılmayan indeksleri düzenli kontrol edin, ama sayaçların yeniden başlatmada sıfırlandığını ve ay sonu ya da yıl sonu raporlarının kullandığı indekslerin haftalarca “kullanılmıyor” görünebileceğini unutmayın:

SELECT OBJECT_NAME(i.object_id) AS table_name,
       i.name                   AS index_name,
       ISNULL(s.user_seeks, 0) + ISNULL(s.user_scans, 0) + ISNULL(s.user_lookups, 0) AS reads,
       ISNULL(s.user_updates, 0) AS writes
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS s
       ON s.object_id = i.object_id AND s.index_id = i.index_id AND s.database_id = DB_ID()
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
  AND i.type_desc = 'NONCLUSTERED'
  AND i.is_primary_key = 0
  AND i.is_unique_constraint = 0
ORDER BY reads ASC, writes DESC;

Eksik indeks DMV’lerinin tuzakları

Optimizer, derleme sırasında “şu indeks olsaydı daha ucuz olurdu” dediği durumları kaydeder. Microsoft Learn’deki sınırlamalar listesi, bu önerilerin neden olduğu gibi uygulanmaması gerektiğini açıklıyor:

  • Tek bir sorgunun derlenmesindeki tahmine dayanır; öneri çalıştırmadan sonra test edilmez.
  • Yalnızca nonclustered rowstore indeks önerir; benzersiz ve filtreli indeks önermez.
  • Anahtar sütunlarının sırasını belirtmez.
  • INCLUDE listesinin büyüklüğü için maliyet–fayda hesabı yapmaz.
  • Aynı tablo için birbirine çok benzeyen öneriler üretir.
  • En fazla 600 eksik indeks grubu toplar; yeniden başlatma, failover ya da tablodaki şema değişikliğiyle sıfırlanır.
SELECT TOP (20)
       CONVERT(decimal(28,1), migs.avg_total_user_cost * migs.avg_user_impact
               * (migs.user_seeks + migs.user_scans)) AS estimated_improvement,
       mid.statement AS table_name,
       mid.equality_columns,
       mid.inequality_columns,
       mid.included_columns
FROM sys.dm_db_missing_index_groups      AS mig
JOIN sys.dm_db_missing_index_group_stats AS migs ON migs.group_handle = mig.index_group_handle
JOIN sys.dm_db_missing_index_details     AS mid  ON mid.index_handle  = mig.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY estimated_improvement DESC;

Benim yöntemim: bir tablonun tüm önerilerini mevcut indekslerle yan yana koyup birleştirmek, eşitlik–aralık kuralına göre sıralamak, sonra etkisini Query Store’da ölçmek.

Adım 5: İstatistikleri güncel tutun

Optimizer bir koşulun kaç satır döndüreceğini istatistik histogramından tahmin eder; plan seçimi bu tahmine dayanır. AUTO_UPDATE_STATISTICS açıkken istatistik, değişen satır sayısı bir eşiği geçtikten sonra bir sorgu onu kullandığında güncellenir (Microsoft Learn: Statistics):

  • SQL Server 2014’e kadar ve uyumluluk düzeyi 130’un altında eşik 500 + tablonun %20’si’dir.
  • SQL Server 2016 ve uyumluluk düzeyi 130’dan itibaren eşik MIN(500 + 0,20 × n, √(1000 × n)) olur. Microsoft’un örneğinde 2 milyon satırlık tabloda eşik 400.500 yerine 44.721 değişikliktir.

Hesapla göstermek gerekirse, 100 milyon satırlık bir hareket tablosunda eski eşik 20 milyonu aşkın değişiklik demektir. Tarihe göre sürekli büyüyen tablolarda en yeni günler histogramın son adımının dışında kalır ve “bugünün” satırları olduğundan az tahmin edilir. Büyük ERP tablolarında bu yüzden zamanlanmış istatistik güncellemesi, otomatik güncellemenin tamamlayıcısıdır:

SELECT s.name AS stats_name,
       sp.last_updated,
       sp.rows,
       sp.rows_sampled,
       sp.modification_counter
FROM sys.stats AS s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
WHERE s.object_id = OBJECT_ID(N'dbo.SalesLine')
ORDER BY sp.modification_counter DESC;

-- Tek bir istatistiği tam taramayla güncelle
UPDATE STATISTICS dbo.SalesLine IX_SalesLine_Store_Date WITH FULLSCAN;

-- Tüm tablo: örnekleme oranını sabitle; sonraki güncellemeler de bu oranı kullanır
UPDATE STATISTICS dbo.SalesLine WITH SAMPLE 25 PERCENT, PERSIST_SAMPLE_PERCENT = ON;

Adım 6: Parameter sniffing’i tanıyın

SQL Server parametreli bir sorguyu veya saklı yordamı ilk çağrıldığı değerlere göre derler ve planı önbellekte tekrar kullanır. Veri dengesiz dağılmışsa ilk değer için iyi olan plan diğerleri için felaket olabilir:

CREATE OR ALTER PROCEDURE dbo.GetStoreSales
    @StoreCode varchar(10),
    @From      date,
    @To        date
AS
BEGIN
    SET NOCOUNT ON;
    SELECT SalesLineID, SaleDate, ItemCode, Qty, Amount, IsReturn
    FROM dbo.SalesLine
    WHERE StoreCode = @StoreCode
      AND SaleDate >= @From AND SaleDate < @To;
END;
GO
-- Küçük mağaza önce çağrılırsa Seek + Key Lookup planı önbelleğe girer;
-- satırların %60'ını tutan M0001 aynı planla yüz binlerce lookup yapar.
EXEC dbo.GetStoreSales @StoreCode = 'M0017', @From = '20240101', @To = '20260101';
EXEC dbo.GetStoreSales @StoreCode = 'M0001', @From = '20240101', @To = '20260101';

Çözüm seçenekleri:

-- Seçenek 1: sorgunun sonuna ekleyin; her çalıştırmada yeniden derlenir
--   OPTION (RECOMPILE)
-- Seçenek 2: tipik bir değere göre ya da histogram yerine ortalama yoğunlukla derleyin
--   OPTION (OPTIMIZE FOR (@StoreCode = 'M0001'))
--   OPTION (OPTIMIZE FOR UNKNOWN)
-- Seçenek 3 (SQL Server 2022+): kodu değiştirmeden Query Store ipucu ekleyin.
-- query_id değerini sys.query_store_query ve sys.query_store_query_text'ten bulun.
EXEC sys.sp_query_store_set_hints @query_id = 42, @query_hints = N'OPTION(RECOMPILE)';

SQL Server 2022’de uyumluluk düzeyi 160 ile gelen Parameter Sensitive Plan (PSP) optimizasyonu, eşitlik koşullarındaki dengesiz dağılımı histogramdan fark edip aynı sorgu için birden çok plan (query variant) tutabiliyor. SQL Server 2025’te uyumluluk düzeyi 170 ile gelen Optional Parameter Plan Optimization (OPPO) ise @p IS NULL OR sütun = @p biçimindeki isteğe bağlı parametre kalıpları için çalışma anında uygun planı seçiyor.

Benim sıram: önce kök nedene bakın. Bu örnekte IsReturn’ü indeksin INCLUDE listesine eklemek lookup’ı kaldırır ve iki çağrıyı da aynı iyi plana taşır. Sorgu seyrek çalışıyorsa RECOMPILE en az riskli seçenektir; saniyede yüzlerce kez çalışıyorsa derleme maliyeti yüzünden plan zorlama ya da PSP daha doğrudur.

Adım 7: Sargable sorgular yazın

Sargable (search argument-able) koşul, indeks üzerinde seek yapılabilen koşuldur. Sütunu bir fonksiyona sararsanız ya da türünü dönüştürürseniz optimizer seek yapamaz ve taramaya düşer:

Sargable değilSargable karşılığı
WHERE YEAR(SaleDate) = 2025WHERE SaleDate >= '20250101' AND SaleDate < '20260101'
WHERE LEFT(ItemCode, 3) = 'ITM'WHERE ItemCode LIKE 'ITM%'
WHERE ISNULL(StoreCode, '') = 'M0017'WHERE StoreCode = 'M0017' (sütun NOT NULL)
WHERE Amount * 1.2 > 1000WHERE Amount > 1000 / 1.2
WHERE StoreCode = N'M0017' (varchar sütun)WHERE StoreCode = 'M0017'

Son satır sinsidir: uygulama katmanı varchar bir sütunu nvarchar parametreyle sorguladığında sütun tarafında örtük dönüşüm oluşur; bu, harmanlamaya (collation) göre seek’i engelleyebilir ya da pahalılaştırabilir. Planda CONVERT_IMPLICIT uyarısı olarak görünür ve çözümü uygulamadaki parametre türünü sütunla eşlemektir.

Adım 8: tempdb’yi ihmal etmeyin

Geçici tablolar, tablo değişkenleri, taşan sıralama ve hash işlemleri ve satır sürümleme (RCSI, snapshot, online indeks işlemleri) tempdb’yi kullanır. Microsoft’un rehberi: mantıksal işlemci sayısı sekiz ya da daha azsa o kadar veri dosyası, fazlaysa sekiz dosya; ayırma çekişmesi sürerse dosya sayısını dörder artırın. Tüm veri dosyaları aynı başlangıç boyutunda ve aynı büyüme ayarında olmalı. SQL Server 2019’daki bellek için optimize edilmiş tempdb meta verisi yalnızca meta veri çekişmesi görüldüğünde açılmalı; SQL Server 2025 ise tempdb alanı için kaynak yönetimi (resource governance) ekledi.

SELECT name,
       type_desc,
       size * 8 / 1024 AS size_mb,
       CASE WHEN is_percent_growth = 1 THEN CONCAT(growth, ' %')
            ELSE CONCAT(growth * 8 / 1024, ' MB') END AS growth
FROM tempdb.sys.database_files;

Adım 9: Körlemesine rebuild yerine ölçülmüş bakım

Microsoft’un güncel indeks bakımı rehberi net: bakım kararı sabit parçalanma ya da sayfa doluluğu eşiklerine göre verilmemeli, etkisi iş yükünde ölçülmeli. Rehberin önemli bir tespiti var: rebuild, anahtar sütunlarının istatistiklerini tam taramayla günceller ve sonrasında görülen iyileşme çoğu zaman bundan gelir. Aynı fayda çok daha ucuz olan istatistik güncellemesiyle alınabilir.

Pratikte birçok ekip bakım için Ola Hallengren’in ücretsiz SQL Server Maintenance Solution betiklerini kullanır: yedekleme, bütünlük denetimi ve indeks/istatistik bakımı için hazır, parametreli yordamlar. Örneğin yalnızca değişmiş istatistikleri güncelleyen, indekslere dokunmayan bir gece işi:

EXECUTE dbo.IndexOptimize
    @Databases = 'USER_DATABASES',
    @FragmentationLow = NULL,
    @FragmentationMedium = NULL,
    @FragmentationHigh = NULL,
    @UpdateStatistics = 'ALL',
    @OnlyModifiedStatistics = 'Y';

Sürüm özellikleri: Intelligent Query Processing

SQL Server 2017’den bu yana Intelligent Query Processing (IQP) ailesi, eskiden elle çözdüğümüz bazı sorunları kendiliğinden ele alıyor. Özelliklerin çoğu sürümü kurmakla değil, veri tabanının uyumluluk düzeyini yükseltmekle devreye giriyor:

Sürüm (uyumluluk düzeyi)Öne çıkan IQP özellikleri
SQL Server 2017 (140)Batch mode adaptive join, çok deyimli tablo değerli fonksiyonlar için interleaved execution, batch mode memory grant feedback
SQL Server 2019 (150)Row mode memory grant feedback, tablo değişkenleri için ertelenmiş derleme, scalar UDF inlining, rowstore üzerinde batch mode, APPROX_COUNT_DISTINCT
SQL Server 2022 (160)Parameter Sensitive Plan optimizasyonu, CE feedback, DOP feedback, bellek izni geri bildiriminin kalıcılığı; yeni veri tabanlarında Query Store varsayılan açık
SQL Server 2025 (170)Optional Parameter Plan Optimization, ifadeler için CE feedback, OPTIMIZED_SP_EXECUTESQL

Bazı özellikler daha düşük düzeyde de çalışır, bazıları ek ayar ister (OPTIMIZED_SP_EXECUTESQL bir veri tabanı kapsamlı ayardır); CE feedback, DOP feedback ve geri bildirim kalıcılığı Query Store’un açık olmasını gerektirir. Uyumluluk düzeyini yükseltmek planları değiştirebilir. Güvenli yol: Query Store açıkken mevcut düzeyde bir süre veri toplayın, düzeyi yükseltin, gerileyen sorguları Regressed Queries raporunda bulun ve gerekirse eski planı zorlayın.

Kontrol listesi

  1. Belirtiyi ve zaman aralığını tanımlayın; “sunucu yavaş” bir belirti değildir.
  2. sys.dm_os_wait_stats farkıyla baskın bekleme türünü bulun.
  3. Query Store’u açın ve en pahalı sorguları CPU, süre ve mantıksal okumaya göre sıralayın.
  4. Gerçek planda seek/scan, key lookup ve tahmini–gerçek satır farkına bakın.
  5. İndeksleri eşitlik–aralık kuralıyla tasarlayın; INCLUDE ile lookup’ı kaldırın, eksik indeks önerilerini birleştirerek uygulayın.
  6. Büyük tablolarda istatistikleri zamanlanmış işle güncelleyin.
  7. Parameter sniffing şüphesinde önce kök nedene, sonra RECOMPILE, plan zorlama ya da PSP’ye bakın.
  8. WHERE koşullarında sütunu fonksiyona sarmayın, türleri eşleyin.
  9. tempdb dosyalarını eşit boyutta ve doğru sayıda tutun.
  10. Bakımı ölçerek yapın; rebuild yerine çoğu zaman istatistik güncellemesi yeter.
  11. Uyumluluk düzeyini Query Store ile önce/sonra karşılaştırarak yükseltin.

Aynı “önce ölç, sonra tek değişiklik” yaklaşımını web tarafında bu sitenin Astro ve Cloudflare ile hızlandırılması yazısında anlattım.

Sık sorulan sorular

SQL Server'da yavaş çalışan sorgu nasıl bulunur?

Önce sys.dm_os_wait_stats ile sunucunun en çok neyi beklediğine bakın: disk, kilit, CPU ya da bellek. Ardından Query Store'da toplam CPU, süre ya da mantıksal okuma bakımından en pahalı sorguları sıralayın. Seçtiğiniz sorgunun gerçek yürütme planını ve SET STATISTICS IO çıktısını inceleyerek sorunun indeks, istatistik ya da sorgu yazımı olduğuna karar verin.

Key lookup nedir, nasıl giderilir?

Key lookup, nonclustered indeksle bulunan her satır için eksik sütunların clustered indeksten tek tek okunmasıdır. Az satırda sorun değildir, binlerce satırda planın en pahalı adımı olur. Genellikle sorgunun ihtiyaç duyduğu sütunları indekse INCLUDE ile ekleyerek, yani kapsayan (covering) indeksle giderilir.

Eksik indeks önerilerini olduğu gibi oluşturmalı mıyım?

Hayır. Öneriler tek bir sorgunun derlenmesi sırasında yapılan tahmine dayanır, sütun sırası belirtmez, aynı tablo için birbirine benzeyen öneriler üretir ve INCLUDE listesinin boyut maliyetini hesaplamaz. Önerileri tablonun mevcut indeksleriyle birlikte değerlendirip birleştirin, sonra etkisini Query Store ile doğrulayın.

Parameter sniffing nedir, nasıl çözülür?

SQL Server parametreli bir sorguyu ilk çalıştırıldığı değerlere göre derler ve planı önbellekte tekrar kullanır; veri dağılımı dengesizse bu plan başka değerler için kötü olabilir. Başlıca çözümler OPTION (RECOMPILE), OPTIMIZE FOR, Query Store ile plan zorlama ya da Query Store ipuçları ve SQL Server 2022'de uyumluluk düzeyi 160 ile gelen Parameter Sensitive Plan optimizasyonudur.

İndeksleri her gece rebuild etmek gerekli mi?

Çoğu zaman hayır. Microsoft, bakım kararının sabit parçalanma eşiklerine göre değil, iş yüküne etkisi ölçülerek verilmesini öneriyor. Rebuild sonrası görülen iyileşme çoğunlukla istatistiklerin tam taramayla güncellenmesinden gelir ve aynı etki çok daha ucuz olan istatistik güncellemesiyle alınabilir.

Kaynaklar

  1. sys.dm_os_wait_stats (Microsoft Learn) learn.microsoft.com
  2. Monitor performance by using the Query Store (Microsoft Learn) learn.microsoft.com
  3. Tune nonclustered indexes with missing index suggestions (Microsoft Learn) learn.microsoft.com
  4. Statistics (Microsoft Learn) learn.microsoft.com
  5. Maintain indexes optimally (Microsoft Learn) learn.microsoft.com
  6. Intelligent query processing (Microsoft Learn) learn.microsoft.com

SQL ServerT-SQLQuery Storeindeksyürütme planıparameter sniffing Markdown sürümü

İletişim

Konuşalım.