Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

MethodTrackingIdentity ResolutionPerformance
AsNoTracking()❌ No❌ NoFastest
AsTracking()✅ Yes✅ YesDefault
AsNoTrackingWithIdentityResolution()❌ No✅ YesFast

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);