はじめに
先日アプリケーションに対するアクセス負荷によるDBダウンを経験しまして、業務でアプリケーション内のクエリ発行処理を見直しました。
Railsのクエリ発行がどこで行われているか
モデルを介してDBにアクセスする際に行われます。
controllerやmodelで行う以下のような処理でクエリが発行されます。
def index
@users = User.all
@user = User.find(params[:id])
@user2 = User.find_by(user_id: current_user.id,
profile_id: params[:profile_id])
@user3 = User.where(name: "aaaaa")
end
ローカルで起動している時、上記コードがコントローラーにある上でindexのページをロードします。
その最中にターミナルを凝視してください。
すると
字が小さくて申し訳ないです。
そしてこの画像のクエリは先程のコントローラーのやつとは別物です。参考画像です。
紫の文字が実際のクエリですね。
用はモデルに対してはfindとかallとかで指令を出していますが、
モデルはDBに対して紫文字のようなSQLを発行しています。
これがクエリ発行です。
この処理が多くなればなるほどページロードも遅くなり、ユーザビリティが悪くなります。
(1.6ms)とかというのはDBからのレスポンスの時間です。
これはローカルでの数値になるので実測値とは異なりますが、ここでも遅いか早いかの判断は付くと思います。
さらに言えば、今は1アクセスに対しての数字が出ているのですが、
実際の運用では同時何千アクセス、とかになるのでレスポンスにかかる時間はどんどん遅くなっていきます。
DBのスペックももちろん大事ですが、アプリケーション内で出来ることはやりましょうと。
実際にどう抑えていくか
とにかく1モデルに対してのアクセスは各アクション内で基本1回!
大量データを取得する処理(.all)とかがあるならその中から欲しいデータを抽出する!
どういうことかというと
例えば
def index
@users = User.all
@user = User.find(params[:id])
@user2 = User.find_by(user_id: current_user.id,
profile_id: params[:profile_id])
@user3 = User.where(name: "aaaaa")
end
みたいな処理があるとして、
これ実は凄い無駄なんですよ。
@usersはともかく、その他に関しては
モデルにアクセスする必要ないんですよね実は。
もうお気づきかもしれませんが、@users変数の中に、@userと@user2,@user3は既に入っているんです。
であればこのusersのデータの中からRubyのメソッドで欲しい情報を取ればいいんです!
def index
@users = User.all
@user = @users.to_a.detect{ |user| user.id == params[:id].to_i}
もしくは
@user = @users.to_a.find { |user| user if user.id == params[:id].to_i}
@user2 = @users.to_a.map { |user| user if user.id == current_user.id && user.profile_id == params[:profile_id].to_i}
@user3 = @users.to_a.select {|user| user.name == "aaaa"}
end
恐らくこんな感じで書き換えられます。
ただし、整合するデータのデータ型には気をつけないと上手くマッチしませんので、byebugとかでparams[:id].classとかでinteger型なのかString型なのかを判断するようにしましょう。
"1"と1ではデータ型が違うので、同じ物と見なされず==はfalseになります。配列を返してくれません。
※コードに関してはqiitaを書いている時に実際に検証はしていないので、コピーだとエラー出るかもしれません。
ですが、select map find detect collect など配列を返すメソッドを上手く使用することでモデルアクセス時のデータと同じものを大きいデータを持っている変数内から取ることができます。
効果
各コントローラー内で実装しました。
基本大きなデータを取っているのであればそれを使って、小さく整形していく。
重複のクエリは発行させない。
この処理を実装した翌日からアクセスの負荷は半分以下になり、ロードのスピードも実測値として上がることが確認できました!!!
注意事項
それならもう全てallで取ったろ!みたいに思うかもしれませんが、
一回に取るデータが大きくなればなるほど、処理は重くなるのでそこはお気をつけください。
あくまで大量データを絶対に取らなければならない場合に限り今回の整形は有用です。
また、同じデータをデータベースから取るにしても検索(一致)させる情報も多ければ多いほどレスポンスは早いということが分かりました。
def index
@users = User.where(user_id: current_user.id)
@users2 = User.where(user_id: current_user.id,profile_id: 1)
end
のようなコードがあったとして、
取得するデータは大差なくても処理にかかる時間は大きく変わるようです。
実際に私も安直にもっと大きな枠でデータ取得しとけばコントローラー内でのクエリ発行処理を0に出来ると思って、@users2から@usersのような処理に変更してコントローラー内のクエリ発行処理を全て無くしたところ、次の日からデータベースへのアクセス負荷数値は大幅に上がってしまいました。笑
なので最初にあった既存処理を変えてもっと大枠を狙う場合は負荷テストもしっかりした上で実装が望ましいです。
まとめ
地味な作業ですがシンプルに勉強にもなり、効果が目に見えて分かるのが面白いので機会があれば是非やってみてください。
分かりづらかったらコメントお待ちしております。