---
title: "SQL Server performans ayarı: indeks, istatistik ve sorgu planı"
author: "Abdulaziz Akyol"
author_url: https://www.abdulazizakyol.com/hakkimda/
url: https://www.abdulazizakyol.com/blog/sql-server-performans-ayari-indeks-istatistik-ve-sorgu-plani/
language: tr
published: 2026-09-25
categories: ["Microsoft SQL Server", "Yazılım geliştirme"]
tags: ["SQL Server", "T-SQL", "Query Store", "indeks", "yürütme planı", "parameter sniffing"]
translation: https://www.abdulazizakyol.com/en/blog/sql-server-performance-tuning-indexes-statistics-query-plans/
description: "SQL Server'da yavaş sorguyu tahminle değil ölçümle bulun: bekleme istatistikleri, Query Store, yürütme planı, indeks, istatistik ve parameter sniffing."
---

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

Yazar: [Abdulaziz Akyol](https://www.abdulazizakyol.com/hakkimda/) · 2026-09-25

## Öne çıkanlar

- SQL Server performans ayarı tahminle değil, sırasıyla bekleme istatistikleri, Query Store ve gerçek yürütme planıyla yapılır: önce sunucunun neyi beklediği, sonra bu beklemeyi hangi sorgunun ürettiği bulunur.
- Yürütme planında tahmini ve gerçek satır sayısı arasındaki büyük fark çoğu zaman eski istatistiğin, sargable olmayan bir koşulun ya da parameter sniffing'in işaretidir.
- Eksik indeks DMV'leri öneridir, reçete değildir: sütun sırası belirtmez, filtreli ve benzersiz indeks önermez, INCLUDE listesinin maliyetini hesaplamaz ve yeniden başlatmada sıfırlanır.
- Microsoft'un güncel rehberine göre indeks bakımı sabit parçalanma eşiklerine göre körlemesine yapılmamalıdır; rebuild sonrası görülen iyileşmenin önemli bir kısmı istatistiklerin güncellenmesinden gelir.
- SQL Server 2022'deki Parameter Sensitive Plan optimizasyonu ve SQL Server 2025'teki Optional Parameter Plan Optimization gibi Intelligent Query Processing özellikleri ancak veri tabanı uyumluluk düzeyi yükseltildiğinde devreye girer.

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ı](https://www.abdulazizakyol.com/blog/abdulaziz-akyol-civil-magazacilik-nebim-v3-veri-ambari-ve-is-zekasi-roportaji/) 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](https://www.abdulazizakyol.com/blog/sql-tablo-olusturma/) yazısına bakabilirsiniz.

```sql
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.

```sql
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 / \_EX | Veri sayfaları diskten okunuyor            | Mantı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 / CXCONSUMER  | Paralel planlar; tek başına sorun değildir | Pahalı planlar, MAXDOP ve paralellik eşiği                      |
| SOS\_SCHEDULER\_YIELD  | CPU baskısı                                | En çok CPU tüketen sorgular                                     |
| WRITELOG               | Transaction log yazma gecikmesi            | Log diski, satır satır commit eden işler                        |
| RESOURCE\_SEMAPHORE    | Bellek izni (memory grant) bekleme         | Büyük sıralama/hash işlemleri, yanlış satır tahminleri          |
| ASYNC\_NETWORK\_IO     | İstemci sonucu yeterince hızlı almıyor     | Uygulamanı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](https://learn.microsoft.com/en-us/sql/relational-databases/performance/monitoring-performance-by-using-the-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.

```sql
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.

```sql
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:

```sql
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:

```sql
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:

```sql
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](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/tune-nonclustered-missing-index-suggestions) 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.

```sql
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](https://learn.microsoft.com/en-us/sql/relational-databases/statistics/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:

```sql
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:

```sql
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:

```sql
-- 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](https://learn.microsoft.com/en-us/sql/relational-databases/performance/parameter-sensitive-plan-optimization), 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ğil                               | Sargable karşılığı                                       |
| -------------------------------------------- | -------------------------------------------------------- |
| `WHERE YEAR(SaleDate) = 2025`                | `WHERE 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 > 1000`                  | `WHERE 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](https://learn.microsoft.com/en-us/sql/relational-databases/databases/tempdb-database): 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.

```sql
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](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/reorganize-and-rebuild-indexes) 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:

```sql
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)](https://learn.microsoft.com/en-us/sql/relational-databases/performance/intelligent-query-processing) 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ı](https://www.abdulazizakyol.com/blog/astro-ve-cloudflare-ile-hizli-kisisel-site-core-web-vitals/) 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)](https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-wait-stats-transact-sql)
2. [Monitor performance by using the Query Store (Microsoft Learn)](https://learn.microsoft.com/en-us/sql/relational-databases/performance/monitoring-performance-by-using-the-query-store)
3. [Tune nonclustered indexes with missing index suggestions (Microsoft Learn)](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/tune-nonclustered-missing-index-suggestions)
4. [Statistics (Microsoft Learn)](https://learn.microsoft.com/en-us/sql/relational-databases/statistics/statistics)
5. [Maintain indexes optimally (Microsoft Learn)](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/reorganize-and-rebuild-indexes)
6. [Intelligent query processing (Microsoft Learn)](https://learn.microsoft.com/en-us/sql/relational-databases/performance/intelligent-query-processing)

---

Abdulaziz Akyol, CX Teknoloji'nin (yapay zekâ, görüntü işleme ve IoT) kurucusudur. Asıl sürüm: https://www.abdulazizakyol.com/blog/sql-server-performans-ayari-indeks-istatistik-ve-sorgu-plani/
