【Rails】ActiveRecordはどのタイミングで実際にクエリを実行しているのか。SQLキャッシュについて。
はじめに
ActiveRecordのメソッドを使ってDBアクセスすることができますが
実際にDBアクセスするタイミングは、実は即時でなかったりします(遅延評価)。
この辺りの理解を深めて、よりパフォーマンスを意識したコードが書けるように整理していきたいと思います。
また、最後に、同じリクエスト、同じ呼び出しであればRailsのモデルにキャッシュされるため
実際にDBアクセスは行われないので、この辺りにも触れたいと思います。
ActiveRecordの遅延評価
ActiveRecordクエリを作成する場合、多くの場合、コードはデータベースの即時呼び出しを実行しません。
これにより、毎回データベースにアクセスすることなく、.where
などで複数の句を連鎖させることができます(メソッドチェーン)。
@users = User.where(soft_destroyed_at: nil)
# DBアクセスしない
@users = @users.where(join_date: Date.today)
# このタイミングでもDBアクセスしない
@users.count
# DBアクセスする
(7.0ms) SELECT COUNT(*) FROM "users" WHERE...
これにはいくつかの例外があり、例えばfind
やfind_by
の句を連鎖させることはできません。つまり、このタイミングでクエリが実行されることになります。
ActiveRecord Relations
以下のようなUser-Postsの1対多の関係があるとします。
class User < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user
end
これにより、データベースクエリを実行して関連するレコードを見つけるための便利なuser.posts
メソッドが提供されます。
Railsアプリケーション内で、以下のサンプルコードを実行したとします。
@user.posts
ではクエリを実行せずに、@posts.each
...、この時点でActiveRecordはデータベースクエリを実行してデータを取得します。
@user = User.includes(:posts).find(1)
# DBアクセスする
@posts = @user.posts
# DBアクセスしない
@posts.each do |post|
end
# DBアクセスする
実際にログでも、@user.posts
ではクエリが実行されず、@posts.each
の部分で、クエリが実行されていました。
Post Load (299.7ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1
(ついでに)
以下のコードを実行した場合、同じリクエスト、同じ呼び出しはRailsのモデルにキャッシュされるため、2回目の@posts.each
ではクエリは実行されません。キャッシュからデータを取り出します。
# @userは、User.includes(:posts).find(1)で取得
@posts = @user.posts
@posts.each do |post|
end
# DBアクセスする
@posts.each do |post|
end
# DBアクセスしない
ActiveRecordに関連するレコードを再度取得したいとき
ActiveRecordのreload
をメソッドチェーンすると、キャッシュされたものを全て無視して、データベースから最新バージョンを取得するように指示させることができます。
@user = User.find(1)
@user.posts # DBアクセスなし
@user.posts # DBアクセスなし
@user.posts.reload # DBアクセスあり => Post Load (485.5ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1
@user.posts # DBアクセスなし
SQL CACHE
ActiveRecordは、パフォーマンスを高速化するために実行したクエリの内部キャッシュを保持してくれます。
ただし、このキャッシュは特定のアクションに関連付けられているので注意が必要です。アクションの開始時に作成され、アクションの終了時に破棄されます。
(1つのコントローラーアクション内で同じクエリを2回実行した場合など)
Railsアプリケーション内で、以下のようなサンプルコードを実行したとします。
User.count # ①
User.count # ②
実際にログを確認すると、ひとつめの呼び出しでは実際にSQLが実行されているのが分かります。
ふたつめの呼び出しでは、SQLの実行結果がすでにRailsのモデルにキャッシュされているため、実際にDBアクセスは行われず、モデルのキャッシュからデータを取得していることになります。
(86.0ms) SELECT COUNT(*) FROM `users`
CACHE (0.7ms) SELECT COUNT(*) FROM `users`
ちなみにirbなどのコンソールの場合では、SQLのキャッシュは行われないため
挙動の確認は、実際にアプリケーションを動かしてみるのがいいかと思います。
sample-project % rails c
Running via Spring preloader in process 49855
Loading development environment (Rails 6.0.3.6)
irb(main):001:0> User.count
(2.8ms) SELECT COUNT(*) FROM `users`
=> 137
irb(main):002:0> User.count
(5.0ms) SELECT COUNT(*) FROM `users`
=> 137
まとめ
ActiveRecordは便利だが、扱いには気をつけたいです。
もし間違っている部分があれば、ご教授いただけますと幸いです。
参考
Discussion