tl; dr Nhiều Include
s làm nổ tập kết quả SQL. Việc tải dữ liệu bằng nhiều lệnh gọi cơ sở dữ liệu sẽ sớm trở nên rẻ hơn thay vì chạy một câu lệnh lớn. Cố gắng tìm hỗn hợp tốt nhất của Include
và Load
tuyên bố.
có vẻ như có một hình phạt hiệu suất khi sử dụng Bao gồm
Đó là một cách nói ngắn gọn! Nhiều Include
s nhanh chóng làm nổ tung kết quả truy vấn SQL cả về chiều rộng và chiều dài. Tại sao vậy?
Yếu tố tăng trưởng của Include
s
(Phần này áp dụng Entity Framework classic, v6 trở về trước)
Giả sử chúng ta có
- thực thể gốc
Root
- thực thể mẹ
Root.Parent
- các thực thể con
Root.Children1
vàRoot.Children2
- câu lệnh LINQ
Root.Include("Parent").Include("Children1").Include("Children2")
Điều này xây dựng một câu lệnh SQL có cấu trúc sau:
SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1
UNION
SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2
<PseudoColumns>
này bao gồm các biểu thức như CAST(NULL AS int) AS [C2],
và chúng có cùng số lượng cột trong tất cả UNION
truy vấn -ed. Phần đầu tiên thêm các cột giả cho Child2
, phần thứ hai thêm các cột giả cho Child1
.
Đây là ý nghĩa của nó đối với kích thước của tập kết quả SQL:
- Số lượng cột trong
SELECT
mệnh đề là tổng của tất cả các cột trong bốn bảng - Số lượng hàng là tổng các bản ghi trong các tập hợp con được bao gồm
Vì tổng số điểm dữ liệu là columns * rows
, mỗi Include
Tăng tổng số điểm dữ liệu trong tập kết quả theo cấp số nhân. Hãy để tôi chứng minh điều đó bằng cách lấy Root
một lần nữa, bây giờ có thêm Children3
thu thập. Nếu tất cả các bảng có 5 cột và 100 hàng, chúng ta nhận được:
Một Include
(Root
+ 1 tập hợp con):10 cột * 100 hàng =1000 điểm dữ liệu.
Hai Include
s (Root
+ 2 tập hợp con):15 cột * 200 hàng =3000 điểm dữ liệu.
Ba Include
s (Root
+ 3 tập hợp con):20 cột * 300 hàng =6000 điểm dữ liệu.
Với 12 Include
con số này sẽ lên tới 78000 điểm dữ liệu!
Ngược lại, nếu bạn nhận được tất cả các bản ghi cho từng bảng riêng biệt thay vì 12 Include
, bạn có 13 * 5 * 100
điểm dữ liệu:6500, ít hơn 10%!
Giờ đây, những con số này hơi phóng đại khi nhiều điểm dữ liệu này sẽ là null
, vì vậy chúng không đóng góp nhiều vào kích thước thực của tập kết quả được gửi đến máy khách. Nhưng kích thước truy vấn và nhiệm vụ cho trình tối ưu hóa truy vấn chắc chắn bị ảnh hưởng tiêu cực bởi số lượng Include
ngày càng tăng s.
Số dư
Vì vậy, sử dụng Include
là một sự cân bằng tinh tế giữa chi phí của các cuộc gọi cơ sở dữ liệu và khối lượng dữ liệu. Thật khó để đưa ra một quy tắc chung, nhưng bây giờ bạn có thể tưởng tượng rằng khối lượng dữ liệu thường nhanh chóng tăng nhanh hơn chi phí của các cuộc gọi bổ sung nếu có nhiều hơn ~ 3 Include
cho các bộ sưu tập con (nhưng nhiều hơn một chút cho bộ sưu tập gốc Include
, điều đó chỉ mở rộng tập kết quả).
Thay thế
Thay thế cho Include
là tải dữ liệu trong các truy vấn riêng biệt:
context.Configuration.LazyLoadingEnabled = false;
var rootId = 1;
context.Children1.Where(c => c.RootId == rootId).Load();
context.Children2.Where(c => c.RootId == rootId).Load();
return context.Roots.Find(rootId);
Điều này tải tất cả dữ liệu cần thiết vào bộ nhớ cache của ngữ cảnh. Trong quá trình này, EF thực thi sửa chữa mối quan hệ theo đó nó tự động điền các thuộc tính điều hướng (Root.Children
vv) bởi các thực thể được tải. Kết quả cuối cùng giống với câu lệnh có Include
s, ngoại trừ một điểm khác biệt quan trọng:các tập hợp con không được đánh dấu là đã tải trong trình quản lý trạng thái thực thể, vì vậy EF sẽ cố gắng kích hoạt tải chậm nếu bạn truy cập chúng. Đó là lý do tại sao điều quan trọng là phải tắt tải chậm.
Trong thực tế, bạn sẽ phải tìm ra sự kết hợp nào của Include
và Load
câu lệnh phù hợp nhất với bạn.
Các khía cạnh khác cần xem xét
Mỗi Include
cũng làm tăng độ phức tạp của truy vấn, vì vậy trình tối ưu hóa truy vấn của cơ sở dữ liệu sẽ ngày càng phải nỗ lực nhiều hơn để tìm ra phương án truy vấn tốt nhất. Tại một số điểm, điều này có thể không còn thành công nữa. Ngoài ra, khi một số chỉ mục quan trọng bị thiếu (đặc biệt là khóa ngoại), hiệu suất có thể bị ảnh hưởng bằng cách thêm Include
s, ngay cả với kế hoạch truy vấn tốt nhất.
Lõi khung thực thể
Vụ nổ Descartes
Vì một số lý do, hành vi được mô tả ở trên, các truy vấn UNIONed, đã bị bỏ qua đối với EF core 3. Giờ đây, nó xây dựng một truy vấn với các phép nối. Khi truy vấn có hình dạng "ngôi sao", điều này dẫn đến sự bùng nổ Descartes (trong tập kết quả SQL). Tôi chỉ có thể tìm thấy một ghi chú thông báo về sự thay đổi đột ngột này, nhưng nó không cho biết lý do tại sao.
Truy vấn phân tách
Để chống lại sự bùng nổ Descartes này, Entity Framework core 5 đã giới thiệu khái niệm truy vấn phân tách cho phép tải dữ liệu liên quan trong nhiều truy vấn. Nó ngăn cản việc xây dựng một tập kết quả SQL khổng lồ, được nhân lên. Ngoài ra, do độ phức tạp của truy vấn thấp hơn, nó có thể giảm thời gian tìm nạp dữ liệu ngay cả với nhiều vòng lặp. Tuy nhiên, nó có thể dẫn đến dữ liệu không nhất quán khi các cập nhật đồng thời xảy ra.
Nhiều mối quan hệ 1:n ngoài gốc truy vấn.