Rails使用 include 和 join 避免 N+1 query


Rails當中要連結model之間的關係非常簡單,不過也因為由於建立關係是這樣的簡便,造成許多指令會在讀取資料庫時有記憶體的浪費。例如我們建立以下關係:

# Post
has_many :comments

# Comment
belongs_To :post

並在helper中寫下:

Comment.each do |comment|
    comment.post.title
end

如果我們有很多個comment,就會產生非常多的資料庫查詢記錄:

pic

每一筆查詢對資料庫的效能都是一種消耗,因此身為後端開發者,查詢資料庫的次數是越少越好。以上寫法讓我們在查詢post的title時都透過comment的關連去查詢,所以執行每一個comment時,都會查詢一次post,增加大量的資料庫查詢比數。這就是一般資料庫容易產生的N+1 query問題,意思是我們在迴圈當中大量查詢N筆資料,再加上開頭查詢的那1筆,稱為N+1。

為了避免在幾千筆資料查詢時大量消耗不必要的記憶體,Rails提供joins和include方法可以在第一次查詢時將所有我們需要的資料一次查完。

記住,只要從model把資料抓到controller之後,剩下的我們就可以自行處理。越少的find和where指令越好。

例如剛才的情況,可以用includes的方式解決:

comments = Comment.includes(:post)
Comment.each do |comment|
    comment.post.title
end

使用這個include方法,會在載入comments時,就先把各項內容載入,解決剛剛N+1的狀況。include這種查詢方法稱為eager loading,先將需要的資料一次查好,避免未來其他

join和include的區別

雖然join和include的字義相像,在model中的用法也相像,但主要差別在於: 1. join主要用於過濾model之間的關係,但對查詢筆數來說並無太大幫助 2. include主要用於將大量資料在同一筆查詢內一次查好

以剛剛的post和comment為例:

comments = Comment.joins(:post)
# 回傳所有comment內含有post_id的項目
# 並不會同時查詢關連資料,所以剛剛的comment.post.title會產生新的查詢指令

comments = Comment.includes(:post)
# 回傳所有comment
# 會查詢關連資料,因此查詢comment.post.title並不會產生新的查詢指令

以上是join用在belongsto的用法,如果用在hasmany,會有不同的狀況:

posts = Post.joins(:comments)
# 查詢全部含有post_id的comment,並回傳該comment所屬的post
# 如果有很多筆comment屬於同一個post,那會回傳大量相同的post,可用.uniq來刪除重複的項目

posts = Post.includes(:comments)
# 回傳所有post
# post和comment內容皆已在本次查詢,之後不產生額外的查詢筆數

不管是joins還是include,都可以搭配where來查詢符合條件的項目:

comments = Comment.joins(:post).where("title like ? ", "my_title")
# 在1筆query內查詢所有post的title是"my_title"的項目
# 回傳符合的post所擁有的comment

有些時候也會遇上有趣的狀況:

posts = Post.includes(:comments).where(:comments => {:content => "hello" } )
# 只要有comment的content為"hello",就回傳該post

posts.first.comments
# 只會回傳content是"hello"的comment
# 兩個指令只產生一筆查詢

posts = Post.joins(:comments).where(:comments => {:content => "hello"})
# 同上

posts.first.comments
# 回傳該post的所有comments,不管content為何
# 兩個指令產生兩筆查詢

以上狀況,可看出include比較能達到我們要的效果。記得每次寫code時都要注意是否會有N+1查詢次數的問題,利用join和include能夠節省許多記憶體的資源,達到更快的效率。

CC圖片授權:Wikimedia