【rails】join先テーブルの指定カラムの合計値算出を370倍高速化した話【sum】

6952 ワード

前提知識

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_expensewageが、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で直接計算するので高速なのではないかと思います。

また、速度に加えて、余計なソースコードの削減にもなり一石二鳥ですね。

今回シンプルなケースですが、複雑に関連テーブルと結合している場合でも、結合先のテーブルのカラムの合計値を取得できるようです。とても便利だったので紹介させてもらいました。

今回の記事がどなたかの役に立にたてば幸いです!

参考