← Blog

SQL Server'da Yavaş Sorguyu Bulmak ve Hızlandırmak: Adım Adım Rehber

"Sistem yavaş" şikâyetinden kök nedene giden yol: Query Store, DMV'ler, execution plan okuma, doğru index, SARGable sorgular, parameter sniffing ve EF Core tarafında dikkat edilmesi gerekenler.

"Sistem yavaşladı" şikâyeti genellikle tek bir cümleyle gelir, ama arkasında çoğu zaman birkaç sorgu vardır. İyi haber şu: SQL Server, sorunun nerede olduğunu bulmanız için gereken her şeyi zaten topluyor. Bu yazıda, sahada kullandığımız sırayla ilerleyen pratik bir yol haritası paylaşıyoruz.

1. Tahmin etmeyin, ölçün

İlk adım, "hangi sorgu?" sorusunu cevaplamak. Bunun için iki temel kaynak var: Query Store ve DMV'ler (dinamik yönetim görünümleri).

Query Store, sorguların planlarını ve çalışma istatistiklerini veritabanı içinde saklar; sunucu yeniden başlasa bile geçmiş kaybolmaz. Kapalıysa açın:

ALTER DATABASE [ErpDb]
SET QUERY_STORE = ON (OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = AUTO);

Hızlı bir anlık görüntü için ise plan önbelleğinden en çok CPU tüketen sorguları listeleyebilirsiniz:

SELECT TOP (20)
       qs.total_worker_time  / qs.execution_count / 1000 AS OrtCpuMs,
       qs.total_elapsed_time / qs.execution_count / 1000 AS OrtSureMs,
       qs.total_logical_reads / qs.execution_count       AS OrtOkuma,
       qs.execution_count                                AS Calisma,
       SUBSTRING(st.text, qs.statement_start_offset / 2 + 1,
           (CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
            ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2 + 1) AS Sorgu
FROM   sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
ORDER BY qs.total_worker_time DESC;

2. Execution plan'i okumak

Sorguyu bulduktan sonra gerçek çalışma planını (Actual Execution Plan) alın. Plan okurken önce şu dört işarete bakın:

  • Index Scan / Table Scan: Büyük tablolarda tüm satırların okunması. Az satır dönen bir sorguda scan görüyorsanız eksik ya da kullanılamayan bir index olabilir.
  • Key Lookup: Index satırı buluyor ama gereken kolonlar index'te olmadığı için tabloya geri dönüyor. Binlerce kez tekrarlanıyorsa maliyetlidir.
  • Tahmini ve gerçek satır sayısı farkı: 10 satır tahmin edilip 500.000 satır geliyorsa, istatistikler eski veya sorgu SQL Server'ın tahmin yapamayacağı biçimde yazılmış olabilir.
  • Uyarılar: Sarı ünlem işaretleri; örtük tip dönüşümü (implicit conversion), tempdb'ye taşan sıralama (spill) gibi sorunları gösterir.

3. Doğru index'i tasarlamak

İyi bir index, sorgunun filtrelediği kolonlarla başlar ve sorgunun döndürdüğü kolonları INCLUDE ile kapsar. Örneğin depo bazlı stok bakiyesi sorgusu için:

CREATE NONCLUSTERED INDEX IX_StokHareket_Depo_Stok
ON dbo.StokHareket (DepoId, StokId)
INCLUDE (Miktar, Tarih);

Bu index ile sorgu hem doğru satırlara doğrudan ulaşır (seek) hem de tabloya geri dönmeden ihtiyaç duyduğu tüm kolonları index'ten okur.

4. SARGable olmayan sorgular

Index olsa bile, sorgu index'in kullanılamayacağı biçimde yazılmışsa SQL Server tüm tabloyu taramak zorunda kalır. En sık gördüğümüz örnekler:

-- Kolon bir fonksiyona sokulduğu için index kullanılamaz
WHERE YEAR(f.FaturaTarihi) = 2026

-- Aynı sonuç, index'i kullanabilen hali
WHERE f.FaturaTarihi >= '20260101' AND f.FaturaTarihi < '20270101'

-- ISNULL kolonu sarmalar; index kullanılamaz
WHERE ISNULL(c.Durum, 0) = 1

-- Doğrusu
WHERE c.Durum = 1

Bir başka sinsi sorun örtük tip dönüşümüdür: varchar olan bir kolon nvarchar bir parametreyle karşılaştırıldığında SQL Server her satırı dönüştürmek zorunda kalabilir. Parametre tiplerinin kolon tipleriyle birebir aynı olduğundan emin olun.

5. Parameter sniffing

Bir stored procedure ya da parametreli sorgu bazen hızlı, bazen çok yavaş çalışıyorsa sebep büyük ihtimalle parameter sniffing'dir. SQL Server planı ilk gelen parametre değerine göre oluşturur ve önbelleğe alır. İlk değer 10 satır döndüren bir müşteriyse, 2 milyon satırlık müşteri için de aynı plan kullanılır.

Çözüm seçenekleri, etkisi küçükten büyüğe:

  1. Sorgunun sık çalışmadığı durumlarda OPTION (RECOMPILE) ile her seferinde yeni plan
  2. OPTIMIZE FOR ile tipik bir değere göre plan
  3. Query Store üzerinden iyi bilinen bir planı zorlamak (plan forcing)
  4. Çok farklı veri dağılımları için sorguyu ikiye ayırmak

6. İstatistikler ve bakım

Tahminlerin doğru olması güncel istatistiklere bağlıdır. Otomatik güncelleme açık olsa da, büyük toplu yüklemelerden sonra istatistikler geride kalabilir. Gece çalışan bir bakım işi ile istatistikleri güncellemek ve index parçalanmasını kontrol etmek, birçok "gizemli" yavaşlığı önler.

7. Uygulama tarafı: EF Core

Sorunların bir kısmı veritabanında değil, uygulamanın veritabanıyla konuşma biçimindedir. EF Core kullanırken dikkat ettiğimiz noktalar:

  • Sadece okunacak verilerde AsNoTracking() kullanın.
  • Tüm entity'yi değil, ihtiyaç duyulan kolonları Select ile çekin.
  • Döngü içinde sorgu atmaktan (N+1 problemi) kaçının.
  • Geliştirme ortamında üretilen SQL'i loglayıp gözden geçirin.
var liste = await db.Faturalar
    .AsNoTracking()
    .Where(f => f.Tarih >= baslangic && f.Tarih < bitis)
    .OrderByDescending(f => f.Tarih)
    .Select(f => new FaturaOzetDto(f.Id, f.FaturaNo, f.Cari.Unvan, f.GenelToplam))
    .Take(100)
    .ToListAsync(ct);

Kontrol listesi

AdımAraç
En maliyetli sorguları bulQuery Store, sys.dm_exec_query_stats
Planı inceleActual Execution Plan
Index'i düzeltKapsayan (covering) index, INCLUDE
Sorguyu düzeltSARGable koşullar, doğru parametre tipleri
Plan kararsızlığıRECOMPILE, OPTIMIZE FOR, plan forcing
Uygulama tarafıAsNoTracking, projeksiyon, N+1 kontrolü

Performans çalışması bir kerelik bir iş değil; sistem büyüdükçe tekrarlanan bir disiplindir. SQL Server tarafında yaşadığınız bir yavaşlık varsa, mevcut sisteminizi inceleyip somut bir iyileştirme planı çıkarabiliriz.

Benzer bir projeniz mi var?

İhtiyacınızı anlatın; size özel bir yol haritası ve tahmini bir plan çıkaralım.

Bize Ulaşın →