rails 6.0.2
ruby 2.7.1
includesとは
N+1問題を起こさないように、関連テーブルのデータをキャッシュしてくれるメソッド。
しかし、関連テーブルのデータをキャッシュするといっても
一体どのようにして、データを取得してきているのだろうか。
実はincludesは、モデルの関連など見て以下のどちらかの最適とrailsが判断したメソッドを実行している。
(このrailsの判断が正しいとは限らない。*後述)
preload | eager_load |
---|---|
関連ごとにSQLを発行してキャッシュ。 | left_joinを用いて、キャッシュ。 |
preload、eager_loadがどの様なSQLを発行してるか見ていく。
以下の様なモデルがあるとする。
class User < ApplicationRecord
belong_to :country #国
has_many :posts #投稿
end
preload
preloadは、関連ごとにSQLを発行してキャッシュする。
いったいどういうことかというと、以下のようなrubyコードでは
User.all.preload(:posts,:country)
以下のようなsqlが発行される。
--user.allのsql
select * from users;
--postsのキャッシュのsql in句は、user.id
select * from posts where user_id in (1,2,3,4,5,6.....);
--countryのキャッシュのsql in句は、user.county_id
select * from countries where id in (1,2,3,4,5,6.....);
eager_load
left_joinを用いて、キャッシュ。
いったいどういうことかというと、以下のようなrubyコードでは
User.all.eager_load(:posts,:country)
以下のようなsqlが発行される。
--user.allのsql兼、postsのキャッシュ兼、countryのキャッシュ
select * from users u
left join posts p on p.user_id = u.id
left join countries c c.id = u.countries ;
以上の様にsqlが発行される訳だが、
railsのincludesがpreload,eager_loadの使い分けを完璧にできる訳ではない。
例えば、以下のように複数のテーブルをキャッシュしたい時、
countryだけ、preloadで他は全てeager_loadみたいなことは出来ない。
全部preload or 全部eager_load のどちらかである。
User.all.includes(posts: [:comments],:country,:profies)
preloadでキャッシャしたらまずいところ
eager_loadでキャッシュしたらまずいところがそれぞれあり、
railsでも使い分けをしている訳だが、
このようなことが起きると、意図せぬところでパフォーマンスの低下が起きてしまう。
したがって、各メリットデメリットを理解しておく必要がある。
メリットデメリット
preload
メリット
1.メモリの圧迫を防げる。
例えば、中間テーブルを持つN対Nの関係のテーブルをeaager_loadを使ってキャッシュした場合。
その実行結果のレコード数は単純に取得したかったデータ量よりもはるかに多くなってしまい、メモリを圧迫してしまうこ とになる。
デメリット
① IN句が大きくなりすぎる場合がある。
例えば、例で用いたrubyコードのUser.allが1万件あった場合、in句が膨大な長さになり、
以下の様なことが起きる。
・ ネットワークI/Oを圧迫する。
・ rdbmsによってin句に指定できる数には限界がある、Oracleは、1000個らしい。
参考 https://oreno-it.info/archives/816
② 関連先テーブルを使って絞り込みなどができない。
別のsqlでキャッシュをしてるので、関連テーブルのカラムをwhereで使うことはできない。
つまり以下の様なrubyコードはエラーになる。
irb(main)> User.preload(:country).where(countries: {name: '日本'})
ActiveRecord::StatementInvalid (PG::UndefinedTable: ERROR: missing FROM-clause entry for table "members")
LINE 1: SELECT "users".* FROM "users" WHERE "members"."name" = $1 LI...
eager_load
メリット
① 関連先テーブルを使って絞り込みなどが出来る。
以下が実行可能。
irb(main)> User.eager_load(:country).where(countries: {name: '日本'})
② 一対一,N対一の関連テーブルを取得するのに良い。
・一対一,N対一の関連の場合、結合の結果のレコードが増えるわけではないので、
結合によるメモリの圧迫は少ない。
・sqlの発行回数が少ない。(通信回数、rdbmsの構文解析などの時間が減る。)
デメリット
① 関連がN対ー,N対Nの場合のパフォーマンス
left_joinなので無駄なデータを沢山とってきてしまい、パフォーマンスは下がる。