EF Core - Tối ưu Hiệu suất
N+1 Query Problem
Vấn đề
N+1 xảy ra khi bạn load một list objects, sau đó access navigation property của từng object (lazy loading):
// ❌ BAD - N+1 Query
var products = context.Products.ToList(); // 1 query
foreach (var product in products)
{
Console.WriteLine(product.Category.Name); // N queries!
}
// Generated SQL:
// SELECT * FROM Products -- 1 query
// SELECT * FROM Categories WHERE Id = 1 -- N queries!
Giải pháp
1. Eager Loading với Include
// ✅ Sử dụng Include
var products = context.Products
.Include(p => p.Category)
.ToList();
// Generated SQL:
// SELECT p.*, c.* FROM Products p
// LEFT JOIN Categories c ON p.CategoryId = c.Id
2. ThenInclude cho nested relationships
var orders = context.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.ThenInclude(p => p.Category)
.ToList();
3. Explicit Loading
var product = context.Products.First();
// Load Category explicitly
await context.Entry(product)
.Reference(p => p.Category)
.LoadAsync();
// Load Collection explicitly
await context.Entry(product)
.Collection(p => p.Reviews)
.LoadAsync();
4. Projections (Select)
// ✅ Tốt - Chỉ lấy những gì cần
var productDtos = context.Products
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
CategoryName = p.Category.Name
})
.ToList();
// Generated SQL: Chỉ select Id, Name, CategoryName
AsNoTracking
Khi nào sử dụng
// ✅ Sử dụng AsNoTracking() cho read-only queries
var products = context.Products
.AsNoTracking()
.Where(p => p.Price > 100)
.ToList();
// ✅ Hoặc với AsNoTrackingWithIdentityResolution
var products = context.Products
.AsNoTrackingWithIdentityResolution()
.Include(p => p.Category)
.ToList();
So sánh
| Method | Tracking | Identity Resolution | Performance |
|---|---|---|---|
AsNoTracking() | ❌ No | ❌ No | Fastest |
AsTracking() | ✅ Yes | ✅ Yes | Default |
AsNoTrackingWithIdentityResolution() | ❌ No | ✅ Yes | Fast |
Lưu ý
// ❌ AsNoTracking - Không thể save changes
var product = context.Products.AsNoTracking().First();
product.Name = "New Name";
context.SaveChanges(); // Không có gì thay đổi!
// ✅ AsTracking - Có thể save changes
var product = context.Products.First();
product.Name = "New Name";
context.SaveChanges(); // Update thành công
Compiled Queries
Cache query plan
// Đăng ký compiled query
private static readonly Func<AppDbContext, IQueryable<Product>>
GetAllProducts = EF.CompileQuery((AppDbContext ctx) =>
ctx.Products.Include(p => p.Category));
// Sử dụng
using (var context = new AppDbContext())
{
var products = GetAllProducts(context).ToList();
}
Pagination
Skip/Take
var page = 1;
var pageSize = 10;
var products = context.Products
.OrderBy(p => p.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
Batch Size
// Cấu hình batch size
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(
"ConnectionString",
options => options.MaxBatchSize(100));
}
Split Queries
Giải quyết vấn đề cartesian product
// ✅ Sử dụng SplitQuery khi có nhiều collections
var orders = context.Orders
.Include(o => o.OrderItems)
.AsSplitQuery()
.ToList();
// Generated SQL: 2 queries thay vì 1 với cartesian product
Raw SQL Queries
FormattedRawSql
// Raw SQL với parameters
var products = context.Products
.FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)
.ToList();
// Stored Procedure
var products = context.Products
.FromSqlRaw("EXEC GetProducts @CategoryId = {0}", categoryId)
.ToList();
Bulk Operations
Install package
dotnet add package EFCore.BulkExtensions
Bulk Insert
var entities = Enumerable.Range(0, 1000)
.Select(i => new Product { Name = $"Product {i}", Price = i })
.ToList();
context.BulkInsert(entities);
Bulk Update
context.BulkUpdate(entities);
Bulk Delete
context.BulkDelete(entitiesToDelete);