Rails 用巢狀include和查表方式來避免 n+1 query
先前在用includes和joins避免N+1 query當中有提到,資料庫一直是Rails效能一大殺手,除了瀏覽器下載很肥的檔案會造成效能低落以外,第二個會讓網頁很慢的原因就是資料庫讀取太頻繁(當然如果被牆了那可能又是另一個原因...)
以下介紹兩種稍微複雜一點的情況,使用不同的方法來避免 n + 1 query。
巢狀 include (nested include)
如果我們遇到非常複雜的table結構,關連得非常遙遠,例如:
class Post < ActiveRecord::Base
has_many :comments
belongs_to :user
end
class Comment < ActiveRecord::Base
has_many :replies
belongs_to :user
end
class Reply < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :posts
has_many :comments
has_many :replies
end
這邊有兩種關連
Post > Comment > Reply (就像是Facebook可以針對回覆還可以進行回覆的結構)
User 擁有所有model
假如在某個畫面中,我們需要所有資料欄位,那include的方式可能較為複雜,會寫成巢狀結構。
# 不包含user
Post.includes(:comments => [:replies])
# 包含user
Post.includes(:user, :comments= > [:user, {:replies => [:user]}])
以上的寫法中,要注意三個地方:
1. 分開include
像User
這樣的model與三個其他model都有關連,就必須分開include。
Post.includes(:user, :comments)
# 錯誤,只有Post有include User,而Comment沒有include User
Post.includes(:user, :comments => [:user])
# 正確,皆include User資料
這樣例如分開來查Comment.first.user
和Post.first.user
才會真正利用到eager_load的機制。
2. Hash和Array寫法
如果巢狀只有一層關連,則使用Array
來撰寫,例如
Post.includes(:comments => [:replies])
但如果底下還包含了其他model,則就要用Hash
來撰寫,例如
Post.includes(:comments => {:replies => [:user]})
3. 順序
包含其他model的巢狀結構要寫在最後方,例如以下寫法就會顯示錯誤。
Post.includes(:comments => [:replies], :user)
# => SyntaxError
這樣一來,查詢時就會將所有相關資料都include進去,避免實際使用時還到資料庫查詢。
用查表方式減少query
這種方式並非Rails建議查詢table的方法,但總是在退無可退的時候相當好用。例如我們在mysql的某個table中有一千萬筆資料,而且是用複合key的方式進行關連。
Post
ID | user_id | date | content
1 | 2 | 2015-01-01 | ...
2 | 3 | 2015-02-05 | ...
3 | 4 | 2015-03-07 | ...
4 | 5 | 2015-08-10 | ...
假如我們需要知道每一個user在特定時間所寫的內容為何,這樣會產生需要用複合key來查詢的狀況。我必須組合user_id
和date
為key,用來查詢特定user在特定date的所寫內容。
由於是針對每一個user來進行查詢,所以不管是用includes
和joins
都會產生不斷查詢資料庫的情況。因此,如果我們將所有的資料先抓出來,塞入一個hash當中,再用key來查詢,這樣的速度會比n+1的情況快上許多。
# 先將Post所有內容塞入hash這個物件當中,當做查詢總表
hash = Post.all.inject({}) do |result, post|
key = (user_id.to_s + date.to_s).to_sym
result.merge({ key => content })
end
# 針對特定user和date查詢
current_key = (current_user.id + current_date.to_s).to_sym
current_content = hash[current_key]
用這樣的查詢方式等於我們將資料庫的table整個搬到code裡面來,在第一步產生整包hash
,接下來再查詢。剛開始從資料庫抓資料時會花一點時間,但接下來用hash來查詢的速度就會非常快。
注意,這樣的用法會造成與rails convention稍有不同,畢竟都提供這麼健全的table關連方法了,還硬要用查表的,維護上會稍微需要費一點心思。建議過渡期過後,還是將資料結構修改為與rails相符的結構。
這些都是敝人測試經驗,如有更好的寫法歡迎提供,謝謝!