Entity Framework Core: Tối Ưu Hiệu Năng và Các Phương Pháp Tốt Nhất Năm 2026
Hướng dẫn toàn diện về tối ưu hiệu năng Entity Framework Core 10 trên .NET 10. Tìm hiểu AsNoTracking, compiled queries, batch updates, split queries và toán tử LeftJoin.

Entity Framework Core 10, được phát hành cùng .NET 10 LTS vào tháng 11 năm 2025, giới thiệu toán tử LeftJoin, tìm kiếm vector, named query filters và nhiều cải tiến đáng kể trong việc dịch SQL. Bài viết này trình bày các phương pháp tốt nhất của EF Core có tác động trực tiếp đến tốc độ truy vấn, phân bổ bộ nhớ và khả năng mở rộng trong môi trường production.
EF Core 10 là phiên bản Long-Term Support, được hỗ trợ đến tháng 11 năm 2028. Framework này yêu cầu SDK và runtime .NET 10. Các ứng dụng vẫn đang sử dụng .NET 8 nên nhắm đến EF Core 8 (LTS) cho đến khi quá trình di chuyển hoàn tất.
Query Tracking: Khi Nào Cần Tắt và Tại Sao
Mỗi lần gọi DbSet<T> theo mặc định sẽ đính kèm các entity được trả về vào change tracker. Tracker này duy trì một bản snapshot các giá trị thuộc tính gốc, tính toán sự khác biệt khi SaveChanges, và giải quyết xung đột danh tính giữa các navigation. Chi phí đó là không cần thiết khi dữ liệu được chuyển trực tiếp đến API response hoặc view model chỉ đọc.
public async Task<List<ProductDto>> GetActiveByCategoryAsync(
int categoryId, CancellationToken ct)
{
return await _context.Products
.AsNoTracking() // bỏ qua change tracker hoàn toàn
.Where(p => p.CategoryId == categoryId && p.IsActive)
.OrderBy(p => p.Name)
.Select(p => new ProductDto // chiếu sang DTO ngay tại tầng database
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync(ct);
}AsNoTracking loại bỏ chi phí tạo snapshot và phân giải danh tính cho mỗi entity. Kết hợp với Select, chỉ các cột cần thiết mới được truyền qua mạng. Trên bảng có 50.000 dòng, mẫu này thường giảm phân bổ bộ nhớ từ 40-60% so với truy vấn full-entity có tracking.
Đối với các context không bao giờ thay đổi dữ liệu, hãy thiết lập mặc định khi đăng ký:
builder.Services.AddDbContext<CatalogContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Split Queries để Tránh Bùng Nổ Tích Cartesian
Việc tải một entity với nhiều collection navigation thông qua Include tạo ra một câu lệnh SQL duy nhất với các JOIN. Khi hai hoặc nhiều collection được tải đồng thời, tập kết quả tăng theo tích Cartesian của các collection, nhân bản dữ liệu dòng cha qua mọi tổ hợp.
public async Task<Order?> GetWithDetailsAsync(int orderId, CancellationToken ct)
{
return await _context.Orders
.AsSplitQuery() // một SQL cho mỗi Include
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Include(o => o.Payments)
.Include(o => o.ShippingEvents)
.FirstOrDefaultAsync(o => o.Id == orderId, ct);
}AsSplitQuery chia việc tải thành các câu lệnh SQL riêng biệt cho mỗi navigation. Đánh đổi ở đây là: nhiều round-trip thay vì một, nhưng mỗi tập kết quả giữ kích thước nhỏ và tránh được vấn đề nhân bản dòng. EF Core 10 cũng sửa một lỗi sắp xếp không nhất quán lâu đời trong split queries, đảm bảo thứ tự subquery khớp với truy vấn chính.
Single query vẫn được ưu tiên khi tải một collection navigation hoặc khi độ trễ round-trip cao (gọi database xuyên vùng). Hãy benchmark cả hai chế độ cho mẫu truy cập cụ thể trước khi đưa ra quyết định.
Thao Tác Hàng Loạt với ExecuteUpdate và ExecuteDelete
Quy trình EF truyền thống tải các entity, chỉnh sửa thuộc tính, rồi gọi SaveChanges. Đối với các thao tác hàng loạt ảnh hưởng đến hàng nghìn dòng, cách tiếp cận này tạo ra hàng nghìn instance được theo dõi và các câu lệnh UPDATE riêng lẻ. EF Core 7 giới thiệu ExecuteUpdateAsync và ExecuteDeleteAsync để đẩy thao tác thành một câu lệnh SQL duy nhất. EF Core 10 đơn giản hóa API hơn nữa bằng cách chấp nhận lambda thông thường thay vì expression tree.
public async Task ApplySeasonalDiscountAsync(
int categoryId, decimal discountPercent, CancellationToken ct)
{
var affected = await _context.Products
.Where(p => p.CategoryId == categoryId && p.IsActive)
.ExecuteUpdateAsync(s =>
{
s.SetProperty(p => p.Price, p => p.Price * (1 - discountPercent / 100));
s.SetProperty(p => p.LastModified, DateTime.UtcNow);
}, ct);
// affected = số dòng đã cập nhật
}Câu lệnh này được dịch thành một UPDATE ... SET ... WHERE duy nhất. Không có entity nào được tải vào bộ nhớ. Đối với việc cập nhật 10.000 dòng, thời gian thực thi giảm từ vài giây (cách tiếp cận tracked) xuống còn mili giây.
Mẫu tương tự áp dụng cho việc xóa:
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
await _context.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(ct);
}Sẵn sàng chinh phục phỏng vấn .NET?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Toán Tử LeftJoin trong EF Core 10
Trước EF Core 10, để thực hiện LEFT JOIN cần kết hợp GroupJoin, SelectMany và DefaultIfEmpty theo một mẫu cụ thể mà hầu hết lập trình viên phải tra cứu mỗi lần sử dụng. EF Core 10 bổ sung LeftJoin như một toán tử LINQ hạng nhất.
public async Task<List<EmployeeReportDto>> GetEmployeeDepartmentReportAsync(
CancellationToken ct)
{
return await _context.Employees
.LeftJoin(
_context.Departments,
employee => employee.DepartmentId,
department => department.Id,
(employee, department) => new EmployeeReportDto
{
FullName = employee.FirstName + " " + employee.LastName,
Department = department.Name ?? "Unassigned",
HiredAt = employee.HiredAt
})
.OrderBy(r => r.FullName)
.ToListAsync(ct);
}SQL được tạo ra sử dụng mệnh đề LEFT JOIN tiêu chuẩn. Toán tử RightJoin cũng có sẵn. Cả hai toán tử đều loại bỏ chuỗi ba phương thức dài dòng mà trước đây bắt buộc phải sử dụng.
Named Query Filters cho Lọc Đa Mục Đích
Global query filters bị giới hạn ở một filter duy nhất cho mỗi loại entity kể từ EF Core 2.0. Các ứng dụng triển khai cả soft-delete và multi-tenancy phải gộp các điều kiện thành một biểu thức và không thể tắt chúng một cách có chọn lọc. EF Core 10 giới thiệu named query filters.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Invoice>()
.HasQueryFilter("SoftDelete", i => !i.IsDeleted)
.HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}Các endpoint quản trị có thể tắt filter soft-delete trong khi vẫn giữ nguyên cách ly tenant:
public async Task<List<Invoice>> GetAllIncludingDeletedAsync(CancellationToken ct)
{
return await _context.Invoices
.IgnoreQueryFilters(["SoftDelete"]) // filter tenant vẫn hoạt động
.ToListAsync(ct);
}Điều này loại bỏ nhu cầu tạo các phương thức mở rộng IQueryable tùy chỉnh để thêm điều kiện lọc thủ công.
Khả Năng Phục Hồi Kết Nối và Cấu Hình Pooling
Các lỗi database tạm thời (gián đoạn mạng, failover Azure SQL, cạn kiệt connection pool) gây ra các exception thường làm sập pipeline request. EF Core cung cấp logic retry tích hợp, nhưng cấu hình mặc định yêu cầu thiết lập rõ ràng.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null); // retry trên tất cả lỗi tạm thời
sqlOptions.CommandTimeout(30); // timeout lệnh 30 giây
}));Các tham số pooling trong connection string cũng quan trọng:
Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;Min Pool Size giữ sẵn các kết nối cho lưu lượng đột biến. Max Pool Size giới hạn tổng số kết nối mở để ngăn quá tải database. Giá trị mặc định 100 phù hợp với hầu hết các ứng dụng web, nhưng các dịch vụ throughput cao có thể cần điều chỉnh dựa trên lượng truy vấn đồng thời thực tế.
Retry tự động không hoạt động bên trong các transaction do người dùng khởi tạo. Hãy bọc toàn bộ transaction trong chiến lược retry thủ công sử dụng context.Database.CreateExecutionStrategy().ExecuteAsync(...) để xử lý lỗi tạm thời qua nhiều thao tác.
Chiến Lược Indexing và Phân Tích Query
Các migration EF Core có thể định nghĩa index một cách khai báo, nhưng việc chọn cột nào để đánh index đòi hỏi hiểu rõ các mẫu truy vấn.
modelBuilder.Entity<Order>(entity =>
{
// index tổ hợp cho mẫu truy vấn thường xuyên
entity.HasIndex(o => new { o.CustomerId, o.Status, o.CreatedAt })
.HasDatabaseName("IX_Order_Customer_Status_Date");
// index có bộ lọc chỉ cho đơn hàng đang hoạt động
entity.HasIndex(o => o.Status)
.HasFilter("[Status] <> 'Cancelled'")
.HasDatabaseName("IX_Order_ActiveStatus");
});Các index có bộ lọc giảm kích thước index bằng cách loại trừ các dòng mà truy vấn không bao giờ nhắm đến. Đối với bảng có 80% đơn hàng đã hủy, index có bộ lọc trên trạng thái hoạt động có thể nhỏ hơn 5 lần và quét nhanh hơn.
Để phân tích SQL được tạo ra, hãy bật logging trong môi trường development:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging());Sao chép SQL đã ghi lại vào SQL Server Management Studio và chạy với SET STATISTICS IO ON để kiểm tra logical reads, hoặc sử dụng EXPLAIN ANALYZE trên PostgreSQL. Các gợi ý index bị thiếu từ query plan thường tiết lộ các cơ hội tối ưu có tác động lớn nhất.
Compiled Queries cho Các Đường Dẫn Nóng
Expression tree LINQ được phân tích và dịch sang SQL mỗi lần thực thi. Đối với các truy vấn chạy hàng nghìn lần mỗi phút (ví dụ: tra cứu xác thực, xác thực phiên), chi phí dịch này tích lũy đáng kể. Compiled queries cache bản dịch khi ứng dụng khởi động.
public static class UserQueries
{
// biên dịch một lần, sử dụng lại mỗi lần gọi
public static readonly Func<AppDbContext, string, CancellationToken, Task<UserSession?>>
GetActiveSession = EF.CompileAsyncQuery(
(AppDbContext ctx, string token, CancellationToken ct) =>
ctx.UserSessions
.AsNoTracking()
.FirstOrDefault(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow));
}
// Sử dụng trong middleware
var session = await UserQueries.GetActiveSession(dbContext, bearerToken, ct);Compiled queries bỏ qua hoàn toàn giai đoạn phân tích expression tree. Sự khác biệt về hiệu năng chỉ đo được trên các đường dẫn tần suất cao (hơn 1.000 lần gọi/phút). Đối với các endpoint CRUD tiêu chuẩn, bộ nhớ cache truy vấn tích hợp trong EF Core đã xử lý việc tái sử dụng bản dịch.
Dịch Parameterized Collection trong EF Core 10
Các truy vấn lọc theo danh sách ID là một trong những mẫu phổ biến nhất trong truy cập dữ liệu. EF Core 10 thay đổi chiến lược dịch mặc định cho parameterized collections. Thay vì mã hóa danh sách dưới dạng mảng JSON (EF Core 8-9) hoặc chèn hằng số trực tiếp (EF Core 7 trở về trước), EF Core 10 dịch mỗi giá trị thành một tham số SQL riêng biệt.
// Trước đây (EF Core 8-9): tham số mảng JSON
// @__ids_0='[1,2,3]'
// SELECT ... WHERE Id IN (SELECT value FROM OPENJSON(@__ids_0))
// EF Core 10: tham số riêng lẻ với padding
// SELECT ... WHERE Id IN (@ids1, @ids2, @ids3)
var orderIds = new[] { 101, 205, 389 };
var orders = await _context.Orders
.Where(o => orderIds.Contains(o.Id))
.ToListAsync(ct);Cách tiếp cận mới cung cấp thông tin cardinality cho query planner của database trong khi vẫn tránh được tình trạng phình cache plan thông qua parameter padding. Trong trường hợp cách tiếp cận JSON hoạt động tốt hơn (collection rất lớn), hãy ghi đè hành vi cho từng truy vấn:
var orders = await _context.Orders
.Where(o => EF.Parameter(orderIds).Contains(o.Id)) // buộc chế độ JSON
.ToListAsync(ct);Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Kết Luận
- Sử dụng
AsNoTracking()và phép chiếuSelecttrên mọi truy vấn chỉ đọc để loại bỏ chi phí change tracker - Áp dụng
AsSplitQuery()khi tải nhiều collection navigation để tránh bùng nổ Cartesian - Thay thế mẫu tracked load-modify-save bằng
ExecuteUpdateAsyncvàExecuteDeleteAsynccho các thao tác hàng loạt - Sử dụng toán tử
LeftJointừ EF Core 10 để thay thế chuỗiGroupJoin/SelectMany/DefaultIfEmptydài dòng - Cấu hình named query filters để quản lý soft-delete và multi-tenancy một cách độc lập
- Thiết lập
EnableRetryOnFailurevà điều chỉnh kích thước connection pool cho khả năng phục hồi production - Định nghĩa index tổ hợp và có bộ lọc dựa trên mẫu truy vấn thực tế, không phải phỏng đoán
- Dành compiled queries cho các đường dẫn thực sự nóng vượt quá 1.000 lần thực thi mỗi phút
- Để EF Core 10 xử lý dịch parameterized collection theo mặc định, và chỉ ghi đè khi benchmark chứng minh điều đó là cần thiết
Đọc thêm: module EF Core Advanced trình bày các mẫu này trong bối cảnh phỏng vấn, và hướng dẫn Clean Architecture với .NET minh họa cách cấu trúc các tầng repository và service bao bọc các truy vấn này.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Câu Hỏi Phỏng Vấn C# và .NET: Hướng Dẫn Đầy Đủ 2026
17 câu hỏi phỏng vấn C# và .NET thường gặp nhất. LINQ, async/await, dependency injection, Entity Framework và các phương pháp tốt nhất với đáp án chi tiết.

.NET 8: Xây dựng API với ASP.NET Core
Hướng dẫn đầy đủ về xây dựng REST API chuyên nghiệp với .NET 8 và ASP.NET Core. Controller, Entity Framework Core, validation và các phương pháp tốt nhất được giải thích chi tiết.

Rust: Kiến Thức Nền Tảng Dành Cho Lập Trình Viên Có Kinh Nghiệm Năm 2026
Tiếp cận Rust nhanh chóng dựa trên kiến thức lập trình hiện có. Ownership, borrowing, lifetimes và các pattern thiết yếu được giải thích dành cho lập trình viên đến từ C++, Java hoặc Python.