MSSQL'de Index Stratejileri — Production'da Öğrendiklerim
MSSQL'de Index Stratejileri — Production'da Öğrendiklerim
Neden Index Bu Kadar Önemli?
En zarif C# kodunu yazabilirsiniz, en temiz mimariyi kurabilirsiniz, en sürdürülebilir repository pattern'ı uygulayabilirsiniz — ve 10 milyon satırlık bir tabloda tek bir table scan ile uygulamanızı diz çöktürebilirsiniz. Index'ler MSSQL'deki en yüksek etkili performans aracıdır ve çoğu geliştirici onlara sonradan düşünülen bir şey gibi yaklaşır.
Bu hatayı ben de yaptım. İşte öğrendiklerim.
Temel Kavram — Index Nedir?
Index, MSSQL'in tablonuzun yanında ayrı olarak tuttuğu bir veri yapısıdır. Bir kitabın sonundaki dizin gibi düşünün — "dependency injection" kelimesini bulmak için her sayfayı okumak yerine doğrudan doğru sayfaya atlarsınız.
Index olmadan: Table Scan — SQL Server her satırı okur.
Index ile: Index Seek — SQL Server doğrudan eşleşen satırlara atlar.
Ölçekte fark 2x veya 5x değildir. 1000x olabilir.
Bilmeniz Gereken Index Tipleri
1. Clustered Index
Her tablonun bir tane olmalıdır. Clustered index tablonun kendisidir — veri satırları fiziksel olarak index sırasında saklanır.
-- PRIMARY KEY varsayılan olarak clustered index oluşturur
CREATE TABLE Orders (
OrderId INT IDENTITY(1,1) NOT NULL,
CustomerId INT NOT NULL,
OrderDate DATETIME2 NOT NULL,
TotalAmount DECIMAL(18,2) NOT NULL,
CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderId)
);
Temel kural: Clustered index key'i şu özelliklere sahip olmalıdır:
- Dar (int veya bigint, GUID değil)
- Sürekli artan (identity kolonu)
- Eşsiz
NEWID() kullanmak sayfa parçalanmasına yol açar — her insert rastgele bir konuma düşer, veri sayfalarınızı parçalar ve yazma performansını mahveder.
-- ❌ Kötü — rastgele GUID'ler clustered index'i parçalar
CREATE TABLE Products (
ProductId UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,
...
);
-- ✅ Daha iyi — sıralı, sürekli artan
CREATE TABLE Products (
ProductId UNIQUEIDENTIFIER DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
...
);
-- ✅ En iyi — harici GUID'e ihtiyaç yoksa
CREATE TABLE Products (
ProductId INT IDENTITY(1,1) PRIMARY KEY,
...
);
2. Non-Clustered Index
İndekslenen kolonları ve veri satırına bir işaretçiyi saklayan ayrı bir yapıdır. Tablo başına 999'a kadar oluşturabilirsiniz (ama bu sayıya yaklaşmamalısınız).
-- Sık filtrelenen kolona index
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId
ON Orders (CustomerId);
Artık bu sorgu Table Scan yerine Index Seek yapar:
SELECT * FROM Orders WHERE CustomerId = 42;
3. Composite Index — Kolon Sırası Kritiktir
Composite index birden fazla kolonu kapsar. En soldaki prefix kuralı kritiktir: index yalnızca sorgunuz öndeki kolonları filtreliyorsa kullanışlıdır.
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId_OrderDate
ON Orders (CustomerId, OrderDate);
-- ✅ Index kullanır (öndeki kolona göre filtreler)
SELECT * FROM Orders WHERE CustomerId = 42;
-- ✅ Index kullanır (her iki kolona göre filtreler)
SELECT * FROM Orders WHERE CustomerId = 42 AND OrderDate >= '2024-01-01';
-- ❌ Index KULLANMAZ (öndeki kolonu atlar)
SELECT * FROM Orders WHERE OrderDate >= '2024-01-01';
Pratik kural: En seçici kolonu öne koyun ya da WHERE clause'larında en sık görünen kolonu.
4. Covering Index — Key Lookup'ları Ortadan Kaldırın
Key lookup, SQL Server'ın non-clustered index aracılığıyla satırları bulup ek kolonları almak için clustered index'e geri dönmesi durumunda yaşanır. Bu maliyetlidir.
-- Bu sorgu Key Lookup'a yol açar
-- IX_Orders_CustomerId sadece CustomerId'ye sahip
-- SQL Server TotalAmount için geri gitmek zorunda
SELECT CustomerId, TotalAmount
FROM Orders
WHERE CustomerId = 42;
INCLUDE clause ile covering index oluşturarak düzeltin:
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId_Covering
ON Orders (CustomerId)
INCLUDE (OrderDate, TotalAmount);
Artık SQL Server sorgunun tamamını index üzerinden karşılayabilir — key lookup yok.
Execution Plan Okuma
Execution plan en iyi arkadaşınızdır. Index eklemeden önce ve sonra mutlaka kontrol edin.
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT o.OrderId, o.TotalAmount, c.Name
FROM Orders o
INNER JOIN Customers c ON c.CustomerId = o.CustomerId
WHERE o.CustomerId = 42
AND o.OrderDate >= '2024-01-01';
Nelere bakmalısınız:
| İkon | Anlam | Aksiyon |
|---|---|---|
| Table Scan | Kullanılabilir index yok | Index ekleyin |
| Index Scan | Index var ama yeterince seçici değil | Index'i iyileştirin |
| Index Seek | ✅ Optimal | Aksiyon gerekmez |
| Key Lookup | Clustered index'ten ek kolon alınıyor | INCLUDE kolonları ekleyin |
| Hash Match | Index desteği olmayan büyük join | Join kolonlarını indexleyin |
Messages sekmesindeki logical reads'e bakın:
Table 'Orders'. Scan count 1, logical reads 4821
Doğru index eklendikten sonra:
Table 'Orders'. Scan count 1, logical reads 3
Bu gerçek. Tek bir covering index ile 4000+ logical reads'in 3'e düştüğünü gördüm.
Missing Index DMV — SQL Server Size Ne Lazım Olduğunu Söylesin
SQL Server, index fırsatlarını dynamic management view'larında takip eder. Yüksek etkili eksik indexleri bulmak için sorgulayın:
SELECT TOP 20
ROUND(s.avg_total_user_cost * s.avg_user_impact * (s.user_seeks + s.user_scans), 0)
AS [Toplam Maliyet],
s.avg_user_impact AS [Etki %],
d.statement AS [Tablo],
d.equality_columns,
d.inequality_columns,
d.included_columns
FROM sys.dm_db_missing_index_groups g
INNER JOIN sys.dm_db_missing_index_group_stats s
ON s.group_handle = g.index_group_handle
INNER JOIN sys.dm_db_missing_index_details d
ON d.index_handle = g.index_handle
ORDER BY [Toplam Maliyet] DESC;
Önemli: Önerilen her index'i körü körüne oluşturmayın. SQL Server yalnızca bireysel sorgu kalıplarını görür — yazma iş yükünüzü bilmez. Eklediğiniz her index INSERT, UPDATE ve DELETE işlemlerini yavaşlatır.
Unused Index DMV — İşe Yaramayanları Kaldırın
Index'lerin bir maliyeti vardır. Kullanılmayan indexler disk alanı israf eder ve yazmaları yavaşlatır.
SELECT
OBJECT_NAME(i.object_id) AS TabloAdi,
i.name AS IndexAdi,
i.type_desc,
s.user_seeks,
s.user_scans,
s.user_lookups,
s.user_updates
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats s
ON s.object_id = i.object_id
AND s.index_id = i.index_id
AND s.database_id = DB_ID()
WHERE OBJECT_NAME(i.object_id) NOT LIKE 'sys%'
AND i.type > 0
AND (s.user_seeks = 0 OR s.user_seeks IS NULL)
AND (s.user_scans = 0 OR s.user_scans IS NULL)
ORDER BY s.user_updates DESC;
Index Parçalanması — Bakımı İhmal Etmeyin
Zamanla insert ve update'ler indexlerinizi parçalar. SQL Server aynı veriyi bulmak için daha fazla sayfa okumak zorunda kalır.
-- Parçalanmayı kontrol edin
SELECT
OBJECT_NAME(ips.object_id) AS TabloAdi,
i.name AS IndexAdi,
ips.avg_fragmentation_in_percent,
ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
INNER JOIN sys.indexes i
ON i.object_id = ips.object_id
AND i.index_id = ips.index_id
WHERE ips.avg_fragmentation_in_percent > 10
AND ips.page_count > 1000
ORDER BY ips.avg_fragmentation_in_percent DESC;
Bakım stratejisi:
-- %10–30 parçalanma: REORGANIZE (online, düşük etki)
ALTER INDEX IX_Orders_CustomerId ON Orders REORGANIZE;
-- >%30 parçalanma: REBUILD (daha kapsamlı)
ALTER INDEX IX_Orders_CustomerId ON Orders REBUILD WITH (ONLINE = ON);
-- Tablodaki tüm indexleri yeniden oluştur
ALTER INDEX ALL ON Orders REBUILD;
Bunu yoğun olmayan saatlerde çalışacak bir SQL Agent job'ı olarak zamanlayın.
Filtered Index — Sadece Sorguladığınızı İndeksleyin
Filtered index yalnızca WHERE koşulunu karşılayan satırları indexler. Daha küçük, bakımı daha hızlı ve çok seçicidir.
-- Yalnızca aktif siparişleri indexle — milyonlarca tamamlanmış siparişi değil
CREATE NONCLUSTERED INDEX IX_Orders_Active
ON Orders (CustomerId, OrderDate)
WHERE Status = 'Active';
Düşük kardinaliteli (Status gibi) ama her zaman belirli bir alt kümeyi sorguladığınız durumlarda mükemmeldir.
Sık Yapılan Hatalar
1. Çok fazla index
Her index bir yazma cezasıdır. 15 index'i olan bir tablo acı verici INSERT performansına sahip olacaktır. Tablo başına 5–7 iyi tasarlanmış index hedefleyin.
2. Düşük kardinaliteli kolonları tek başına indexlemek
Yalnızca 2–3 farklı değeri olan bir kolon (IsDeleted BIT gibi) tek başına zayıf bir index adayıdır. Daha seçici bir kolonla birleştirin.
3. İstatistikleri görmezden gelmek
SQL Server satır sayısını tahmin etmek ve sorgu planlarını seçmek için istatistikleri kullanır. Düzenli güncelleyin:
UPDATE STATISTICS Orders WITH FULLSCAN;
-- Ya da veritabanındaki tüm istatistikleri güncelleyin
EXEC sp_updatestats;
4. WHERE clause'da fonksiyon kullanmak
Bir kolonu fonksiyona sarmak index kullanımını engeller:
-- ❌ OrderDate üzerinde index kullanamaz
WHERE YEAR(OrderDate) = 2024
-- ✅ OrderDate üzerinde index seek
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'
Temel Çıkarımlar
- Her tablonun bir clustered index'e ihtiyacı var — GUID yerine INT IDENTITY tercih edin
- Composite index'lerde en çok filtrelenen kolonu öne koyun
- Key lookup'ları ortadan kaldırmak için INCLUDE kolonları ekleyin
- Missing index DMV'larını sorgulayın ama önerileri körü körüne uygulamayın
- Kullanılmayan indexleri kaldırın — okuma faydası olmadan yazma maliyeti yaratırlar
- Düzenli rebuild/reorganize job'larıyla indexleri bakımlı tutun
- İndekslenen kolonları fonksiyona sararsanız index'i öldürmüş olursunuz
Index sorgunuzu hızlandırmaz. SQL Server'ın işini kolaylaştırır, böylece SQL Server daha hızlı bir plan seçer. Bu ayrımı anlamak, indexleme hakkındaki düşünce biçiminizi değiştirir.