Entity Framework Core giúp lập trình viên làm việc với cơ sở dữ liệu bằng C# thuận tiện hơn, nhưng truy vấn dễ viết không đồng nghĩa với truy vấn luôn hiệu quả. Khi dữ liệu tăng, một đoạn LINQ tưởng như đơn giản có thể lấy thừa hàng nghìn cột, tạo nhiều câu SQL hoặc giữ quá nhiều đối tượng trong bộ nhớ.
Dưới đây là 10 kỹ thuật tối ưu Entity Framework Core có thể áp dụng cho API, website thương mại điện tử và hệ thống quản trị. Nguyên tắc chung là đo trước, lấy đúng dữ liệu cần dùng và kiểm tra SQL thực sự được tạo.
Nếu trang danh sách chỉ cần mã, tên và giá, không nên tải toàn bộ entity cùng các trường mô tả dài. Dùng Select để chiếu dữ liệu sang DTO giúp giảm lượng dữ liệu truyền, bộ nhớ và chi phí theo dõi.
var products = await db.Products .Where(x => x.IsPublished) .OrderByDescending(x => x.Id) .Select(x => new ProductListItem { Id = x.Id, Name = x.Name, Price = x.Price }) .Take(20) .ToListAsync(cancellationToken);Projection đặc biệt hữu ích khi entity có trường nội dung HTML, ảnh lớn hoặc nhiều navigation property.
Mặc định, EF Core theo dõi entity để phát hiện thay đổi khi gọi SaveChanges. Với màn hình chỉ đọc, theo dõi này thường không cần thiết. AsNoTracking giảm chi phí tạo snapshot và quản lý change tracker.
var article = await db.Articles .AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);Không nên áp dụng máy móc cho luồng sẽ sửa entity ngay sau đó. Khi cần cập nhật, tracking có thể đơn giản và phù hợp hơn.
N+1 xảy ra khi hệ thống chạy một truy vấn lấy danh sách cha rồi chạy thêm một truy vấn cho từng phần tử để lấy dữ liệu con. Với 100 sản phẩm, ứng dụng có thể gửi 101 truy vấn thay vì một hoặc vài truy vấn có chủ đích.
Cách phòng tránh:
Include khi thực sự cần entity liên quan.Không gọi ToListAsync trước rồi mới phân trang trong bộ nhớ. Hãy áp dụng Where, OrderBy, Skip và Take trước khi thực thi truy vấn.
var pageItems = await query .OrderByDescending(x => x.CreatedAt) .ThenByDescending(x => x.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken);Với tập dữ liệu rất lớn và người dùng duyệt sâu, keyset pagination thường hiệu quả hơn offset pagination. Thay vì bỏ qua hàng trăm nghìn bản ghi, truy vấn tiếp tục từ khóa cuối của trang trước.
EF Core không thể bù cho việc thiếu index. Nếu truy vấn thường lọc theo StoreId, IsPublished và sắp xếp theo CreatedAt, hãy xem execution plan và cân nhắc composite index theo đúng mẫu sử dụng.
Index cũng có chi phí: tăng dung lượng và làm thao tác ghi chậm hơn. Không nên tạo index cho mọi cột. Hãy dựa trên truy vấn thường xuyên, độ chọn lọc của dữ liệu và kế hoạch thực thi.
Endpoint tìm kiếm hoặc autocomplete cần giới hạn rõ ràng. Một truy vấn thiếu Take có thể trả toàn bộ bảng khi từ khóa rỗng hoặc bộ lọc bị bỏ qua. Giới hạn giúp bảo vệ cả cơ sở dữ liệu, mạng và bộ nhớ ứng dụng.
var suggestions = await db.Products .AsNoTracking() .Where(x => x.Name.StartsWith(keyword)) .OrderBy(x => x.Name) .Select(x => new { x.Id, x.Name }) .Take(10) .ToListAsync(cancellationToken);Nhiều collection được join trong một truy vấn có thể tạo phép nhân số dòng, còn gọi là cartesian explosion. Dữ liệu cha bị lặp lại nhiều lần, làm tăng kích thước kết quả.
Có thể cân nhắc AsSplitQuery để tách thành nhiều truy vấn. Cách này giảm dữ liệu lặp nhưng tăng số lần trao đổi với cơ sở dữ liệu. Không có lựa chọn luôn tốt hơn, vì vậy cần đo với kích thước dữ liệu thật.
IQueryable cho phép EF Core ghép điều kiện trước khi tạo SQL. Nếu gọi ToList quá sớm, các bước lọc sau đó chạy trong bộ nhớ và toàn bộ dữ liệu trung gian đã được tải.
IQueryable query = db.Products.AsNoTracking(); if (categoryId.HasValue) query = query.Where(x => x.CategoryId == categoryId.Value); if (!string.IsNullOrWhiteSpace(keyword)) query = query.Where(x => x.Name.Contains(keyword)); var items = await query .OrderBy(x => x.Name) .Take(50) .ToListAsync(cancellationToken); EF Core cache hình dạng truy vấn, nhưng vẫn có chi phí xử lý biểu thức trước khi thực thi. Với truy vấn ngắn, chạy rất thường xuyên và có cấu trúc cố định, compiled query có thể giảm thêm overhead. Đây là tối ưu nâng cao và chỉ nên dùng sau khi đo cho thấy bước biên dịch truy vấn là đáng kể.
Không tối ưu LINQ bằng cảm giác. Hãy xem SQL bằng ToQueryString trong quá trình phát triển, bật log truy vấn chậm và theo dõi số lượng query theo request. Ở phía cơ sở dữ liệu, kiểm tra execution plan, index được dùng, số dòng đọc và thời gian CPU.
Một truy vấn nhanh trên dữ liệu thử nghiệm vài trăm dòng có thể trở thành nút thắt khi production có hàng triệu dòng. Dữ liệu kiểm thử hiệu năng cần đủ lớn và có phân bố gần với thực tế.
Count khi chỉ cần biết có dữ liệu, thay vì Any.AsNoTracking chưa?CancellationToken tới phương thức bất đồng bộ không?Tối ưu EF Core hiệu quả bắt đầu bằng việc giảm dữ liệu dư thừa và số lần trao đổi với cơ sở dữ liệu. Projection, AsNoTracking, phân trang, index phù hợp và kiểm soát N+1 thường mang lại tác động lớn hơn các vi tối ưu phức tạp. Quan trọng nhất, hãy quan sát SQL và số liệu production để sửa đúng truy vấn đang gây chậm.
Nguồn tham khảo:Efficient Querying trong EF Core.
Cập nhật tin tức nhanh nhất và mới nhất