欢迎来到cool的博客
7

Music box

Click to Start

点击头像播放音乐
新博客链接

N+1 queries 问题, 使用includes方法,减少sql语句查询,提高性能

参考文章:

https://ithelp.ithome.com.tw/articles/10161667

https://ithelp.ithome.com.tw/articles/10161667

 

N+1 queries

N+1 queries是資料庫效能頭號殺手。ActiveRecordAssociation功能很方便,所以很容易就寫出以下的程式:

# model
class User < ActieRecord::Base
  has_one :car
end

class Car < ActiveRecord::Base
  belongs_to :user
end

# your controller
def index
  @users = User.page(params[:page])
end

# view
<% @users.each do |user| %>
 <%= user.car.name %>
<% end %>

我們在View中讀取user.car.name的值。但是這樣的程式導致了N+1 queries問題,假設User有10筆,這程式會產生出11筆Queries,一筆是查User,另外10筆是一筆一筆去查Car,嚴重拖慢效能。

SELECT * FROM `users` LIMIT 10 OFFSET 0
SELECT * FROM `cars` WHERE (`cars`.`user_id` = 1)
SELECT * FROM `cars` WHERE (`cars`.`user_id` = 2)
SELECT * FROM `cars` WHERE (`cars`.`user_id` = 3)
...
...
...
SELECT * FROM `cars` WHERE (`cars`.`user_id` = 10)

解決方法,加上includes

# your controller
def index
  @users = User.includes(:car).page(params[:page])
end

如此SQL query就只有兩個,只用一個就撈出所有Cars資料。

SELECT * FROM `users` LIMIT 10 OFFSET 0
SELECT * FROM `cars` WHERE (`cars`.`user_id` IN('1','2','3','4','5','6','7','8','9','10'))

如果user還有parts零件的關聯資料想要一起撈出來,includes也支援hash寫法:@users = User.includes(:car => :parts ).page(params[:page])

 

 

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

# Post
has_many :comments

# Comment
belongs_To :post

並在helper中寫下:

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

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

每一筆查詢對資料庫的效能都是一種消耗,因此身為後端開發者,查詢資料庫的次數是越少越好。以上寫法讓我們在查詢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.include(:post)
# 回傳所有comment
# 會查詢關連資料,因此查詢comment.post.title並不會產生新的查詢指令

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

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能夠節省許多記憶體的資源,達到更快的效率。

返回列表