EF Core'da bir index'i entity sınıfına [Index(nameof(Url))] attribute'u koyarak ya da OnModelCreating içinde HasIndex(b => b.Url) çağırarak tanımlarsınız; tekrar eden değerleri engellemek için attribute'a IsUnique = true ekler ya da Fluent API'de .IsUnique() çağırırsınız. Entity Framework Core index'i migration ile veritabanında oluşturur; ayrıca her foreign key için konvansiyonla bir index açar. Composite index, sıralama yönü, ad, filtre, included columns ve check constraint gibi ayrıntılar da model üzerinden yönetilir.

Bu yazıda index tanımlamanın tüm seçeneklerini, SQL Server'a özgü clustered, fill factor ve online oluşturma ayarlarını ve belgelerin index kullanımı için verdiği performans ipuçlarını ele alıyoruz. Veritabanı tarafında sorgu planı okumak ve index'leri izlemek EF'ten bağımsız bir konudur; PostgreSQL için PostgreSQL performans optimizasyonu yazısına bakabilirsiniz. EF tarafındaki genel performans çerçevesi EF Core performans yazısında, serinin tamamı EF Core öğrenme rehberi sayfasında.

Index tanımlamak: [Index] ve HasIndex

Index tanımlamak: [Index] ve HasIndex

Index'ler, bir kolon ya da kolon grubu üzerinden yapılan aramaları hızlandırmak için kullanılır. Uygulanışları veri deposuna göre değişse de kavram neredeyse her veritabanında ortaktır. Data annotations tarafında [Index] attribute'u sınıfın üzerine konur ve property adlarını string olarak alır; nameof kullanmak yeniden adlandırmalarda hatayı derleme anında yakalar. Fluent API tarafında HasIndex bir lambda alır:

csharp
[Index(nameof(Url))]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

// Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasIndex(b => b.Url);
}

Belgelerin önemli bir notu: konvansiyon gereği foreign key olarak kullanılan her property'ye ya da property grubuna bir index oluşturulur. Bunu ForeignKeyIndexConvention adlı yerleşik konvansiyon yapar. Model belgelerindeki debug görünümü örneğinde, Blog ve Author ile ilişkili bir Post entity'si için AuthorId ve BlogId üzerinde iki index görünür. Index'lerin bir maliyeti olduğu için her FK'ye index istemiyorsanız bu konvansiyonu kaldırabilirsiniz; gerektiğinde istediğiniz FK'lere yine [Index] veya HasIndex ile açıkça index eklersiniz.

csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Konvansiyonu kaldırmadan önce iki tarafı birlikte düşünün. Belgeler FK kolonlarına index açmanın genellikle mantıklı olduğunu, bu yüzden konvansiyonun varsayılan olarak açık geldiğini söyler; öte yandan index'lerin bir maliyeti vardır ve her FK için uygun olmayabilir. Kararı, sorgularınızın o FK'ye göre filtreleyip filtrelemediğine ve tablonun yazma yüküne bakarak verin. Konvansiyonun kaldırılmasından sonra debug görünümünde FK property'lerinin yanındaki Index işaretinin kaybolduğunu görürsünüz. Konvansiyonların genel olarak nasıl kaldırıldığını model yapılandırma yazısında anlattık.

Composite index ve kolon sırasının önemi

Bir index birden çok kolonu kapsayabilir; bunlara composite index denir. Attribute'a birden fazla property adı verirsiniz, Fluent API'de anonim tip kullanırsınız:

csharp
[Index(nameof(FirstName), nameof(LastName))]
public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

modelBuilder.Entity<Person>()
    .HasIndex(p => new { p.FirstName, p.LastName });

Composite index'in en çok gözden kaçan özelliği kolon sırasıdır. Belgelere göre composite index, index'in tüm kolonlarına göre filtreleyen sorguları hızlandırır, ama yalnızca index'in ilk kolonlarına göre filtreleyen sorguları da hızlandırabilir. Performans belgelerindeki örnekle: A ve B kolonları üzerindeki bir index, A ve B'ye göre filtreleyen sorguları ve yalnızca A'ya göre filtreleyen sorguları hızlandırır; ama yalnızca B'ye göre filtreleyen sorguları hızlandırmaz. Bu yüzden composite index'te en sık tek başına filtrelediğiniz kolonu öne koymak gerekir.

Unique index: tekrarı engellemek

Index'ler varsayılan olarak tekil değildir; birden çok satır index kolonlarında aynı değerleri taşıyabilir. IsUnique ile index'i tekil yaparsınız. Bundan sonra index kolonlarında aynı değerlere sahip birden fazla entity eklemeye çalışmak bir istisnaya yol açar. E-posta adresi, kullanıcı adı, stok kodu gibi doğal olarak tekil olması gereken alanlar için tipik çözüm budur.

csharp
[Index(nameof(Url), IsUnique = true)]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

modelBuilder.Entity<Blog>()
    .HasIndex(b => b.Url)
    .IsUnique();

Unique index ile alternate key sık karıştırılır. Belgelerin tavsiyesi açıktır: yalnızca bir kolonda tekilliği zorunlu kılmak istiyorsanız alternate key yerine unique index tanımlayın. Alternate key, foreign key'lerin hedefi olabildiği için ek anlam taşır ve EF'te salt okunurdur. Ayrıntılar primary key ve alternate key yazısında.

Sıralama yönü: artan ve azalan

Çoğu veritabanında index'in kapsadığı her kolon artan ya da azalan sırada tutulabilir. Tek kolonlu index'lerde bunun genellikle önemi yoktur, çünkü veritabanı index'i gerektiğinde ters yönde dolaşabilir. Composite index'lerde ise sıralama, iyi performans için kritik olabilir ve index'in bir sorguda kullanılıp kullanılmamasını belirleyebilir. Belgelerin genel kuralı: index kolonlarının sıralama yönü, sorgunuzun ORDER BY ifadesindeki yönlerle uyuşmalıdır. Varsayılan yön artandır. Tüm kolonları azalan yapabilir ya da kolon kolon belirleyebilirsiniz:

csharp
// Tum kolonlar azalan
[Index(nameof(Url), nameof(Rating), AllDescending = true)]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

modelBuilder.Entity<Blog>()
    .HasIndex(b => new { b.Url, b.Rating })
    .IsDescending();

// Kolon kolon: Url artan, Rating azalan
modelBuilder.Entity<Blog>()
    .HasIndex(b => new { b.Url, b.Rating })
    .IsDescending(false, true);

Attribute tarafında kolon kolon yön için IsDescending = new[] { false, true } yazılır. Sayfalama gibi sabit sıralı sorgularda bu ayar doğrudan işe yarar; keyset sayfalamada sıralama ile index'in uyumunu sayfalama yazısında ele aldık.

Index adı ve aynı kolonlarda birden çok index

Index adı ve aynı kolonlarda birden çok index

İlişkisel veritabanlarında index'ler konvansiyonla IX_<tip adı>_<property adı> biçiminde adlandırılır; composite index'lerde property adları alt çizgiyle birleştirilir. Adı [Index(nameof(Url), Name = "Index_Url")] veya HasDatabaseName("Index_Url") ile değiştirebilirsiniz.

Burada ince bir davranış var. Aynı property grubu için HasIndex'i birden fazla çağırırsanız yeni bir index oluşmaz, aynı index yapılandırılmaya devam eder. Aşağıdaki ilk blokta ikinci çağrı birinciyi ezer ve sonuçta yalnızca tek, azalan bir index oluşur. Bu, konvansiyonla oluşmuş bir index'i (örneğin FK index'ini) sonradan yapılandırmak için kullanışlıdır. Aynı kolonlar üzerinde gerçekten iki ayrı index istiyorsanız HasIndex'e ikinci parametre olarak bir ad verirsiniz; bu ad index'i EF modelinde ayırt eder ve aynı zamanda veritabanındaki varsayılan ad olur, ayrıca HasDatabaseName çağırmanız gerekmez.

csharp
// Tek index olusur: ikinci cagri birinciyi ezer
modelBuilder.Entity<Person>()
    .HasIndex(p => new { p.FirstName, p.LastName })
    .HasDatabaseName("IX_Names_Ascending");

modelBuilder.Entity<Person>()
    .HasIndex(p => new { p.FirstName, p.LastName })
    .HasDatabaseName("IX_Names_Descending")
    .IsDescending();

// Iki ayri index olusur: her birine model adi verilir
modelBuilder.Entity<Person>()
    .HasIndex(p => new { p.FirstName, p.LastName }, "IX_Names_Ascending");

modelBuilder.Entity<Person>()
    .HasIndex(p => new { p.FirstName, p.LastName }, "IX_Names_Descending")
    .IsDescending();

Filtreli index

Bazı ilişkisel veritabanları filtreli (filtered) ya da kısmi (partial) index'i destekler. Böylece kolon değerlerinin yalnızca bir alt kümesini index'lersiniz; index küçülür, hem performans hem disk kullanımı iyileşir. Filtre, Fluent API ile bir SQL ifadesi olarak verilir. Performans belgeleri de index'lerin güncellemeleri yavaşlattığını hatırlatıp bu yükü azaltmak için index filtrelerini önerir.

csharp
modelBuilder.Entity<Blog>()
    .HasIndex(b => b.Url)
    .HasFilter("[Url] IS NOT NULL");

SQL Server sağlayıcısının bir konvansiyonu burada sürpriz yaratabilir: EF, unique bir index'in parçası olan tüm nullable kolonlar için 'IS NOT NULL' filtresini kendisi ekler. Yani nullable bir Url üzerindeki unique index, birden çok NULL değere izin verir; tekillik yalnızca dolu değerler arasında aranır. Bu davranışı istemiyorsanız filtreye null verirsiniz:

csharp
modelBuilder.Entity<Blog>()
    .HasIndex(b => b.Url)
    .IsUnique()
    .HasFilter(null);

Filtrede kullandığınız ifade olduğu gibi veritabanına gider; köşeli parantezli kolon adları SQL Server sözdizimidir, başka bir sağlayıcıya geçtiğinizde ifadeyi o veritabanına göre yazmanız gerekir. Filtreli index'in veritabanı tarafındaki kuralları (hangi ifadelere izin verildiği gibi) veritabanının kendi belgelerinde anlatılır. Uygulama düzeyinde satır filtreleme ise başka bir konudur; soft delete gibi senaryoların EF tarafındaki kurgusu global query filter yazısında anlatılıyor.

Included columns: kaplayan index

Bazı ilişkisel veritabanları, index'e anahtarının parçası olmayan ek kolonlar dahil etmenize izin verir. Sorgudaki tüm kolonlar index'te, ya anahtar ya da dahil edilmiş kolon olarak bulunuyorsa veritabanı tabloya hiç gitmeden yanıt verebilir; bu da sorgu performansını belirgin biçimde artırabilir. Aşağıdaki örnekte Url index anahtarıdır, yani ona göre filtreleyen her sorgu index'i kullanabilir. Ek olarak, yalnızca Title ve PublishedOn kolonlarına erişen sorgular tabloya erişmek zorunda kalmaz.

csharp
modelBuilder.Entity<Post>()
    .HasIndex(p => p.Url)
    .IncludeProperties(
        p => new { p.Title, p.PublishedOn });

Included columns'ın etkisini en iyi, sorgunuzda yalnızca gereken kolonları seçtiğinizde görürsünüz. Tüm entity'yi yükleyen bir sorgu zaten tablodaki her kolona ihtiyaç duyar. Yalnızca gereken property'leri projeksiyonla almak performans yazısının ana konularından biridir.

Check constraint

Check constraint, tablodaki her satırın sağlaması gereken bir koşul tanımlamanızı sağlayan standart bir ilişkisel özelliktir; koşulu ihlal eden ekleme veya güncelleme başarısız olur. NOT NULL ve unique constraint'lere benzer, ama keyfi bir SQL ifadesi alabilir. Fluent API ile tabloya eklenir ve aynı tabloda her biri kendi adıyla birden çok check constraint tanımlanabilir. Belgeler, sık kullanılan bazı check constraint'lerin topluluk tarafından geliştirilen EFCore.CheckConstraints paketiyle yapılandırılabildiğini de not eder.

csharp
modelBuilder
    .Entity<Product>()
    .ToTable(b => b.HasCheckConstraint("CK_Prices", "[Price] > [DiscountedPrice]"));

SQL Server'a özgü index ayarları

SQL Server sağlayıcısı üç ek ayar sunar. Clustered index, tablodaki satırları anahtar değerlerine göre sıralayıp saklar; satırlar tek bir sırada saklanabildiği için tablo başına yalnızca bir clustered index olabilir. Varsayılan olarak birincil anahtar clustered index ile desteklenir, diğer tüm index'ler non-clustered'dır. Anahtar olmayan bir kolonda clustered index istiyorsanız anahtarınızı açıkça non-clustered yapmanız gerekir. Fill factor, index veri depolamasını ve performansını ince ayarlamak için kullanılır. Online oluşturma (ONLINE seçeneği), index oluşturulurken kullanıcıların tabloyu sorgulamaya ve güncellemeye devam etmesine izin verir; çevrimdışı DDL işlemleri ise alttaki veri ve index'ler üzerinde özel kilit tutar. Canlı sistemde büyük bir tabloya index eklerken bu seçenek önem kazanır.

csharp
modelBuilder.Entity<Blog>().HasIndex(b => b.PublishedOn).IsClustered();

modelBuilder.Entity<Blog>().HasIndex(b => b.PublishedOn).HasFillFactor(10);

modelBuilder.Entity<Blog>().HasIndex(b => b.PublishedOn).IsCreatedOnline();

Index kullanımı için belgelerdeki performans ipuçları

Index kullanımı için belgelerdeki performans ipuçları: Index'ler sorguları hızlandırır ama güncellemeleri yavaşlatır, çünkü her..., Composite index, kolon sırasına bağlı olarak...

Performans belgelerine göre bir sorgunun hızlı çalışıp çalışmayacağını belirleyen ana etken, uygun index'leri doğru kullanıp kullanmadığıdır. Sorun şu ki bir sorgunun index kullanıp kullanmayacağı her zaman gözle anlaşılmaz. Belgelerdeki örnekte Title.StartsWith("A") başa göre eşleştiği için SQL Server'da index kullanır, Title.EndsWith("A") ise kullanmaz. Yavaş bir sorguyu bulup veritabanının kendi aracıyla sorgu planını incelemek, tablonun baştan sona taranıp taranmadığını gösterir; sorgu planı incelemesinin EF tarafı SQL loglama yazısında başlar.

csharp
// Basa gore eslesir, index kullanir (SQL Server)
var posts1 = await context.Posts.Where(p => p.Title.StartsWith("A")).ToListAsync();
// Sona gore eslesir, index kullanmaz
var posts2 = await context.Posts.Where(p => p.Title.EndsWith("A")).ToListAsync();
  • Index'ler sorguları hızlandırır ama güncellemeleri yavaşlatır, çünkü her yazmada güncel tutulmaları gerekir. Gereksiz index tanımlamayın; gerekirse filtreli index ile yükü azaltın.
  • Composite index, kolon sırasına bağlı olarak tüm kolonlara değil yalnızca ilk kolonlara göre filtreleyen sorguları da hızlandırabilir; sondaki kolona göre tek başına filtrelemeyi hızlandırmaz.
  • Bir kolon üzerindeki ifadeyle filtreleme yapılıyorsa (ör. price / 2) basit bir index kullanılamaz; ifade için kalıcı bir hesaplanan kolon tanımlayıp ona index koyabilirsiniz (computed column). Bazı veritabanları ifade index'lerini de destekler.
  • Index'lerin yapılandırma seçenekleri veritabanına göre değişir ve sağlayıcılar bunları çoğu zaman Fluent API ile açar; sağlayıcınızın belgelerine bakın.

Geliştirme aşamasındaki EF Core 11: complex type ve JSON yolları

Yerel belgeler, geliştirme aşamasındaki EF Core 11.0 için index'lerin complex type içindeki skaler property'leri kullanabileceğini anlatır: HasIndex(c => c.Address.PostalCode), normal property'lerle karışık composite index'ler ve adla yazılan HasIndex("Address.PostalCode"). JSON kolonlarına eşlenmiş complex type'larda sağlayıcılar JSON belgesinin içindeki yolları index'lemeyi destekleyebilir; complex koleksiyonlar için "Items[].Sku" gibi yol sözdizimi tanımlanmıştır. Belgelere göre JSON yolu index'lerinin desteği sağlayıcıya bağlıdır. Bu özellikler yayımlanmış EF Core 10'da bulunmaz; EF Core 11 yayımlanana kadar değişebilir. Complex type'ların bugünkü kullanımı için complex types yazısına bakın.

Sık yapılan hatalar

  • Composite index'te kolon sırasını önemsememek. Yalnızca ikinci kolona göre filtreleyen sorgu index'ten yararlanmaz.
  • Aynı kolonlara iki HasIndex yazıp iki index beklemek. Ad parametresi vermezseniz ikinci çağrı birinciyi yapılandırır.
  • SQL Server'da nullable kolondaki unique index'in birden çok NULL'a izin vermesine şaşırmak. EF, IS NOT NULL filtresini kendisi ekler; istemiyorsanız HasFilter(null) kullanın.
  • Her alana index eklemek. Index'ler yazmaları yavaşlatır; yalnızca gerçekten kullanılan sorgular için ekleyin.
  • Index'i modele ekleyip migration'ı unutmak. HasIndex yalnızca modeli değiştirir; index veritabanında migration ile oluşur.
  • Filtre ifadesini sağlayıcıdan bağımsız sanmak. HasFilter ve HasCheckConstraint içindeki SQL olduğu gibi veritabanına gider.

Sık Sorulan Sorular

Foreign key kolonlarına ayrıca index eklemem gerekir mi?

Hayır, konvansiyon her foreign key için zaten bir index oluşturur. Bunu istemiyorsanız ForeignKeyIndexConvention'ı kaldırabilir, sadece istediğiniz FK'lere açıkça index ekleyebilirsiniz.

Index adını neden değiştirmeliyim?

Konvansiyonla verilen IX_Tip_Property adı çoğu proje için yeterlidir. Kurumsal adlandırma standardınız varsa, mevcut bir veritabanıyla uyumlu kalmanız gerekiyorsa ya da aynı kolonlarda birden çok index tanımlıyorsanız ad vermek gerekir.

Unique index mi, alternate key mi?

Yalnızca tekillik istiyorsanız unique index. Değer başka bir tablonun foreign key'inin hedefi olacaksa alternate key.

Index eklemek için migration gerekir mi?

Evet. HasIndex modeli değiştirir; değişikliğin veritabanına yansıması için yeni bir migration ekleyip uygulamanız gerekir. Canlı sistemde uygulama yöntemleri production migration yazısında anlatılıyor.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: EF Core öğrenme rehberi · Primary key ve alternate key · EF Core performans optimizasyonu · Sayfalama: Skip/Take ve keyset · Computed column · PostgreSQL performans optimizasyonu