コントローラーやモデルでの処理でパフォーマンスを意識した書き方ができてなかったので、備忘録にまとめていきたいと思います。
① 繰り返し処理中でARオブジェクトの生成している例
今回はフロントエンドからIDの配列が送られてくるものとします。それに対してイテレートしてカラムデータを取得するというケースを考えます。まずはアンチパターンです。
article_titles = []
params[:article_ids].each do |article_id|
article = Article.find(article_id)
article_titles = article_titles.push(article.title)
end
この処理には問題点が2つあります。
- ループの都度SQLが発行されている
- 無駄にActive Recordのオブジェクトを生成させている
3行目のariticleを取得するSQLがariticle_idsの中のidが増えると比例して走ることになります。これは以下のようにリファクタリングすると、一回のSQLで済みます。
Article.where(id: params[:article_ids])
同じ箇所の3行目でarticleのオブジェクトを生成して、そこからtitleを取得しています。この場合pluckメソッドを使うとオブジェクトの生成を行わずにテーブルの値を取得することができます。pluckメソッドはActiveRecordインスタンスの配列ではなく指定されたカラムの取得値配列を返すメソッドです。
pluckメソッドを使うと次のように書き換えられます。
Article.where(id: params[:article_ids]).pluck(:title)
# (1.8ms) SELECT `articles`.`title` FROM `articles` WHERE `articles`.`id` IN (1, 2)
② レコードの存在確認でARオブジェクトを生成しているパターン
次に下のような例を考えてみます。
こちらは全subscriptionレコードの中で特定のユーザのものが存在するかをチェックするコードです。
bool = Subscription.all.none? { |s| s.user_id == params[:user_id] }
if bool
# do_something
end
ここで問題点が2つあります。
- subscriptionを全件取得するSQLが走っている
- user_idを確かめるためにARのオブジェクトをレコード分作成している
これらはレコード数があまりない場合は大した問題にはならないですが、増えていくと徐々にパフォーマンス上で支障をきたすようになります。
これらは以下のように書き直すことで、①と②両方の問題をなくすことができます。
bool = Subscription.exists?(user_id: cookies.encrypted[:user_id])
if bool
# do_something
end
exists?メソッドとは、指定した条件のレコードがデータベースに存在するかどうかを真偽値で返すメソッドです。存在すればtrueを存在しなければfalseを返します。
exists?メソッドを使って発行されたSQLを見ると、user_idが386のものがあるかどうか確認しているのが分かります。
Subscription Exists? (1.2ms) SELECT 1 AS one FROM `subscriptions` WHERE `subscriptions`.`user_id` = 386 LIMIT 1
これにより、すべてのデータを取得するという無駄が省けています。さらにARのオブジェクトを一つも生成していません。
試しにどれくらい影響があるかのベンチマークをとったので、紹介します。
def index
base_logger = ActiveRecord::Base.logger
ActiveRecord::Base.logger = nil
Benchmark.bm do |x|
x.report('全件比較') do
Subscription.all.none? { |s| s.user_id == cookies.encrypted[:user_id] } # subscriptionを全件取得し、またARを生成している
end
x.report('exists?') do
Subscription.exists?(user_id: cookies.encrypted[:user_id])
end
end
ActiveRecord::Base.logger = base_logger
end
上記のコードをsubscriptionレコードの量を増やしながら、どれだけ影響があるかを見ていきました。
user | system | total | real | |
全件比較 | 0.001022 | 0.000000 | 0.001022 | ( 0.001972) |
exists? | 0.000844 | 0.000000 | 0.000844 | ( 0.002199) |
user | system | total | real | |
全件比較 | 0.001358 | 0.000000 | 0.001358 | ( 0.003182) |
exists? | 0.000609 | 0.000000 | 0.000609 | ( 0.001791) |
user | system | total | real | |
全件比較 | 0.004583 | 0.000069 | 0.004652 | ( 0.014316) |
exists? | 0.000624 | 0.000000 | 0.000624 | ( 0.001739) |
user | system | total | real | |
全件比較 | 0.046396 | 0.003901 | 0.050297 | ( 0.153289) |
exists? | 0.001568 | 0.000000 | 0.001568 | ( 0.004984) |
レコード数が少ないときは両者に差はありませんでしたが、10000件ほどになるとかなりパフォーマンスに差が出てました。ローカル環境でデモしたところ、レコード数が10000件あると30倍以上早くなりました。
補足
少し分かりづらい人のためにSELECT 1 AS one FROM subscriptions;
の結果を載せておきます。
SELECT 1 AS one FROM `subscriptions`;
one
---
1
1
1
1
1
1
1
③ レコード数のカウントにSQLを用いているパターン
はじめに例を示します。
@subscriptions = Subscription.all
@subscriptions.size
これにより発行されるSQLは以下のようになります。
Subscription Count (2.1ms) SELECT COUNT(*) FROM `subscriptions`
COUNT文が走ってしまってますね。レコード数が少なかったり、例のようにテーブルの結合がない場合は速いですが、レコード数が多く、複数テーブルが結合しているとパフォーマンスに影響が出はじめます。
この場合ARオブジェクトでなく、Arrayに変換するとよいです。
@subscriptions = Subscription.all
arr_subscriptions = @subscriptions.to_a
arr_subscriptions.size
# SQL
# SELECT `subscriptions`.* FROM `subscriptions`
レコード数をカウントする場合はできる限りArrayを使う
まとめ
大事な点は以下です。
- Active Recordのオブジェクトはなるべく生成しない
- SQLの実行回数や取得レコード数はなるべく少なくする
- レコード数をカウントする場合はできる限りArrayを使う
これらは常に頭に入れて書いていかないとと思いました。レコード数が少ない内は気になりませんが、データ量が増えてくるとそれなりに大きな差になりました。
分かりにくいやアドバイス等ありましたらコメントくださると幸いです。では!
Twitterもやってますので、フォローしていただけるとうれしいです。
参考
https://tech.recruit-mp.co.jp/server-side/post-19614/
[…] […]