💎

【Rails】ActiveRecordはどのタイミングで実際にクエリを実行しているのか。SQLキャッシュについて。

2022/05/30に公開

はじめに

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...

これにはいくつかの例外があり、例えばfindfind_byの句を連鎖させることはできません。つまり、このタイミングでクエリが実行されることになります。

ActiveRecord Relations

以下のようなUser-Postsの1対多の関係があるとします。

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end
# app/models/post.rb
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は便利だが、扱いには気をつけたいです。
もし間違っている部分があれば、ご教授いただけますと幸いです。

参考

https://qiita.com/ykamez/items/0c81a33ec1b90219d541

https://zenn.dev/ledsun/books/700ccad6ad861d/viewer/dcdf69

https://www.honeybadger.io/blog/rails-activerecord-caching/

Discussion