Một số câu hỏi để hiểu rõ hơn về joins, includes, preload và eager_load trong ActiveRecord
Bài đăng này đã không được cập nhật trong 6 năm
Các phương thức joins
, includes
, preload
và eager_load
của ActiveRecord
đều vô cùng hữu ích, nhưng cũng rất nguy hiểm nếu sử dụng không đúng cách. Hiểu được việc sử dụng nó khi nào và ở đâu – và cả khi nào nên kết hợp lại – có thể giúp bạn rất nhiều khi phát triển ứng dụng.
Dưới đây, tôi sẽ chỉ cho các bạn ở đâu và khi nào sử dụng các phương thức.
Joins
Trường hợp lý tưởng để sử dụng joins?
Nếu bạn chỉ muốn dùng bảng liên kết để lọc dữ liệu, không lấy dữ liệu – joins
là mục tiêu của bạn. Ví dụ dưới đây lấy các bài post có comment được viết bời Derek. Ta không lấy dữ liệu về comment, vậy nên joins
là lựa chọn phù hợp:
Post.joins(:comments).where(:comments => {author: 'Derek'}).map { |post| post.title }
Post Load (1.2ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."author" = $1
=> ["One weird trick to better Rails apps",
"1,234 weird tricks to faster Rails apps",
"You wouldn't believe what happened to this Rails developer after 14 days"]
Joins có tránh được N+1 queries không?
Không. Bản thân joins
không tải dữ liệu có liên kết vào bộ nhớ: việc lấy dữ liệu từ các bảng liên quan có thể gây ra truy vấn N+1.
Ví dụ, đếm số comment trong mỗi bài post trên, ta phải lấy dữ liệu từ bảng Comment:
Post.joins(:comments).where(:comments => {author: 'Derek'}).map { |post| post.comments.size }
Post Load (1.2ms) SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."author" = $1
(1.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(3.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(0.3ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(1.0ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(2.1ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
(1.4ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1
=> [3,5,2,4,2,1]
Includes
Includes có tránh được N+1 queries không?
Có. includes
sẽ tải (1) tất cả các bản ghi cha và (2) tất cả các bản ghi liên quan được tham chiếu thông qua includes
.
Trong ví dụ dưới đây, việc sử dụng includes
chỉ tạo ra một câu truy vấn. Nếu không có includes
, sẽ có một câu truy vấn bổ sung để đếm số lượng bình luận ở mỗi post:
Post.includes(:comments).map { |post| post.comments.size }
Post Load (1.2ms) SELECT "posts".* FROM "posts"
Comment Load (2.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 3, 4, 5, 6)
=> [3,5,2,4,2,1]
Includes có sinh ra các câu truy vấn riêng cho từng bản ghi liên quan?
Không. includes
sẽ không sử dụng truy vấn riêng. Nếu bạn có mệnh đề where
hay order
gọi đến bảng liên kết, LEFT OUTER JOIN
được sử dụng với một truy vấn riêng.
Một truy vấn hay hai truy vấn sẽ nhanh hơn?
Đào sâu vào bản chất của ActiveRecord
, tôi không tin rằng ActiveRecord
quyết định việc sử dụng hai truy vấn hay một truy vấn dựa trên hiệu năng. Nếu bạn đang thấy hiệu năng thấp với truy vấn includes
, tôi đề xuất việc sử dụng tool như Scout DevTrace và kiểm tra cách mà ActiveRecord
đang sử dụng khi chạy includes
.
Nếu hai truy vấn đang được sử dụng, bạn có thể tạo một truy vấn với LEFT OUTER JOIN
bằng cách thêm references
vào liên kết ActiveRecord
:
Post.includes(:comments).references(:comments).map { |post| post.comments.size }
Điều gì sẽ xảy ra khi tôi thêm điều kiện cho liên kết được includes?
ActiveRecord
sẽ trả lại tất cả bản ghi cha và chỉ những bản ghi liên quan khớp với điều kiện.
Ví dụ, dưới đây sẽ trả về tất cả dữ liệu của Post có comment bởi Derek, và chỉ đếm những comment của Derek:
Post.includes(:comments).references(:comments).where(comments => {author: 'Derek'}).map { |post| post.comments.size }
Includes có tránh được mọi N+1 queries không?
Không. Nếu bạn truy cập dữ liệu trong nested relationship
, dữ liệu đó không được tải từ trước. Ví dụ, một câu truy vấn bổ sung sẽ được thêm để lấy Comment#likes với mỗi comment:
<% post.comments.each do |comment| %>
<%= comment.likes.map { |like| like.user_avatar_url }
<% end %>
Tôi có thể tránh N+1s trong nested relationships không?
Có. Bạn có thể tải các nested relationship
bằng includes
:
Post.includes(comments => :likes).references(:comments).map { |post| post.comments.size }
Tôi có nên luôn luôn tải dữ liệu từ nested relationships không?
Không. Rất dễ để tạo một số lượng đáng kể dữ liệu. Ví dụ, một Comment có thể có hàng ngàn bản ghi Like, sẽ khiến câu truy vấn bị chậm và tốn bộ nhớ. Tool như Scout DevTrace có thể giúp bạn xác định cách tiếp cận nhanh hơn.
Preload
Việc kết hợp joins và preload có phổ biến không?
Nếu tôi cần tất cả bản ghi có liên kết - không chỉ những bản ghi phù hợp với điều kiện liên kết - Tôi sẽ kết hợp preload
và joins
. Ví dụ như:
- Tìm tất cả dữ liệu Post có Comment tác giả là Derek
- Xử lý những bản ghi của Post và tổng số comment của mỗi post.
includes
sẽ chỉ lấy dữ liệu Comment được viết bởi Derek, không phải tất cả comment của mỗi post.
Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id").where(:comments => {author: 'Derek'}).preload(:comments).map { |post| post.comments.size }
Eager_load
Tôi có nên sử dụng eager_load không?
Có. Nếu thấy việc dùng includes
bị chậm khi có 2 truy vấn, sử dụng eager_load
sẽ ép nó thành một truy vấn bằng LEFT OUTER JOIN
.
Tôi có thể kết hợp eager_load với joins không?
Có. Trong đoạn ví dụ sau:
Post.joins(:comments).eager_load(:comments).map { |post| post.comments.size }
ActiveRecord
sẽ làm những điều sau:
- Trả lại Array các dữ liệu Post với các comments.
- Tải các comments liên kết với mỗi Post.
Đó là
includes
vớiINNER JOIN
vàLEFT OUTER JOIN
.
Tổng kết
- Nếu tôi chỉ muốn lọc dữ liệu, sử dụng
joins
. - Nếu tôi truy cập vào những dữ liệu liên kết, bắt đầu với
includes
. - Nếu
includes
bị chậm khi sử dụng hai truy vấn riêng biệt, tôi sẽ sử dụngeager_load
để ép nó thành truy vấn đơn và so sánh hiệu năng.
Nguồn tham khảo: http://blog.scoutapp.com/articles/2017/01/24/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where
All rights reserved