一個很常的議題是討論 Ruby on Rails 很慢,但其實追根究底起來,一般網站慢的問題痛點都在於讀取 Database 的 response time 太久,讓人有很慢的錯覺,其實不管用哪套框架,如果在讀資料的時候慢,是會對使用體驗非常差的。
你的 application 慢在哪?
因為 Rails 有良好的 ORM 可以使用,所以我們大可不必自己手寫 SQL Query,但也因為這樣衍生出表面上看起來 code 很好讀懂,卻在讀取較大資料時明顯的發現有效能並不好的問題。
造成讀取數據慢的幾項常見原因:
- N+1 queries
- 一次拉出太多不必要的資料,塞爆 Database memory
- 沒有建立 index, 或是用了錯誤的 index
- 沒有適當的 cache 資料,凡事伸手向 Database 拿
這些問題在一開始資料還小的時候(小於百萬),都可以靠 database cpu 硬幹過去,可能不會有太明顯的感受,但 client 端口變多、資料增長速度快的情況下,可能光 api 就被 call 掛了,這樣就會收到滿滿的客訴抱怨了
從 Rails 的語句下手
確認資料是否存在的四種常見語句
- present?
- empty?
- any?
- exists?
這裡先講結論,建議從 ActiveRecord 中拉資料出來確認是否存在時,偏好使用 exists?
為什麼?我們舉例來看
Order.where(created_at: 7.days.ago..1.day.ago).done.present?
# SELECT "orders".* FROM "orders" WHERE ("orders"."created_at" BETWEEN
# '2017-02-22 21:22:27.133402' AND '2017-02-28 21:22:27.133529') AND
# "orders"."aasm_state" = $1 [["aasm_state", "done"]]
Order.where(:created_at => 7.days.ago..1.day.ago).done.any?
# SELECT COUNT(*) FROM "orders" WHERE ("orders"."created_at" BETWEEN
# '2017-02-22 21:22:16.885942' AND '2017-02-28 21:22:16.886077') AND
# "orders"."aasm_state" = $1 [["aasm_state", "done"]]
Order.where(:created_at => 7.days.ago..1.day.ago).done.empty?
# SELECT COUNT(*) FROM "orders" WHERE ("orders"."created_at" BETWEEN
# '2017-02-22 21:22:16.885942' AND '2017-02-28 21:22:16.886077') AND
# "orders"."aasm_state" = $1 [["aasm_state", "done"]]
Order.where(:created_at => 7.days.ago..1.day.ago).done.exists?
# SELECT 1 AS one FROM "builds" WHERE ("builds"."created_at" BETWEEN
# '2017-02-22 21:23:04.066301' AND '2017-02-28 21:23:04.066443') AND
# "orders"."aasm_state" = $1 LIMIT 1 [["aasm_state", "done"]]
從第一個例子來看,我們較常使用的 .present?
效率是非常低的,他等於是讀取了所有相關的資料,只為了確定是否存在?
如果在這個時間區間內的資料量更大,會更明顯感受到這是非常低效的作法,更具體的說明是,我為了要確認這個圖書館有沒有書,我把所有的書都拿出來,然後跟你說,有書。
那麼第二個和第三個的作法,用了 .any?
和 .empty?
,從 SQL query 來看,用了 COUNT(*)
,在 Rails 中,有針對 COUNT
進行優化,所以會將這個資料放進內存中,整體上來說,並沒有什麼副作用或是效能太慢的問題。
則最後一種方法 .exists?
,則是最佳解,這應該是你要檢查資料是否存在的首選項,因為從 SQL query 來看,他用了 SELECT 1 LIMIT 1
的方法,這是非常快的,就算你的資料表數據量非常龐大。
具體來說,我要確認圖書館有沒有書,只要找到一本,我就確定有書。
速度分析參考
present? => 2892.7 ms
any? => 400.9 ms
empty? => 403.9 ms
exists => 1.1 ms
如果說 200ms 的 response time 是可以接受的程度,那麼這個好習慣可以讓你盡可能的避免寫出有效能問題的 code
所以我該總是使用 exists? 嗎
基本上這是一個很好的 default 寫法,在確認資料存在的情況下。
但有些例外並不適用,例如前幾行 code 已經將相關數據緩存進來的時候,那麼你再次調用 .exists?
,其實就會再多一條 SQL query
舉例來說
user = User.find_by(name: "NicLin")
user.orders.load # eager loads all the builds into the association cache
user.orders.any? # no database hit
user.orders.exists? # hits the database
# 如果你改變了 cache, 將會再次 hits the database
user.orders(true).any? # hits the database
user.orders(true).exists? # hits the database
結論
建議使用 .exists?
, 依據不同情況改善