Photo by Mathew Schwartz on Unsplash

EF Core Compiled Query ve Performans Etkisi

4 min readDec 30, 2024

--

Entity Framework Core veri tabanı işlemlerini gerçekleştirmek için kullanılan bir ORM aracıdır. Bu araç ile veri tabanı üzerinde yaptığımız işlemlerde performans önemli bir metrik haline gelmektedir. Sorgu performansını optimize etmek için EF Core, “compiled query” adı verilen bir özellik sağlar. Bu özellik, LINQ sorgularını verimli bir hale getirmek için tasarlanmıştır. Bu yazıda, bu özelliği derinlemesine analiz etmeye çalışacağım.

Testlerimi yaparken kullandığım modeli açıklayayım.

Customer isimli bir varlığımız var. Bu entity üzerinde çeşitli sorgular çalıştırarak performansı izlemeyi düşünüyorum.


public class Customer(string name, string lastName, int age, DateTime creationTime)
{
public int Id { get; set; }

public string Name { get; set; } = name;

public string LastName { get; set; } = lastName;

public int Age { get; set; } = age;

public DateTime CreationTime { get; set; } = creationTime;
}

Compiled Query

Bir LINQ sorgusu çalıştırdığımızda EF Core, sorguyu veritabanında yürütülebilen geçerli bir SQL’e dönüştürmeden önce sorguyu derlemesi gerekir. Derlenen sorgu önbelleğe alınır. Ardından EF Core derlenen bir sorguyu çalıştırır ve sonuçları geri döner.

Compiled Query, LINQ ifadelerini önceden derleyip parametrik bir SQL cümleceğini dönüştürür. Çalışma zamanında tekrar tekrar derlenmeyen bu sorgular performans açısından bir avantaj sağlar.

Compiled Query yalnızca sorguların önceden derlenmesini ve parametre desteğiyle cachelendiğini unutmamak gerekiyor. Yani veri tabanına gidiş-dönüş süresi ve veritabanında gerçekleşen işlemleri etkilemez.

Compiled Query Nasıl Oluşturulur?

Yukarıda tanımlanan Customer modelini kullanarak Id ile kayıtları sorgulayan bir LINQ sorgusu hazırlayalım.

public async Task<Customer?> GetCustomerNormalAsync(int id)
{
var context = new MyDbContext();
return await context.Customers.FirstOrDefaultAsync(x=>x.Id == id);
}

Şimdi bu sorguyu Compiled Query haline dönüştürelim.

  • EF.CompileQuery kullanarak bir sorgu oluşturun
  • Oluşturulan bu sorguyu static bir değişkene atayın
  • Oluşturulan bu sorguyu execute edin
public static Func<MyDbContext, int, Customer?> s_GetCustomer = EF.CompileQuery((MyDbContext context, int id) => context.Customers.FirstOrDefault(x=>x.Id == id));

public Customer? GetCustomer(int id)
{
var context = new MyDbContext();
return s_GetCustomer(context,id);
}

Compiled Query asenkron çalıştırmayı da destekler. Veri tabanı sorgularının genellikle asenkron olarak yürütmek isteriz.

public static Func<MyDbContext, int, Task<Customer?>> s_GetCustomer_Async = EF.CompileAsyncQuery((MyDbContext context, int id) => context.Customers.FirstOrDefault(x=>x.Id == id));

public async Task<Customer?> GetCustomerAsync(int id)
{
var context = new MyDbContext();
return await s_GetCustomer_Async(context, id);
}

Compiled Query yazmayı ve nasıl çalıştırılacağını öğrendik. Şimdi bazı sorgular yazarak performansını ölçümlemek istiyorum. Bunun için;

  • BenchmarkDotnet
  • PostgreSQL
  • 10K data

kullanarak testlerimi yapacağım.

public class Operation
{
public static Func<MyDbContext, int, Task<Customer?>> s_GetCustomer_Async = EF.CompileAsyncQuery((MyDbContext context, int id) => context.Customers.FirstOrDefault(x=>x.Id == id));

[Benchmark]
public async Task<Customer?> GetCustomerWithCompiledQueryAsync()
{
var context = new MyDbContext();
return await s_GetCustomer_Async(context, 15);
}

[Benchmark]
public async Task<Customer?> GetCustomerWithoutCompiledQueryAsync()
{
var context = new MyDbContext();
return await context.Customers.FirstOrDefaultAsync(x=> x.Id == 15);
}
}
| Method                               | Mean     | Error   | StdDev  |
|------------------------------------- |---------:|--------:|--------:|
| GetCustomerWithoutCompiledQueryAsync | 194.7 us | 1.01 us | 0.94 us |
| GetCustomerWithCompiledQueryAsync | 219.8 us | 1.01 us | 0.90 us |

Bu test sonucunda yaklaşık %10'luk bir performans artışı sağlandı. Harika! 🥳

Sorguyu biraz daha fazla parametre ile besleyelim. Sorgumuz artık iki alanı destekler, id ve name.

public class Operation
{
public static Func<MyDbContext, int, string,Customer?> s_GetCustomer = EF.CompileQuery((MyDbContext context, int id, string name) => context.Customers.FirstOrDefault(x=>x.Id == id && x.Name == name)); [Benchmark]

[Benchmark]
public async Task<Customer?> GetCustomerWithCompiledQueryAsync()
{
var context = new MyDbContext();
return await s_GetCustomer_Async(context, 15, "furkan");
}

[Benchmark]
public async Task<Customer?> GetCustomerWithoutCompiledQueryAsync()
{
var context = new MyDbContext();
return await context.Customers.FirstOrDefaultAsync(x=> x.Id == 15 && x.Name == "furkan");
}
}
| Method                               | Mean     | Error   | StdDev  |
|------------------------------------- |---------:|--------:|--------:|
| GetCustomerWithoutCompiledQueryAsync | 219.3 us | 1.01 us | 0.94 us |
| GetCustomerWithCompiledQueryAsync | 179.0 us | 1.01 us | 0.90 us |

Sonuçlar hemen hemen aynı, ortalama olarak compiled query bu senaryoda daha hızlı çalışıyor. 🚀

Testler şu ana kadar tutarlı. İyileşme yüzdesi beklendiği kadar olmasa da fena değil.

Çoğu performans sorunu, yalnızca bir satır alan sorgular için daha az ortaya çıkar. Listeler gibi büyük miktarda veriyi manipüle ederken daha çok rastlanır. Şimdi listeler üzerinde çalışalım.

public class Operation
{
public static Func<MyDbContext, int, Task<List<Customer>>> s_GetCustomer_Age_Async = EF.CompileAsyncQuery((MyDbContext context, int age) => context.Customers.Where(x=>x.Age > age).ToList());

[Benchmark]
public async Task<List<Customer>> GetCustomerWithoutCompiledQueryAsync()
{
var context = new MyDbContext();
return await context.Customers.Where(x=> x.Age > 20).ToListAsync();
}

[Benchmark]
public async Task<List<Customer>> GetCustomerWithCompiledQueryAsync()
{
var context = new MyDbContext();
return await s_GetCustomer_Age_Async(context, 20);
}
}
| Method                               | Mean     | Error   | StdDev  |
|------------------------------------- |---------:|--------:|--------:|
| GetCustomerWithoutCompiledQueryAsync | 457.3 ms | 1.01 ms | 0.94 ms |
| GetCustomerWithCompiledQueryAsync | 439.5 ms | 1.01 ms | 0.90 ms |

İyileşme yüzdesi oldukça düştü ama hala avantajlı.

Sorgularımızı biraz daha karmaşıklaştıralım.

Compiled Query EF Core optimizasyonu için mucizevi bir çözüm değildir. Evet performans olarak pozitif bir katkı sağlar ancak benim açımdan micro optimizasyon gibi görünüyor. Aşağıdaki koşullarda bence kullanılabilir;

  • Kullanılan parametrelerden bağımsız olarak sorgu birden fazla kez yürütülüyorsa
  • Tek row alınan durumlarda kullanılması mantıklıdır çünkü bu senaryoda sorgunun derlenmesi performans yüzdesi olarak önemli bir yer kapsar
  • CompiledQuery ile çalışmayan IEnumerable<T>.Contains() gibi bir LINQ özelliği kullanmıyorsanız

Kullandığınız sorgu ne kadar çok sonuca sahip olursa, compiled query etkisi o kadar az olur ve bu şaşırtıcı değildir. Derleme, bir sorgunun genel yapısının yalnızca küçük bir parçasıdır. Parça etkisi yüzdesel olarak ne kadar büyükse compiled query etkisi o kadar fazla olacaktır.

Yani bir sorgunun büyük miktarda veri ve özellikle listeler alıyorsa, performansa olan etki sorgunun süresinin çoğunu alır.

Eğer sorgu performansını arttırabiliyorsanız, derlemeyi optimize etmek yerine sorguyu optimize etmek daha kesin sonuçlar verecektir.

--

--

Furkan Güngör
Furkan Güngör

Written by Furkan Güngör

Solution Developer — I want to change the world, give me the source code.

No responses yet