Enumerable#injectとArray#sumとActiveRecord#sumを比べてみた

Ruby, Railsの話です。
ちょっとした集計処理には色々な書き方があるなぁと思っていて、今携わっているプロジェクトにもいろんな書き方があったので比べてみるコトにしました。

f:id:tanaken0515:20181230205821p:plain

検証環境

  • ruby 2.5.1
  • rails 5.2.1
  • postgresql 10.5

問題状況

例えば注文明細の情報があり、「単価(price)」と「数量(quantity)」のデータを持っているとしましょう。
このデータを使って注文金額の合計値を計算したい場合について考えてみます。

orders テーブルのスキーマ:

  create_table "orders", force: :cascade do |t|
    t.integer "price"
    t.integer "quantity"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

計算ロジック

注文金額の合計値は全レコード分の price * quantity を足し合わせた値なので以下のような方法で計算することができそうです。

Enumerable#inject を使う場合

Order.all.inject(0){|sum, order| sum + order.price * order.quantity}

instance method Enumerable#inject (Ruby 2.6.0)

Array#sum を使う場合

Order.all.map{|order| order.price * order.quantity}.sum

instance method Array#sum (Ruby 2.6.0)

ActiveRecord#sum を使う場合

Order.all.sum("price*quantity")

https://devdocs.io/rails~5.2/activerecord/calculations#method-i-sum

比較してみた

毎回データを取得する場合

number_of_trial = 500
Order.uncached do
  Benchmark.bm 20 do |r|
    r.report "Enumerable#inject" do
      number_of_trial.times do
        Order.all.inject(0){|sum, order| sum + order.price * order.quantity}
      end
    end

    r.report "Array#sum" do
      number_of_trial.times do
        Order.all.map{|order| order.price * order.quantity}.sum
      end
    end

    r.report "ActiveRecord#sum" do
      number_of_trial.times do
        Order.all.sum("price*quantity")
      end
    end
  end
end
                           user     system      total        real
Enumerable#inject      6.380000   0.140000   6.520000 (  8.091545)
Array#sum              7.150000   0.040000   7.190000 (  8.771430)
ActiveRecord#sum       0.260000   0.070000   0.330000 (  1.081005)

前もって取得しておいたデータを使う場合

number_of_trial = 500
Order.uncached do
  Benchmark.bm 20 do |r|
    orders = Order.all
    r.report "Enumerable#inject" do
      number_of_trial.times do
        orders.inject(0){|sum, order| sum + order.price * order.quantity}
      end
    end

    r.report "Array#sum" do
      number_of_trial.times do
        orders.map{|order| order.price * order.quantity}.sum
      end
    end

    r.report "ActiveRecord#sum" do
      number_of_trial.times do
        orders.sum("price*quantity")
      end
    end
  end
end
                           user     system      total        real
Enumerable#inject      0.370000   0.000000   0.370000 (  0.408662)
Array#sum              0.350000   0.000000   0.350000 (  0.356449)
ActiveRecord#sum       0.260000   0.030000   0.290000 (  1.094346)

比較結果

  • Enumerable#inject を使うか Array#sum を使うかは、好みの話かも。
    • 該当コードの文脈に合わせてわかりやすい方を使えば良さそう
  • 合計値だけが欲しい場合は ActiveRecord#sum を使うと良さそう。
  • 個別のデータは他でよしなに使いつつ、ついでに合計値も出したい、というケースでは Enumerable#inject なり Array#sum を使った方が良さそう。

まとめ

今回はちょっとした集計ロジックを比較してみました。
データの取得が絡んでいるのでキャッシュ使わないよう uncached したり、処理時間を比較するために Benchmark を使ってみています。(詳しく調べたらまた記事にしようかな〜)

ではまた。