【rails】join先テーブルの指定カラムの合計値算出を370倍高速化した話【sum】
前提知識
job_offers
(求人)テーブル
field | type |
---|---|
wage(賃金) | integer |
transport_expense(交通費) | integer |
work_date(勤務日) | date |
2022年3月30日の全ての求人の賃金と交通費の合計値を出したい場合、
以下のようにすれば簡単に取得できます。
JobOffer.where(work_date: "2022-03-30".to_date.all_day).sum("wage + transport_expense")
(1.5ms) SELECT SUM(wage + transport_expense) FROM "job_offers"
=> 1000000
今回のテーマ
では、transport_expense
とwage
が、job_offers
テーブルのカラムではなく、別テーブルになっている設計の場合はどうすればいいでしょうか?
具体的にいうと以下のようなテーブル設計で、賃金と交通費の合計を算出したいケースですね。
job_offers
(求人)テーブル
field | type |
---|---|
work_date(勤務日) | date |
wages
(賃金)テーブル
field | type |
---|---|
job_offer_id(外部キー) | integer |
amount(金額) | integer |
transport_expenses
(交通費)テーブル
field | type |
---|---|
job_offer_id(外部キー) | integer |
amount(金額) | integer |
modelのassociation
は以下とします。
job_offer has_one wage
job_offer has_one transport_expense
自分ははじめ以下を想定しました。
N+1問題の回避のためにeager_load
で関連テーブルのデータごと取得して、rubyのメソッドEnumerable#inject
で合計値を算出する感じですね。
job_offers = JobOffer.where(work_date: "2022-03-30".to_date.all_day).eager_load(:wage, :transport_expense)
job_offers.inject(0) { |sum, job_offer| sum + job_offer.wage.amount + job_offer.transport_expense.amount }
(SQL略)
=> 1000000
リファクタ
上記でもよかったんですが、もっとうまくできる方法がないか思案したところ以下のような手段を発見しました。
join
した上で、sum("テーブル名.カラム名 + テーブル名.カラム名")
という書き方ですね。
JobOffer.where(work_date: "2022-03-30".to_date.all_day).joins(:wage, :transport_expense).sum("wages.amount + transport_expenses.amount")
(SQL略)
=> 1000000
Benchmarkで100回実行した結果を比較してたところ、以下のような結果になりました。
user system total real
リファクタ前 6.380000 0.216000 21.792 ( 23.098)
リファクタ後 0.059999 0.0 0.059 ( 0.545)
なんと、total
で比較すると約370倍高速化しているという結果に❗❗
リファクタ前は、取得したデータからモデルオブジェクトの構築を行なった後、rubyのループ処理で合計値を算出するのに対し、リファクタ後は、それらを行わずDBで直接計算するので高速なのではないかと思います。
また、速度に加えて、余計なソースコードの削減にもなり一石二鳥ですね。
今回シンプルなケースですが、複雑に関連テーブルと結合している場合でも、結合先のテーブルのカラムの合計値を取得できるようです。とても便利だったので紹介させてもらいました。
今回の記事がどなたかの役に立にたてば幸いです!
参考
Discussion