昨天[Day 14] 資料表關聯,以一對多為例介紹資料表的關聯has_many
跟belongs_to
,讓我們很容易做關聯式資料表。但容易寫出拖慢效能的N+1 query
code。N+1 query
就是今天的主題
那麼來探討我一年前面試被問到的題目:什麼是N+1 query? 該怎麼解決?
繼續使用前兩天建立的Product
跟ProductStyle
Model
#app/models/product.rb
class Product < ApplicationRecord
has_many :styles, class_name: 'ProductStyle'
end
#app/models/product_style.rb
class ProductStyle < ApplicationRecord
belongs_to :product
end
在資料庫裡有7筆商品products
資料,跟屬於這些商品的商品款式product_styles
資料。
在ProductStylesController
建立all_title_price
方法回傳所有商品名稱,以及每個商品底下所有款式的名稱及價格。因為has_many
的關係,很直覺就會寫出下列程式:
class ProductStylesController < ApplicationController
def all_title_price
Product.all.map do |product|
hash = {}
hash[product.title] =
product.styles.each_with_object({}) do |style, result|
result[style.title] = style.price
end
hash
end.to_json
end
end
先列舉出所有商品,再把每個商品用map
來列舉,以product
的title
作為key,再用[Day 7] Enumable 迭代方法的each_with_object
產生product_style
的title-price
鍵值對,回傳給product
的key
做對應的value
。最後用to_json
轉換成JSON格式給前端
因為是示範程式,所以沒有寫view來接參數,那就把ProductStylesController
當作普通 Ruby class 執行實體方法。
ProductStylesController.new.all_title_price
執行後,會產生以下SQL
Product Load (0.4ms) SELECT "products".* FROM "products"
ProductStyle Load (0.3ms) SELECT "product_styles".* FROM "product_styles" WHERE "product_styles"."product_id" = $1 [["product_id", 3]]
ProductStyle Load (0.5ms) SELECT "product_styles".* FROM "product_styles" WHERE "product_styles"."product_id" = $1 [["product_id", 4]]
ProductStyle Load (0.2ms) SELECT "product_styles".* FROM "product_styles" WHERE "product_styles"."product_id" = $1 [["product_id", 6]]
ProductStyle Load (0.2ms) SELECT "product_styles".* FROM "product_styles" WHERE "product_styles"."product_id" = $1 [["product_id", 7]]
ProductStyle Load (0.2ms) SELECT "product_styles".* FROM "product_styles" WHERE "product_styles"."product_id" = $1 [["product_id", 8]]
ProductStyle Load (0.2ms) SELECT "product_styles".* FROM "product_styles" WHERE "product_styles"."product_id" = $1 [["product_id", 9]]
ProductStyle Load (0.1ms) SELECT "product_styles".* FROM "product_styles" WHERE "product_styles"."product_id" = $1 [["product_id", 10]]
# => "[{\"果乾\":{\"香蕉乾\":462.0,\"鳳梨乾\":9487.0,\"蘋果乾\":2330.0}},{\"商品二\":{\"芒果乾\":1450.0,\"test\":null}},{\"商品2\":{\"商品款式2\":999.0}},{\"商品3\":{\"商品款式3\":999.0}},{\"商品4\":{\"商品款式4\":999.0}},{\"商品5\":{\"商品款式5\":999.0}},{\"商品6\":{\"商品款式6\":999.0}}]"
第一行SQL是從products
資料表叫出資料,這是N+1 query
的1
,第2~8行從product_styles
資料表叫出資料。
因為product資料表共有7筆,每次跑到map迴圈裡的product.styles
就要呼叫SQL,總共呼叫七次(N+1 query,N 在這裡是7
),總共呼叫7+1
次,每次需要0.1ms~0.5ms,平均一個商品0.3ms
,一個稍有規模的商家可能有上萬個商品,那就要...3s
!!!,消費者看到網頁一直出不來,超過2秒可能就關網頁不下單了。N+1 Query
可以說是常見的效能殺手(業績殺手),因為呼叫SQL很花時間成本啊
,改進的方法就是減少呼叫SQL
現在知道N + 1 Query
就是:
N次迴圈中,每次迭代都呼叫資料庫,因而拖慢速度
沒想到光是介紹N+1 query
就花了不少時間,明天再來介紹如何解決它
終於進行一半了,感動ଘ(੭ˊᵕˋ)੭ˋ
也謝謝看到這篇文章的你,如果覺得我的文筆哪裏需要改進歡迎指正,覺得不錯也歡迎訂閱。