Active Recordクエリの高速化: joins, preload, includes, eager_load の違いと使い分け
はじめに
Ruby on RailsのActive Recordは、データベースとのやり取りを抽象化してくれる非常に強力なORMです。しかし、その便利さの裏で、非効率なクエリを意図せず発行してしまうことがあります。特に、アソシエーション(関連)を持つモデルを扱う際にパフォーマンスを大きく左右するのが、joins, preload, includes, eager_loadといったメソッドの使い分けです。
これらのメソッドは似ているようで、内部的な動作や得意なシナリオが異なります。この記事では、それぞれのメソッドがどのようなSQLクエリを発行し、どのような場合に使うべきなのかを徹底的に比較・解説します。
前提となるモデル
以下の様な、User(投稿者)とArticle(記事)の1対多の関係を例に進めます。
class User < ApplicationRecord
has_many :articles
end
class Article < ApplicationRecord
belongs_to :user
end1. joins: 関連テーブルでの絞り込み
joinsは、SQLのINNER JOIN句を生成します。その主な目的は、関連テーブルの条件を使って、メインのモデルを絞り込むことです。
# アクティブなユーザーが投稿した記事だけを取得
Article.joins(:user).where(users: { active: true })発行されるSQL
SELECT
"articles".*
FROM
"articles"
INNER JOIN
"users" ON "users"."id" = "articles"."user_id"
WHERE
"users"."active" = TRUEjoinsの重要な注意点
joinsは、デフォルトでは関連先のモデルのデータ(この場合はusersテーブルのデータ)をメモリにロードしません。SELECT "articles".*となっている通り、articlesテーブルのカラムしか取得しません。
そのため、joinsで取得した結果に対して以下のように関連モデルにアクセスすると、N+1問題が発生します。
articles = Article.joins(:user).where(users: { active: true })
articles.each do |article|
puts article.user.name # ここでN+1クエリが発生!
endjoinsはあくまで絞り込みのためのメソッドであり、関連データを効率的に読み込むためのものではない、と理解することが重要です。
2. Eager Loading(事前読み込み)御三家
N+1問題を解決するために使われるのがEager Loadingです。preload, eager_load, そして両者の挙動を賢く使い分けるincludesの3つがあります。
preload: 個別のクエリで事前読み込み
preloadは、メインのモデルを取得するクエリと、関連モデルを取得するクエリの2つのクエリを個別に発行します。
Article.preload(:user).all発行されるSQL
-- 1. まず記事を取得
SELECT "articles".* FROM "articles"
-- 2. 次に、取得した記事のuser_idを使って、ユーザーをまとめて取得
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 5, ...) -- (記事のuser_idのリスト)preloadは、関連テーブルのカラムをwhere句などで使わない場合に、シンプルで効率的なクエリを発行します。
eager_load: LEFT OUTER JOINで事前読み込み
eager_loadは、LEFT OUTER JOINを使って、1つの巨大なクエリでメインのモデルと関連モデルのデータを一度に取得します。
Article.eager_load(:user).all発行されるSQL
SELECT
"articles"."id" AS t0_r0, ... , "articles"."updated_at" AS t0_r3,
"users"."id" AS t1_r0, ... , "users"."updated_at" AS t1_r2
FROM
"articles"
LEFT OUTER JOIN
"users" ON "users"."id" = "articles"."user_id"eager_loadの最大のメリットは、関連テーブルのカラムで絞り込み(where)を行いつつ、N+1問題を回避できることです。
# アクティブなユーザーの記事を取得し、かつユーザー情報も事前読み込み
Article.eager_load(:user).where(users: { active: true })この場合、preloadではwhere句でusersテーブルを参照できないため、eager_loadを使う必要があります。
includes: Railsにおまかせする賢い選択
includesは、これまでのpreloadとeager_loadの挙動をRailsが自動で判断してくれる、最も便利で一般的に使われるメソッドです。
# 関連テーブルの条件がない場合 → preloadと同じ挙動
Article.includes(:user).all
# 関連テーブルの条件がある場合 → eager_loadと同じ挙動
Article.includes(:user).where(users: { active: true })where(users: { ... })のように、referencesメソッドを使わずに直接関連テーブルの条件を指定すると、includesは自動的にeager_load(LEFT OUTER JOIN)を選択します。これにより、開発者は細かい使い分けを意識することなく、効率的なクエリの恩恵を受けることができます。
まとめ: 使い分けのフローチャート
どのメソッドを使うべきか、以下のフローチャートで判断できます。
graph TD
A[クエリの目的は?] --> B{関連データを表示・利用したい?<br>(N+1問題を避けたい)};
A --> C{関連データの条件で<br>絞り込みたいだけ?};
C --> D[<b>joins</b> を使う];
B --> E{関連データの条件で<br>絞り込みもしたい?};
E --> F[Yes];
E --> G[No];
F --> H[<b>includes</b> を使う<br>(内部でeager_loadが呼ばれる)];
G --> I[<b>includes</b> を使う<br>(内部でpreloadが呼ばれる)];| メソッド | SQL | 主な目的 | N+1問題の解決 | 関連テーブルでのwhere | 一般的な推奨度 |
|---|---|---|---|---|---|
joins | INNER JOIN | 関連データで絞り込み | × | ○ | △ (単体では) |
preload | 2つのSELECT | 関連データの事前読み込み | ○ | × | ○ |
eager_load | LEFT OUTER JOIN | 関連データで絞り込み かつ 事前読み込み | ○ | ○ | ○ |
includes | preloadかeager_loadを自動選択 | ほとんどのケースでこれを使う | ○ | ○ | ◎ (最推奨) |
joinsとincludesの組み合わせ
INNER JOINで絞り込みつつ、N+1問題も解決したい、という稀なケースでは、joinsとpreload(またはincludes)を明示的に組み合わせることもあります。
Article.joins(:user).where(users: { active: true }).preload(:user)Active Recordのクエリインターフェースを正しく理解し、使い分けることは、スケーラブルなRailsアプリケーションを構築するための必須スキルです。まずはincludesを基本とし、必要に応じて他のメソッドを検討するというアプローチから始めてみましょう。