Rails ActiveRecord — count、length、size

使用 ActiveRecord 取得資料筆數的方式有三種用法,分別是 count、length、size,各自由不同的實作去取得資料筆數,正確的使用可以避免不必要資料庫查詢。

以下範例為 GroupClass 底下有多堂 Lesson 課程

group = GroupClass.first
lessons = group.lessons.all

# count

使用 count 時,ActiveRecord 會產生像 Select count(*) From ... 的 COUNT SQL 到資料庫取回目前資料筆數,,在 Loop 內使用會有 N+1 Query 的問題。

lesson.count
# (0.4ms)  SELECT COUNT(*) FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 5

lesson.count
# (0.4ms)  SELECT COUNT(*) FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 5

# length

length 會根據目前 association 是否已經載入記憶做出不同行為,若還沒有載入會產生 SELECT xxx.* FROM ... 的 SELECT ALL SQL,若已經載入則會直接計算 association 的 Array 大小。

# 先用 length 將 Lesson 載入記憶體 ,lessons[0].id 就不會再重新執行 Query
lessons.length
# Lesson Load (0.5ms)  SELECT `lessons`.* FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 5

lessons[0].id
=> 1
# 先用 lessons[0].id 將 Lesson 載入記憶體,lessons.length 也不會重新執行 Query
lessons = group.lessons.all # refresh lessons association
lessons[0].id
# Lesson Load (0.7ms)  SELECT `lessons`.* FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 1

lessons.length
=> 5

size size 會根據目前 association 是否已經載入記憶體做出不同行為,與 length 不同點在於產生的 Query 是 COUNT SQL,且呼叫後不會把結果儲存至記憶體。

# 情境一: lessons 已存在於記憶體

lessons = group.lessons.all # refresh lessons association
lessons[0].id
# Lesson Load (0.7ms)  SELECT `lessons`.* FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 1

lessons.size
=> 5

# 情境二: lessons 不存在於記憶體

lessons = group.lessons.all # refresh lessons association
lessons.size
# (0.4ms)  SELECT COUNT(*) FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 5

# 由於 size 使用 SELECT COUNT(*),其他欄位並不存在於記憶體,會重新執行 SELECT SQL
lessons[0].id
# Lesson Load (0.6ms)  SELECT `lessons`.* FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 1

# 情境三: 重複使用 size

# 與 count 相同
lessons.size
# (0.5ms)  SELECT COUNT(*) FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 5

lessons.size
# (0.5ms)  SELECT COUNT(*) FROM `lessons` WHERE `lessons`.`group_class_id` = 1
=> 5

# 整理

使用上要先瞭解目前的 association instance 是否已經將結果載入記憶體,不確定的情況下可以用 loaded? 來做檢查,再依據結果選擇要用 length 還是 count 就不會有太大的問題。

size 可以適用大部的狀況,但 size 並不是完美的通用解,若已經知道後續會取用到其他欄位,使用 length 可以多節省一次查詢。

另外 size 可以使用 counter_cache (opens new window) 在 table 建一個欄位,關聯刪減時會將欄位數量增減記錄起來,但是這樣的紀錄方式,有可能會隨著時間產生落差,像是人為的刪減或是批次操作沒有觸發計數,使用上要比較小心。

# references

  • https://stackoverflow.com/questions/6083219/activerecord-size-vs-count/21615375#21615375