Ứng dụng .NET chạy mượt lúc đầu nhưng chậm dần khi dữ liệu tăng
Chúng tôi từng tiếp nhận một hệ thống quản lý bán hàng viết bằng ASP.NET Core. Ngày mới triển khai, bảng dữ liệu chỉ có vài trăm bản ghi và thao tác nào cũng nhanh. Sau khoảng nửa năm vận hành, số dòng đã lên tới hàng trăm nghìn. Một số trang danh sách bắt đầu mất vài giây mới hiển thị xong, trong khi phần code gần như không đổi.
Bạn có thể đã gặp tình huống này. Vấn đề hiệu năng truy vấn thường chỉ lộ ra khi dữ liệu đạt một quy mô nhất định. Lúc dự án còn nhỏ, mọi thứ vẫn chạy ổn nên rất dễ chủ quan. Đến khi người dùng than phiền, việc tìm nguyên nhân lại tốn thêm nhiều thời gian hơn.
Khi mới làm quen với Entity Framework, phần lớn chúng ta chỉ quan tâm làm sao cho chức năng chạy đúng. Ít ai dừng lại xem LINQ được dịch thành câu SQL như thế nào. EF rất tiện, nhưng sự tiện đó dễ khiến ta quên rằng mỗi dòng LINQ đều có chi phí thực thi. Viết thiếu cẩn thận thì chi phí ấy sẽ lớn dần theo dữ liệu.
Tối ưu hiệu năng ứng dụng .NET ở lớp truy vấn dữ liệu
Khi rà lại các dự án EF bị chậm dần, chúng tôi thường thấy một nguyên nhân lặp đi lặp lại: truy vấn kéo về quá nhiều dữ liệu. Lỗi điển hình nhất là gọi ToList() quá sớm rồi mới lọc bằng LINQ trên bộ nhớ. Khi đó, điều kiện lọc không được chuyển xuống mệnh đề WHERE của SQL. Database phải trả về toàn bộ bản ghi, còn ứng dụng tự loại bỏ phần thừa.
Chỉ cần đưa Where lên trước ToList() là câu SQL sinh ra đã gọn hơn nhiều. Bạn nên kiểm tra thói quen này trong các hàm lấy danh sách có phân trang. Nếu thấy ToList() xuất hiện giữa chuỗi truy vấn, hãy tự hỏi liệu có thể dời nó xuống cuối hay không.
Một vấn đề khác cũng rất hay gặp là lấy toàn bộ cột trong khi giao diện chỉ hiển thị vài trường. Lượng dữ liệu đi qua mạng tăng lên mà không mang lại giá trị gì. Cách xử lý khá đơn giản: dùng Select để chiếu sang một DTO chỉ chứa các trường cần thiết. Với những bảng có nhiều cột, bạn sẽ thấy thời gian phản hồi giảm rõ rệt.
Nếu muốn nhìn rộng hơn về các điểm hiệu năng dễ bị bỏ qua trong ứng dụng .NET, bạn có thể đọc thêm những điểm tối ưu hiệu năng mà dev hay bỏ qua. Bài đó đi xa hơn phạm vi riêng của Entity Framework.
Thiếu index và cách load quan hệ không phù hợp
Thiếu index là nguyên nhân khiến truy vấn chậm dần mà nhiều người không để ý. Những cột thường xuyên dùng để lọc hoặc sắp xếp mà chưa có index thì database buộc phải quét toàn bộ bảng. Khi bảng còn ít dòng, việc quét này vẫn nhanh nên không ai thấy vấn đề. Đến khi bảng lên vài triệu dòng, cùng một truy vấn có thể chậm hẳn đi.
Cách load quan hệ cũng ảnh hưởng lớn. Eager loading kéo theo mọi dữ liệu liên quan, kể cả những thứ bạn không dùng đến. Lazy loading thì dễ dẫn đến lỗi N+1: một truy vấn lấy danh sách, sau đó mỗi phần tử trong vòng lặp lại phát sinh thêm một truy vấn. Chúng tôi từng gặp trường hợp một trang báo cáo gửi hơn bốn trăm câu SQL xuống database chỉ để hiển thị một danh sách, trong khi code nhìn qua tưởng rất gọn.
Bảng dưới đây tóm tắt ba lỗi phổ biến nhất và ảnh hưởng của chúng:
| Vấn đề | Ảnh hưởng đến hiệu năng |
|---|---|
| Gọi ToList() quá sớm | Tải thừa dữ liệu, lọc trên bộ nhớ thay vì trên database |
| Thiếu index trên cột lọc | Quét toàn bộ bảng thay vì tra cứu nhanh qua index |
| N+1 query do lazy loading | Số truy vấn tăng theo số bản ghi trong vòng lặp |
Công cụ giúp phát hiện truy vấn chậm mà không cần dò từng dòng code
Thay vì đọc lại toàn bộ codebase để đoán đoạn nào gây chậm, chúng tôi khuyên bạn bật logging cho câu SQL mà Entity Framework sinh ra ở môi trường phát triển. Chỉ cần nhìn log vài phút, bạn đã thấy câu nào đang chạy lâu hoặc bị lặp bất thường. Đây là cách nhanh nhất để khoanh vùng vấn đề.
Ở môi trường production, các công cụ giám sát hiệu năng cho phép theo dõi thời gian thực thi từng truy vấn. Điều này đặc biệt hữu ích với những truy vấn có xu hướng chậm dần theo thời gian. Phát hiện sớm nghĩa là bạn xử lý trước khi người dùng cảm nhận được sự khác biệt.
Ngoài ra, bạn nên đưa bước kiểm tra truy vấn vào quy trình review code trước khi merge. Khi reviewer hỏi thêm một câu “câu SQL này sinh ra có ổn không?”, nhiều vấn đề được chặn lại ngay từ đầu. Nếu nhóm bạn cần thêm tiêu chí kiểm tra, có thể tham khảo best practices cho backend developer khi review code.
Thói quen viết truy vấn nên có ngay từ đầu dự án
Theo kinh nghiệm của chúng tôi, thói quen quan trọng nhất là kiểm tra câu SQL thực tế mỗi khi viết một truy vấn LINQ mới. Đừng tin rằng EF sẽ tự tối ưu mọi thứ. Viết xong, bật log lên và xem nó sinh ra những gì. Mất thêm vài phút lúc viết có thể tiết kiệm nhiều ngày khi hệ thống đã lên production.
Tiếp theo, hãy đánh index cho các cột thường được lọc hoặc sắp xếp. Chỉ chọn đúng những trường cần dùng thay vì tải nguyên đối tượng. Và hiểu rõ cách load quan hệ ngay từ những dự án đầu tiên. Ba việc đơn giản này giúp bạn tránh phần lớn tình trạng chậm dần khi dữ liệu tăng.
Nếu bạn vẫn đang làm quen với hệ sinh thái .NET, hãy đọc thêm .NET là gì và các tài liệu học .NET Framework hữu ích để có nền tảng vững trước khi đi sâu vào tối ưu. Còn nếu muốn tìm hiểu thêm các lỗi thường gặp khi dùng EF, bạn có thể xem top 10 sai lầm thường gặp về Entity Framework. Để kiểm tra thêm cách EF xử lý truy vấn ở tầng dữ liệu, bạn cũng có thể tham khảo tài liệu chính thức của Entity Framework Core.


