Hisakeyのブログ

エンジニアが色々呟くブログです。

Railsの遅延実行を理解して、無駄なクエリを防ぐ

はじめに

RailsActiveRecordは、データベースとのやり取りを簡単にする強力なツールですが、遅延実行という仕組みを正しく理解していないと、無駄なクエリが発生します。

遅延実行と即時実行の違いを明確にすることで、無駄なクエリを発生させないようにするにはどうするかを記事にしてみました

私も、最近までデータを取得できていればよいと思っていましたが、パフォーマンスに響いてくるので正しく理解することで、よりよい開発ができるようになると思ってます!


1. 遅延実行と即時実行の違い

遅延実行とは?

遅延実行(Lazy Evaluation)は、必要になるまで実際のデータベースクエリを発行しない仕組みです。ActiveRecordでは、whereorderなどのメソッドを使うと、クエリはすぐには発行されず、必要なタイミングでまとめて実行されます。

遅延実行の例

# ここではまだクエリは発行されない
users = User.where(active: true).order(:created_at)

# この時点でクエリが発行される
users.each do |user|
  puts user.name
end
  • ポイント: whereorderはクエリの条件を積み重ねるだけで、eachなどのメソッドで初めて実行されます。

即時実行とは?

即時実行(Eager Evaluation)は、メソッドを呼び出した瞬間にクエリが発行される仕組みです。ActiveRecordでは、findfirstpluckなどが即時実行に該当します。

即時実行の例

# ここでクエリが即座に発行される
user = User.find(1)

# ここでも別のクエリが発行される
name = User.where(active: true).pluck(:name)
  • ポイント: findpluckはすぐにデータベースへアクセスし、結果を取得します。

2. 遅延実行を活用して無駄なクエリを減らす

問題の例:即時実行による無駄なクエリ

以下のコードは、良さそうなコードですが、複数回のクエリが発行されます。

# 無駄なクエリが発生する例
active_users = User.where(active: true)

active_users.each do |user|
  # 各ユーザーごとにクエリが発行される
  posts_count = user.posts.count
  puts "#{user.name} has #{posts_count} posts"
end

発行されるクエリ

  1. アクティブなユーザーを取得するクエリ
SELECT * FROM users WHERE active = true;
  1. 各ユーザーの投稿数を取得するクエリ(ユーザーごとに発行)
SELECT COUNT(*) FROM posts WHERE user_id = 1;
SELECT COUNT(*) FROM posts WHERE user_id = 2;
...

解決方法:遅延実行とpreloadを組み合わせる

preloadを使うことで、関連データを事前に読み込み、N+1問題を防ぐことができます。遅延実行を活用することで、クエリをまとめて発行できます。

# includesを使って関連データを一括取得
active_users = User.preload(:posts).where(active: true)

active_users.each do |user|
  # ここでは追加のクエリは発行されない
  posts_count = user.posts.size
  puts "#{user.name} has #{posts_count} posts"
end

発行されるクエリ

  1. アクティブなユーザーとその投稿を一括で取得するクエリ
SELECT * FROM users WHERE active = true;
SELECT * FROM posts WHERE user_id IN (1, 2, 3);
  • ポイント: preloadと遅延実行を組み合わせることで、必要なデータをまとめて取得し、余計なクエリを防げます。

3. 即時実行メソッドとの比較

即時実行を使った場合のクエリ数

# 即時実行によるクエリの例
user = User.find_by(email: 'user@example.com')  # 1回目のクエリ
post = Post.find_by(user_id: user.id, title: 'Sample Post')  # 2回目のクエリ
  • クエリ数: 2回

遅延実行を使った場合のクエリ数

# 遅延実行とpreloadを使った効率的な例
user = User.preload(:posts).find_by(email: 'user@example.com')
post = user.posts.find { |p| p.title == 'Sample Post' }
  • クエリ数: 1回(ユーザーと投稿をまとめて取得)

発行されるクエリ

SELECT "users".* FROM "users" WHERE "users"."email" = 'user@example.com' LIMIT 1;
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1);

4. まとめ

Railsの遅延実行を正しく理解し、クエリをまとめて発行することで、無駄なデータベースアクセスを防ぎ、アプリケーションのパフォーマンス向上につながります。特に、即時実行メソッドと遅延実行メソッドの違いを意識して使い分けることで、クエリの数を減らし、効率的なコードを書くことが可能になります。

大規模な処理やN+1問題の回避などで、あとから無駄なクエリを探すのは大変になるので、気づいたときに対処して行ければと思います!