Active Recordにおいて、親子関係にあるテーブルのデータを取得する際に、N+1問題を解消するためにincludesをよく使いますが、使い方についてあまり理解できていなかったのでその学習をまとめます。
また調べていく中で多くの方が「eager_loadとpreloadを使用すべき」と言っています!
その理由も調べてまとめます!
N+1問題とは?
N+1問題 とは、ループ処理の中で都度クエリを発行してしまい、大量のクエリが発行されてしまうことです。余計なクエリが発行されるということは、データの取得や参照に時間がかかってきてしまいパフォーマンスに影響が出ます。
コーディングなどで下記のような同じようなログが大量に出てきてしまうことはありませんか??
ループ処理の中で都度クエリを発行してしまい、大量のクエリが発行されてしまっています。
これがN+1問題です。
↳ app/views/articles/index.html.slim:18
ColumnCategory Load (0.8ms) SELECT `column_categories`.* FROM `column_categories` INNER JOIN `article_column_categories` ON `column_categories`.`id` = `article_column_categories`.`column_category_id` WHERE `article_column_categories`.`article_id` = 〇〇
↳ app/views/articles/index.html.slim:26
Admin Load (0.2ms) SELECT `admins`.* FROM `admins` WHERE `admins`.`id` = 〇〇 LIMIT 1
↳ app/views/articles/index.html.slim:39
ColumnCategory Load (0.1ms) SELECT `column_categories`.* FROM `column_categories` INNER JOIN `article_column_categories` ON `column_categories`.`id` = `article_column_categories`.`column_category_id` WHERE `article_column_categories`.`article_id` = 〇〇
↳ app/views/articles/index.html.slim:26
Admin Load (0.3ms) SELECT `admins`.* FROM `admins` WHERE `admins`.`id` = 〇〇 LIMIT 1
↳ app/views/articles/index.html.slim:39
ColumnCategory Load (0.1ms) SELECT `column_categories`.* FROM `column_categories` INNER JOIN `article_column_categories` ON `column_categories`.`id` = `article_column_categories`.`column_category_id`
上記の例のように、大量のクエリを発行することでパフォーマンスの低下を招くN+1問題を回避するために、includesメソッドを使っていました。
includesメソッドとは?
includesメソッドは、Railsで関連するレコードを効率的に取得するために使用されます。N+1問題を防ぐために、関連するモデルを一度に読み込み、データベースクエリの回数を削減します。
使い方としては、下記のように使用します。
#子モデル1つの場合
Columun.includes(:user)
#子モデル2つの場合
Columun.includes(:user, :tags)
#子モデル2つの場合
Columun.includes(user: :state)
#孫モデルが複数ある場合
Columun.includes(user: [:state, :comments])
そしてここからが重要で、includesメソッドは条件に応じて、preloadとeager_loadのいずれかの処理を行います。
preloadとeager_loadとは?
includes は内部で preload と eager_load のどちらかを自動的に選択されるようになっています。
※preload と eager_load はincludesと同じように単体でメソッドしても使用できます。
また調べていくとincludesが自動で行う処理の選択が常に最適とは限りません。複雑なクエリの場合、予期しないJOINが発生したり、複数のクエリが発行されてパフォーマンスが低下することがあるとわかりました。そのためより細かく制御できる preload または eager_load を明示的に使用する方が推奨されているとのことでした。
preload
メインのクエリで主なレコードを取得し、その後、別のクエリで関連するレコードを取得します。
特徴: 2回のSQLクエリが発行されますが、それでもN+1問題を防ぐことができます。複雑なSQLの結合が必要ない場合に適しています。
User Load (0.2ms) SELECT `users`.* FROM `users`
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3)
eager_load
メインのクエリでLEFT OUTER JOINを使用し、関連するレコードを一度に取得します。
特徴: 1回のSQLクエリで全てのデータを取得しますが、JOINが複雑になる可能性があります。データを一度に取得したい場合に適しています。
User Load (0.5ms) SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `posts`.`id` AS t1_r0, `posts`.`title` AS t1_r1
FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id`
eager_loadとpreloadの推奨ケース
下記のマネーフォワード方がかいた記事を参照すると
記事:
https://moneyforward-dev.jp/entry/2019/04/02/activerecord-includes-preload-eagerload/
内容:
belongs_to, has_one アソシエーションについては eager_loadして、has_many なアソシエーションについてはpreloadすることを基本線としています。includesはクエリが状況によって変わってコントロールしずらいので基本使わないようにしています。
belongs_to, has_one アソシエーションについては、1対1あるいはN対1関連なのでSQLを分割して取得するより、left joinでまとめて取得した方が効率的な事が多いと思っています。 一方、has_many アソシエーションについてはeager_loadしておくと、以下に書くようにslow queryを踏みやすいためpreloadを基本にするのが良いと思っています。
とのことでした。詳細な内容についてはここでは触れませんので気になった方は記事を見てみてください。
まとめ
今回は以上の内容から、includes メソッドを使用するのではなく、eager_load と preload を明示的に使う方が、クエリの動作をより細かく制御でき、パフォーマンスを最適化するために適しています。特に、アソシエーションの種類やクエリの複雑さに応じて使い分けることで、N+1問題を効果的に回避できます。