Tối ưu Entity Framework Core: 10 cách truy vấn nhanh và nhẹ hơn

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.

1. Chỉ lấy cột cần thiết bằng projection

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.

2. Dùng AsNoTracking cho truy vấn chỉ đọc

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.

3. Tránh lỗi N+1 query

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:

  • Dùng projection để lấy trực tiếp dữ liệu cần hiển thị.
  • Dùng Include khi thực sự cần entity liên quan.
  • Tắt hoặc kiểm soát lazy loading ở khu vực nhạy cảm về hiệu năng.
  • Bật log SQL trong môi trường phát triển để thấy số truy vấn.

4. Phân trang tại cơ sở dữ liệu

Không gọi ToListAsync trước rồi mới phân trang trong bộ nhớ. Hãy áp dụng Where, OrderBy, SkipTake 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.

5. Tạo index theo mẫu truy vấn thật

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.

6. Giới hạn số dòng ngay cả khi không phân trang

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

7. Cẩn thận với Include nhiều collection

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.

8. Không thực thi truy vấn quá sớm

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

9. Dùng truy vấn biên dịch cho điểm nóng phù hợp

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ể.

10. Đo SQL và thời gian thực thi

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ế.

Các lỗi thiết kế thường gặp

  • Trả entity trực tiếp từ API và vô tình serialize cả navigation property.
  • Dùng Count khi chỉ cần biết có dữ liệu, thay vì Any.
  • Gọi phương thức C# mà nhà cung cấp cơ sở dữ liệu không thể dịch hiệu quả.
  • Chạy nhiều truy vấn tuần tự dù chúng độc lập, nhưng lại dùng chung một DbContext không hỗ trợ thao tác song song.
  • Cache kết quả nhưng cache key thiếu tenant hoặc bộ lọc.
  • Dùng một truy vấn khổng lồ để giải quyết mọi màn hình.

Checklist trước khi đưa truy vấn lên production

  1. Truy vấn có lấy đúng các cột cần dùng không?
  2. Truy vấn chỉ đọc đã dùng AsNoTracking chưa?
  3. Có giới hạn số dòng và sắp xếp ổn định không?
  4. Số câu SQL trên mỗi request có nằm trong dự kiến không?
  5. Các điều kiện lọc chính có index phù hợp không?
  6. Đã thử với dữ liệu đủ lớn chưa?
  7. Có truyền CancellationToken tới phương thức bất đồng bộ không?

Kết luận

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.

Tin nổi bật

Cập nhật tin tức nhanh nhất và mới nhất