N+1查詢:Rails開發者的隱藏陷阱 (2/3)
上一篇文章提到了幾種不小心造成的 N+1 情況,這個章節也會提到關於 N+1 效能的問題。
count 、size 和 length 有什麼差別?
count 方法是對資料庫送出一次 COUNT 的 SQL 查詢。
如果我呼叫了兩次,他就會產生兩筆的查詢
post = Post.first
post.comments.count
# Comment Count (0.6ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 [["post_id", 1]]
post.comments.count
# Comment Count (0.3ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 [["post_id", 1]]
在 size 與 length 一樣是用陣列的方法計算物件裡面元素的數量,因為物件已經在記憶體了,因此不需要再發出 COUNT 的 SQL 查詢。
post = Post.first
post.comments.load
# Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1 [["post_id", 1]]
post.comments.size
# (no query)
在沒有加載 preload 的情況下,size 就會和 count 一樣執行一次 COUNT 的 SQL 查詢。
post = Post.first
post.comments.size
# Comment Count (0.4ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1 [["post_id", 1]]
在避免 N+1 的同時也可能導致效能下降
這邊我們舉一個例子,想要看到每一篇文章的按讚次數,這邊使用 count 也沒有 preload,一看就知道產生了N+1
posts.each do |post|
post.likes.count # n+1 queries
end
接著我們先 preload,然後使用 size 來計算按讚次數,順利解決 n+1 的問題,但可能會引入效能問題。
#view
posts.preload(:likes).each do |post|
post.likes.size
end
# Post Load (2.4ms) SELECT "posts".* FROM "posts"
# Like Load (19.9ms) SELECT "likes".* FROM "likes" WHERE "likes"."post_id" IN
解決方案
對這些文章的 “likes” 使用 group 搭配 count 查詢。這個查詢的目的是取得每篇文章的 “likes” 數量,而不是實際的 likes 資訊,實際的好處如下面所說的:
- 較少的資料從資料庫中取得,特別是在有大量 likes 的情境下。
- 只有所需的資訊被加載,不需要多餘的資料。
- 由於不需要將所有 “likes” 載入記憶體,所以對於資源使用效率更高。
posts = Post.limit(1000)
likes = Like.where(post_id: posts)
counts = likes.group(:post_id).count
#view
posts.each do |post|
puts "Post: #{post.id}, likes: #{counts[post.id] || 0}"
end
# (9.5ms) SELECT COUNT(*) AS count_all, "likes"."post_id" AS likes_post_id FROM "likes" WHERE "likes"."post_id" IN (SELECT "posts"."id" FROM "posts") GROUP BY "likes"."post_id"
我們也可以使用 joins
來完成這件事,來撈出所需要的資訊即可,這應該是比較常見的組合技。
posts = Post.limit(1000)
counts = posts.joins(:likes).group("posts.id").count("likes.id")
# view
posts.each do |post|
puts "Post: #{post.id}, likes: #{counts[post.id] || 0}"
end
# (9.3ms) SELECT COUNT(likes.id) AS count_likes_id, posts.id AS posts_id FROM "posts" INNER JOIN "likes" ON "likes"."post_id" = "posts"."id" GROUP BY posts.id
這時候又能夠發現一件事情 joins
只會返回有 likes 的文章,那麼我想知道有多少文章沒有被按讚怎麼辦?
我們可以改用 left_joins
,posts 表中的所有文章都會被載入,無論文章有沒有被按讚。
posts = Post.limit(1000)
counts = posts.left_joins(:likes).group("posts.id").count("likes.id")
posts.each do |post|
puts "Post: #{post.id}, likes: #{counts[post.id]}"
end
# (10.7ms) SELECT COUNT(likes.id) AS count_likes_id, posts.id AS posts_id FROM "posts" LEFT OUTER JOIN "likes" ON "likes"."post_id" = "posts"."id" GROUP BY posts.id
下一篇我們會提到使用工具幫忙找出隱藏的 N+1 ~