Thực tế, dưới hai interface này là hai cơ chế xử lý khác nhau: một bên thao tác trực tiếp trên dữ liệu nằm trong RAM, một bên âm thầm build expression rồi dịch thành SQL đẩy xuống database.
Đôi lúc, chỉ một lần .ToList() sai chỗ cũng đủ làm hệ thống kéo toàn bộ dữ liệu bảng về memory rồi mới đi filter bằng C#. Thử "mổ bụng" sự khác biệt giữa IEnumerable và IQueryable và coi vì sao nhìn gần giống nhau nhưng lại tạo ra performance khác biệt lớn trong dự án thực tế.
LINQ đã lừa mình
Có bao giờ nhìn vào hai đoạn code này, và thấy nó gần như giống nhau?
var users = _context.Users
.Where(x => x.Age > 18)
.Take(10);
và
var users = _context.Users
.ToList()
.Where(x => x.Age > 18)
.Take(10);
Nhìn qua chỉ khác mỗi .ToList()
Nhưng runtime behavior hai đoạn code này thì khác hoàn toàn.
Một bên sẽ generate SQL:
SELECT TOP(10) * FROM Users WHERE Age > 18
Bên còn lại có thể:
SELECT * FROM Users
Tiếp theo, nó sẽ kéo toàn bộ về RAM, sau đó mới bắt đầu filter bằng C#.
Đây cũng lý do khiến nhiều hệ thống:
- Query chậm bất thường
- RAM tăng mạnh
- CPU app server tăng cao
- Database traffic lớn vô lý
Trong khi nhìn lại LINQ vẫn thấy... bình thường, không có gì vô lý.
Và cái LINQ nó lừa mình ở chỗ:
IEnumerable và IQueryable dùng syntax gần như nhau, nhưng phía dưới là hai cơ chế hoàn toàn khác nhau.
IEnumerable - dữ liệu đã trong memory
Khi code như vậy:
IEnumerable<User>
Nghĩa là dữ liệu lúc này đã nằm trong RAM.
IEnumerable không biết gì về:
- SQL
- Database
- Network
- Query optimization
Nó chỉ biết:
Tao đang có một đống object C# trong memory
Ví dụ:
var users = _context.Users.ToList();
Ngay khi .ToList() chạy:
Database
↓
Kéo dữ liệu về RAM
↓
Biến thành List<User>
Từ thời điểm này:
users.Where(x => x.Age > 18)
Sẽ filter bằng C#.
Nghĩa là:
- CPU app server xử lý
- Loop chạy trong memory
- Dữ liệu đã load lên hết rồi
Lúc này LINQ chỉ còn thao tác collection bình thường.
IQueryable - query nhưng sao chưa chạy
Ví dụ:
var users = _context.Users
.Where(x => x.Age > 18);
Nhìn qua tưởng query đã chạy rồi đúng không?!
Không.
Ở thời điểm này, Entity Framework vẫn chưa query database.
Lúc này nó chỉ đang:
- Ghi nhận điều kiện query
- Build expression tree (Debug đặt con trỏ ngay dòng này là thấy)
- Chuẩn bị generate SQL
Nói đơn giản hơn:
IQueryable chưa giữ dữ liệu thật.
Nó chỉ đagn giữ "bản thảo" query. Đây cũng là lý do có khái niệm:
Deferred Execution.
Deferred Execution - query chưa chạy khi người nghĩ
Ví dụ:
var users = _context.Users
.Where(x => x.Age > 18);
SQL vẫn chưa được gửi xuống database.
Chỉ khi gặp những thứ như:
.ToList() .First() .Count() .Any()
Query lúc này mới thật sự chạy.
Ví dụ:
var query = _context.Users
.Where(x => x.Age > 18);
Console.WriteLine("Chưa query DB");
var result = query.ToList();
Flow nó sẽ chạy như vậy:
Build expression tree
↓
Chưa query gì cả
↓
Gặp ToList()
↓
Generate SQL
↓
Query database
Đây cũng là lý do nhiều người vô tình query database nhiều lần mà không biết.
var query = _context.Users
.Where(x => x.Age > 18);
var count = query.Count();
var users = query.ToList();
Có phải code này nó đang bắn xuống database với hai query khác nhau?!
Cú lừa của ToList()
Ví dụ:
var users = _context.Users
.ToList()
.Where(x => x.Age > 18);
Nhiều người sẽ nghĩ:
"Where đang nằm đó, chắc database sẽ filter"
Không.
Ngay khi .ToList() xuất hiện:
IQueryable
↓
Query database
↓
Kéo toàn bộ data về RAM
↓
Biến thành IEnumerable/List
Sau thời điểm đó:
.Where() .OrderBy() .Take()
Đều chạy bằng C# trong memory.
Ví dụ một bảng:
5 triệu rows
Application có thể:
- Kéo 5 triệu record đó về RAM
- Ngốn memory cực khủng
- Filter bằng CPU app server
Trong khi database đáng lẽ sẽ làm tốt hơn rất nhiều.
Đây là kiểu bug "vô hại" nhưng lại phá performance rất mạnh ở production.
Expression Tree - thứ đứng sau IQueryable
Khi viết:
.Where(x => x.Age > 18)
lambda này đang chạy.
Không hẳn vậy.
Với IQueryable, lambda lúc này thường chưa được execute ngay.
Nó sẽ thành:
Expression<Func<User, bool>>
Nói đơn giản, thay vì chạy code, framework sẽ lưu "cấu trúc" của đoạn query.
Ví dụ:
Age > 18
Entity Framework đọc expression tree rồi transalte thành SQL.
Flow thật của nó:
LINQ ↓ Expression Tree ↓ EF Core Query Provider ↓ Generate SQL ↓ Database
Đây là lý do không phải mọi method C# đều translate được sang SQL.
Ví dụ:
.Where(x => MyCustomMethod(x.Name))
Entity Framework có thể báo lỗi:
Cannot translate expression to SQL
Vì database lúc này hoàn toàn không biết MyCustomMethod() là gì.
AsEnumerable() - điểm chuyển từ database sang memory
Một method nhìn rất vô hại khác:
.AsEnumerable()
Ví dụ:
_context.Users
.Where(x => x.Age > 18)
.AsEnumerable()
.Where(x => x.Name.StartsWith("D"));
Flow nó chạy:
WHERE Age > 18 -> chạy ở SQL
↓
Kéo data về RAM
↓
Name.StartsWith("D") -> chạy bằng C#
Sau .AsEnumerable():
- Query không còn translate sang SQL nữa
- Mọi thứ phía sau chạy trong memory
Đây là chỗ rất dễ bị bỏ qua khi debug performance.
IQueryable mạnh, nhưng không phải magic
.Where() .Select() .Join() .GroupBy() .OrderBy() .Skip() .Take()
Nhiều người sau khi hiểu IQueryable bắt đầu chain LINQ khắp nơi, Framework vẫn generate SQL được.
Nhưng đôi khi SQL sẽ tạo ra rất "khủng"
Ví dụ:
- Nested subquery
- Multiple join
- Query plan rất nặng
- Execution chậm
Lúc này:
- SQL khó debug
- Không đoán được performance
- Query optimize rất khó
IQueryable rất mạnh, nhưng mạnh do người viết, không phải lúc nào LINQ cũng xử lý cho SQL tối ưu.
Kết luận
IEnumerable<T> IQueryable<T>
Nhìn bề ngoài, nó giống hệt nhau. Thậm chí synxtax LINQ cũng giống nhau.
.Where() .Select() .OrderBy()
Nhưng phía dưới lại là hai cơ chế xử lý hoàn toàn khác nhau:
IEnumerablethao tác dữ liệu đã nằm trong RAMIQueryablebuild query để đẩy xuống database
Chỉ một lần .ToList() hoặc .AsEnumerable() đặt sai chỗ sẽ làm:
- Query kéo cả bảng
- RAM tăng mạnh
- CPU app server đốt vô ích
- System chậm đi
Hiểu được ranh giới không những giúp viết LINQ “ngầu” hơn. Nhưng nó cho một thứ quan trọng hơn nhiều:
Khả năng nhìn ra dữ liệu đang được xử lý ở đâu.
Khi application bắt đầu:
- Query chậm
- Generate SQL kỳ lạ
- RAM tăng bất thường
- Database bị spam query
- EF Core kéo cả bảng về memory
Lúc này sẽ biết chính xác vấn đề đang nằm ở tầng nào thay vì debug trong tuyệt vọng.
No comments yet. Be the first to comment!