1. DX支援サービス

    進化したデジタル技術を浸透させることで人々の生活をより良いものへと変革する

  2. ソフトウェア開発サービス

    VAREALだからできる、RubyとRuby on Railsに特化した、素早く柔軟なソフトウェア開発

  3. AI関連サービス

    データ活用と機械学習を用いたビジネスの着実な深化を。

  4. クリエイティブサービス事業

    美しいだけではない
    機能的UI/UXと正しいコーディング。

  1. SUNWEALC株式会社様 資産形成アプリ「enrich」のMVP開発支援

  2. 株式会社みらいワークス様 求人サイト「MOREWORKS」のメンテナンス支援

  3. 株式会社エムステージ様 Ruby/Railsのバージョンアップ支援

  4. 株式会社アイキューブドシステムズ様 CLOMO MDMのリプレイス開発支援

  5. ライオン株式会社様 「by me」のAI診断サービスの開発支援

  6. 株式会社TOEZ様 幼児向けのレッスン通信講座サイトおよび基幹システムの開発支援

  7. 株式会社カカクコム様 食べログノート の開発支援

  8. 有限会社秀栄社様 パーソナライズ絵本「JibunEHON」の開発支援

  9. 株式会社TRN様 不動産会社・建築会社向け_営業支援システム「renovo」の開発支援

  10. 株式会社Touch&Links様 新規CMSのシステム構築

  11. オフショア開発・長期ラボ型 Webアプリケーション開発事例/顧客ロイヤリティを高めるサービスの開発(株式会社ギフティ様)

  12. イベントサイト 「オンラインで集まろう 学研クリスマス&おとしだまウィーク」

  1. 株式会社ワイドウィンドウズ様 予約管理システムの既存機能の精査・変更と新規機能の追加およびUI/UXデザイン支援

  2. ライオン株式会社様 「by me」のAI診断サービスの開発支援

  3. 製造業のDX支援〜営業日報管理システム開発〜

  4. ウォータージェット加工.com サイトリニューアル

  5. 佳秀バイオケムサイトリニューアル

  6. 佳秀工業株式会社コーポレートサイトリニューアル

  7. 開発コンサルティング

  8. 団体管理システム

  9. ITコンサルティング

  1. 株式会社アイキューブドシステムズ様 CLOMO MDMのリプレイス開発支援

  2. 株式会社マネーフォワード様 マネーフォワード クラウドの開発支援

  3. 株式会社フレンバシー様 ベジタリアン、ヴィーガン向けのレストラン検索サイトの開発

  4. ライオン株式会社様 「by me」のAI診断サービスの開発支援

  5. 株式会社カカクコム様 食べログノート の開発支援

  6. 大手建設コンサルティング会社I社様 「自然災害を検知するAI」の開発 

  7. Webサイト訪問者分析のためのデータ分析基盤構築

  8. 製造業のDX支援〜営業日報管理システム開発〜

  9. 生産管理システム

  10. 仮想化サーバー導入

  11. タブレット端末導入

  1. VAREAL AI HUBの開発

  2. ライオン株式会社様 「by me」のAI診断サービスの開発支援

  3. 北海道大学様 オープンソースの大規模言語モデル(LLM)を使用したプロダクト共同研究開発

  4. 埼玉医科大学様 画像分類AIを用いた膠原病診断補助ツールの研究開発

  5. 大手建設コンサルティング会社I社様 「自然災害を検知するAI」の開発 

  1. ライオン株式会社様 「by me」のAI診断サービスの開発支援

  2. イベントサイト 「オンラインで集まろう 学研クリスマス&おとしだまウィーク」

  3. ウォータージェット加工.com サイトリニューアル

  4. 佳秀バイオケムサイトリニューアル

  5. スカイライト コンサルティング様コーポレイトサイトリニューアル

  6. ハイブリィド株式会社 様 [ IT-Manager SD ]

  7. 国際的機関の組織内システム開発

  8. 既存会計サービスのUI/UXデザイン改善

  9. 株式会社 クリニカル・トライアル 様 希少疾患SNS「RareS.(レアズ)」

  10. 人材マネジメントシステムUI/UXデザイン

  11. 保育園関連情報メディア開発

  12. Vareal株式会社中途採用情報サイト

Development

RailsエンジニアがN+1解決で使用するincludesの解説

ブログ作成者紹介

氏名:D・H
所属:開発部
入社年:2024年4月

解説の流れ

  1. はじめに
  2. 結論
  3. includesメソッドで条件を指定しない場合
  4. includesメソッドで条件を指定する場合
  5. まとめ

はじめに

includesメソッドの内部構造について解説します。 Railsで開発をしていると、N+1問題が発生することがあります。
多くのRailsエンジニアは、この問題を解決するためにincludesメソッドを使用します。
Railsでの開発を始めたばかりの頃は、includesメソッドの動作を十分に理解せずに使っていたため、同じような状況にある方々のために、includesメソッドの動作について解説します。

結論

まず結論から言います。
includesメソッドは、メソッドチェーンにwhereが入るかどうかによって、eager_loadとpreloadを使い分けています。
さらにいうと、whereで条件指定が入ると、eager_loadを使用し、条件指定が入らないとpreloadを使用します。

  • preload: 関連データを個別のSQLクエリでロードします。
  • eager_load: 関連データをテーブル結合(LEFT OUTER JOIN)してロードします。

includesは、eager_loadとpreloadを使い分けてN+1問題を解決してくれます。

説明のために使用するデータベース構成

単純でシンプルな1対1のリレーション構成になります。

includesメソッドで条件を指定しない場合

includesメソッドのみを使用した場合はpreloadメソッドと同じ挙動です。

includes

irb(main):010* User.includes(:profile).each do |user|
irb(main):011*   user.profile.bio
irb(main):012> end
  User Load (5.1ms)  SELECT "users".* FROM "users"
  Profile Load (1.2ms)  SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" IN ($1, $2, $3, $4, $5)  [["user_id", 1], ["user_id", 2], ["user_id", 3], ["user_id", 4], ["user_id", 5]]

preload

irb(main):007* User.preload(:profile).each do |user|
irb(main):008*   user.profile.bio
irb(main):009> end
  User Load (4.1ms)  SELECT "users".* FROM "users"
  Profile Load (1.4ms)  SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" IN ($1, $2, $3, $4, $5)  [["user_id", 1], ["user_id", 2], ["user_id", 3], ["user_id", 4], ["user_id", 5]]

生成されるSQL

SQLのログの内容を確認していただくと内容が同じなのがわかると思います。 ログだとわかりにくいので以下に記述し直しました。

SELECT "users".* FROM "users";
SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" IN (1, 2, 3, 4, 5);

User.includes(:profile)を実行すると、まずusersテーブルから全てのユーザーを取得します。 その後、取得したユーザーのIDを使って、profilesテーブルから関連するプロフィールを取得します。 具体的には、profilesテーブルのuser_idが、最初のクエリで取得したユーザーのIDに一致するレコードを取得します。

おまけ

counメソッドではなくsizeメソッドを使用しないとN+1の対策が無駄になりますので注意してください。 sizeメソッドは、既にロードされている関連オブジェクトの数を返すため、追加のSQLクエリを発行しません。
一方、countメソッドはSQLクエリを発行します。

includesメソッドで条件を指定する場合

includesメソッドはwhereで条件指定があると、LEFT OUTER JOINでテーブルを結合して、関連属性も同時に取得します。
これはeager_loadメソッドと同じ挙動です。 eager_loadは特定の条件がなくてもテーブルを結合します。 もしwhere句が含まれている場合、関連するテーブルを結合することで、条件に一致するデータのみを取得できます。

includes

irb(main):001* User.includes(:profile).where(profiles: {status: "active"}).each do |user|
irb(main):002*   puts user.profile.status
irb(main):003> end
SQL (0.3ms)  SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "profiles"."id" AS t1_r0, "profiles"."bio" AS t1_r1, "profiles"."status" AS t1_r2, "profiles"."user_id" AS t1_r3, "profiles"."created_at" AS t1_r4, "profiles"."updated_at" AS t1_r5 FROM "users" LEFT OUTER JOIN "profiles" ON "profiles"."user_id" = "users"."id" WHERE "profiles"."status" = $1  [["status", 0]]

eager_load

irb(main):004* User.eager_load(:profile).where(profiles: {status: "active"}).each do |user|
irb(main):005*   puts user.profile.status
irb(main):006> end
SQL (4.2ms)  SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "profiles"."id" AS t1_r0, "profiles"."bio" AS t1_r1, "profiles"."status" AS t1_r2, "profiles"."user_id" AS t1_r3, "profiles"."created_at" AS t1_r4, "profiles"."updated_at" AS t1_r5 FROM "users" LEFT OUTER JOIN "profiles" ON "profiles"."user_id" = "users"."id" WHERE "profiles"."status" = $1  [["status", 0]]

生成されるSQL

SQLのログの内容を確認していただくと内容が同じなのがわかると思います。 ログだとわかりにくいので以下に記述し直しました。 テーブルを結合して、自身と関連づけられた属性をまとめて取得しているのがわかります。 まとめて取得していることで無駄なSQLが発行されないようにしています。

SELECT "users"."id", "users"."name", "users"."created_at", "users"."updated_at",
       "profiles"."id", "profiles"."bio", "profiles"."status", "profiles"."user_id",
       "profiles"."created_at", "profiles"."updated_at"
FROM "users"
LEFT OUTER JOIN "profiles" ON "profiles"."user_id" = "users"."id"
WHERE "profiles"."status" = 'active';

注意

joinsメソッドやleft_outer_joinsメソッドというテーブル結合するだけのメソッドもありますが、こちらには関連テーブルの属性は含まれないのでN+1の解決にはなりません。
下のコードを見るととuserは全てのカラムを取得していますが、profileは属性が取得されていないことがわかります。
ただ、関連データの属性値でフィルタリングしたいだけの際はN+1を考慮する必要がない場合があります。
その際は関連属性をロードしないほうが処理が軽くなるので注意してください。

irb(main):020> User.joins(:profile).where(profile: {status:  "active"})
  User Load (4.8ms)  SELECT "users".* FROM "users" INNER JOIN "profiles" "profile" ON "profile"."user_id" = "users"."id" WHERE "profile"."status" = $1 /* loading for pp */ LIMIT $2  [["status", 0], ["LIMIT", 11]]

irb(main):021> User.left_outer_joins(:profile).where(profile: {status:  "active"})
  User Load (2.8ms)  SELECT "users".* FROM "users" LEFT OUTER JOIN "profiles" "profile" ON "profile"."user_id" = "users"."id" WHERE "profile"."status" = $1 /* loading for pp */ LIMIT $2  [["status", 0], ["LIMIT", 11]]

まとめ

一番大切なのは内部でどのようなSQLで構成されているのかをしっかりと把握することです。
SQLの内容を把握できていればプロジェクトのルールを優先してincludes、preload、eager_loadを使い分けて使用すれば良いと思います。

参考

https://railsguides.jp/active_record_querying.html#関連付けをeager-loadingする

関連記事

%d人のブロガーが「いいね」をつけました。