背景
自社コンテンツのアクセス数が大きく伸び、コンテンツ内のコードを見返すことになりました。
最初に行った取り組みはActiveRecordを経由してSQLを発行するコードを極力減らすこと。
詳細は下記に。
ざっくり言うと大枠でデータを取得して、同じアクション内であればそのデータの中から細かいデータをRubyのコードで取得しようというものです。
これだけでも相当コンテンツのロードの時間は速くなりますし、データベースへの負荷も大幅に減りました。
しかしながら最初にリファクタリングしてからもコンテンツのアクセス数は伸び続け、初期の改修だけでは足りないという事態に。
そこで今回初回記事より一歩踏み込んだ形でロードの時間を削減できたので下記にまとめます。
アプリケーションの構造によっては今回のやり方を使用しない方が良いことも多々あるので、ログやABテストなどで検証した上での実装をお勧めします。
開発環境
※今回は実務のコードはお見せできないので、私のテスト環境で再現いたします。
・Rails version -5.2.4.4
・Ruby version -2.5.7
・DB -sqlite
テスト用に使用するモデル情報
PostImage_model
・Id
・user_id
・body
・hashbody
・created_at
・update_at
上記のカラムを持つ。
リファクタリング前コード
@current_user_postimages = PostImage.where(user_id: current_user.id)
こちら割とメジャーな書き方なのかなと思っています。
発行されるクエリ
PostImage Load (0.4ms) SELECT "post_images".* FROM "post_images" WHERE "post_images"."user_id" = ? [["user_id", "1"]]
結論から言うとこれは遅いようです。
私も原理はわかっていないですが、
・検索をさせる為の項目(Where条件)が少なすぎること
・Record全ての情報を取ってきてしまうことが遅さにつながるようです。
初回リファクタリング
見直さなければいけないこと
・もっと検索の条件を指定出来ないか。
・そもそもこのレコードの中の何を使用、出力するのか。(レコードの情報を全て取る必要があるか)
先ほど取ってきた情報のうち1レコード
#<PostImage id: 1, body: "テスト投稿\r\nテスト投稿", hashbody: "#test", user_id: "1", created_at: "2021-03-07 04:28:43", updated_at: "2021-03-07 04:28:43">
例えば順番に表示する必要がないのであれば、そもそもcreated_atやupdate_atが必要無いことが分かるので、取得情報をまずは絞る。
@current_user_postimages = PostImage.where(user_id: current_user.id).select(:id,:user_id,:body,:hashbody)
発行されるクエリ
PostImage Load (0.4ms) SELECT "post_images"."id", "post_images"."user_id", "post_images"."body", "post_images"."hashbody" FROM "post_images" WHERE "post_images"."user_id" = ? [["user_id", "1"]]
これで少しは速くなるはずです。
ただこれはselectの内容が多すぎるので結果的にあまり変わりません。
下記のように取得する情報をもっと限定的にすればもっと高速化が狙えると感じました。
@current_user_postimages = PostImage.where(user_id: current_user.id).select(:body)
発行されるクエリ
PostImage Load (0.3ms) SELECT "post_images"."body" FROM "post_images" WHERE "post_images"."user_id" = ? [["user_id", "1"]]
取得したレコード
>> @current_user_postimages=> #<ActiveRecord::Relation [#<PostImage id: nil, body: "テスト投稿\r\nテスト投稿">, #<PostImage id: nil, body: "Test2\r\ntest2\r\ntest2">, #<PostImage id: nil, body: "aaaaaaabbbbbbbb">, #<PostImage id: nil, body: "aaaa">, #<PostImage id: nil, body: "aiueo">, #<PostImage id: nil, body: "aiueo">, #<PostImage id: nil, body: "aiueo">, #<PostImage id: nil, body: "aiueo">, #<PostImage id: nil, body: "aiueo">, #<PostImage id: nil, body: "aiueo">, ...]>
>> @current_user_postimages[1]=> #<PostImage id: nil, body: "Test2\r\ntest2\r\ntest2">
>> @current_user_postimages[1].body=> "Test2\r\ntest2\r\ntest2"
>> @current_user_postimages[2]=> #<PostImage id: nil, body: "aaaaaaabbbbbbbb">
>> @current_user_postimages[2].body=> "aaaaaaabbbbbbbb"
bodyの情報しか無い状態です。
@current_user_post_images[i].bodyという情報しか出力できません。
ローカル環境なので0.4msから0.3msとあまり大きな差は無いですがレコード数が多いであろう本番の環境であれば、この小さな積み重ねが生きてくると思います。
第二回リファクタリング
どうせやるなら限界までやりたい。
アプリケーション側でやれることはもう無いですというところまで持っていきたいと考えた私は、
その後も色々と調べて恐らくこれをすればかなり速くなるというものを発見しました。
こちらです。
@current_user_postimages = PostImage.where(user_id: current_user.id).pluck(:body)
発行されるクエリ
(0.2ms) SELECT "post_images"."body" FROM "post_images" WHERE "post_images"."user_id" = ? [["user_id", "1"]]
取得したレコード
>> @current_user_postimages=> ["テスト投稿\r\nテスト投稿", "Test2\r\ntest2\r\ntest2", "aaaaaaabbbbbbbb", "aaaa", "aiueo", "aiueo", "aiueo", "aiueo", "aiueo", "aiueo", "aiueo"]
>> @current_user_postimages[2]
=> "aaaaaaabbbbbbbb"
>> @current_user_postimages[3]
=> "aaaa"
>> @current_user_postimages[4]
=> "aiueo"
今回使用してきたメソッドにおいては最速?だと思います。
whereの後に.pluckをつけることでActiveRecordのインスタンスを作成しないで配列として情報を取得します。引数には必要なカラムの情報をシンボルで渡します。
よく見ると「PostImage Load(0.4ms)」などという表示が無くなっております。
ただし戻り値が上記のようにArray型になるのでデータの扱い方が若干異なります。
@current_user_postimages[i].bodyと今まで出力していたものは、@current_user_postimages[i]のみで出力するようになります。
それと一応レコードが無い場合の戻り値も異なります。
#レコード情報が無い場合 pluckを使用
@current_user_postimages = PostImage.where(user_id: current_user.id,body: 'aiuelsogheiuh').pluck(:body) #=> []
#pluckを不使用
@current_user_postimages = PostImage.where(user_id: current_user.id,body: 'aiuelsogheiuh')=> #<ActiveRecord::Relation []>
これはあまり心配しなくても良さそうな気がします。
ちなみに・・・
pluckもselect同様に複数の引数を入れることができます。
例えば下記のようにです。
@current_user_postimages = PostImage.where(user_id: current_user.id).pluck(:id,:user_id,:body)
クエリ
(0.3ms) SELECT "post_images"."id", "post_images"."user_id", "post_images"."body" FROM "post_images" WHERE "post_images"."user_id" = ? [["user_id", "1"]]
取得した値
@current_user_postimages=> [[1, "1", "テスト投稿\r\nテスト投稿"], [2, "1", "Test2\r\ntest2\r\ntest2"], [3, "1", "aaaaaaabbbbbbbb"], [4, "1", "aaaa"], [5, "1", "aiueo"],
>> @current_user_postimages[2]
=> [3, "1", "aaaaaaabbbbbbbb"]
>> @current_user_postimages[3]
=> [4, "1", "aaaa"]
>> @current_user_postimages[4]
=> [5, "1", "aiueo"]
こちらの中でbody情報のみ取る場合は
@current_user_postimages = PostImage.where(user_id: current_user.id).pluck(:id,:user_id,:body)
@body = @current_user_postimages.map{|record| record[2]} #=> > ["テスト投稿\r\nテスト投稿", "Test2\r\ntest2\r\ntest2", "aaaaaaabbbbbbbb", "aaaa", "aiueo"]
上記のようにmapを使用して新たに配列として格納すればbodyの情報のみ入っている配列が出来上がります。
また、条件式を使って配列を作成する場合は
#aがbodyに含まれているものを検索したい
@current_user_postimages = PostImage.where(user_id: current_user.id).pluck(:id,:user_id,:body)
@body = @current_user_postimages.map{|record| record[2] if record[2].include?("a")} #=> [nil, nil, "aaaaaaabbbbbbbb", "aaaa", "aiueo", "aiueo", "aiueo]
#なぜかnilまで入ってくるので更に下記のようにする
@body = @current_user_postimages.map{|record| record[2] if record[2].include?("a")}.compact #=> ["aaaaaaabbbbbbbb", "aaaa", "aiueo", "aiueo", "aiueo]
#重複レコードも消したい
@body = @current_user_postimages.map{|record| record[2] if record[2].include?("a")}.compact.uniq #=> ["aaaaaaabbbbbbbb", "aaaa", "aiueo"]
などなど。
Arrayに使えるメソッドを使いデータを綺麗にして使えるようにできます。
Array.instance_methods #=> 使えるメソッドが沢山出てくる。
終わりに
正直私も完璧には分かっていないのですが、
もし同じような悩みを抱えている方がいらっしゃった時、
一つの選択肢になればと思い書かせていただきました。
最速?と書いてしまいましたが、もしもっと速くする方法があれば是非お教え願います。
また、ご指摘などあればよろしくお願いいたします。