iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
0
Modern Web

關於 Ruby on Rails,我想說的是系列 第 15

[Day 15] 效能殺手 N+1 Query

  • 分享至 

  • twitterImage
  •  

昨天[Day 14] 資料表關聯,以一對多為例介紹資料表的關聯has_manybelongs_to,讓我們很容易做關聯式資料表。但容易寫出拖慢效能的N+1 querycode。
N+1 query就是今天的主題
那麼來探討我一年前面試被問到的題目:什麼是N+1 query? 該怎麼解決?

什麼是N+1 query?

繼續使用前兩天建立的ProductProductStyle 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來列舉,以producttitle作為key,再用[Day 7] Enumable 迭代方法each_with_object產生product_styletitle-price鍵值對,回傳給productkey做對應的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 query1,第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就花了不少時間,明天再來介紹如何解決它


終於進行一半了,感動ଘ(੭ˊᵕˋ)੭ˋ

也謝謝看到這篇文章的你,如果覺得我的文筆哪裏需要改進歡迎指正,覺得不錯也歡迎訂閱。


上一篇
[Day 14] 資料表關聯,以一對多為例
下一篇
[Day 16] 關聯資料表加載,解決 N+1 Query
系列文
關於 Ruby on Rails,我想說的是23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言