N+1 クエリとは?
N+1 クエリは、Rails アプリで Active Record などのObject Relational Mapping(ORM) を使用するときによく発生するパフォーマンスの問題です。 これは、ORM が単一のクエリを使用してすべてのオブジェクトを一度にロードするのではなく、オブジェクトのリストにあるオブジェクトごとに個別のクエリを実行するときに発生します。
たとえば、次のコードを考えてみましょう。
# Get all comments
comments = Comment.all
# For each comment, a new user query will be triggered
comments.each do |comment|
comment.user
end
Comments テーブルに 1000 レコードがある場合、以上のコードは、コメントしたユーザーを取得するために 1000件の個別のクエリを実行することになりますが、これは非常に非効率的になる可能性があります。 これは N+1 クエリの例です。
N+1 クエリは、特にクエリが JOIN や集計などの負荷の高い操作を実行している場合、Rails アプリの性能を大幅に低下させる可能性があります。 Rails アプリのパフォーマンスを最適化するには、パフォーマンスの問題を引き起こしている N+1 クエリを特定して修正することが重要です.
Rails で N+1 クエリを修正する方法
Eager loading associations
ほぼ同じ結果を達成できる 3 つの異なる方法がありますが、実行する内容はまったく異なります。 これらのメソッドは、preload、eager_load、および include です。
-
preload()
:where in
メソッドの利用
2.3.1 :110 > User.preload(:posts)
User Load (0.3ms) SELECT "users".* FROM "users"
Post Load (1.0ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9)
-
eager_load()
:join
メソッドの利用
2.3.1 :111 > User.eager_load(:posts)
SQL (0.7ms) SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "users"."post_id" AS t0_r4, "posts"."id" AS t1_r0, "posts"."title" AS t1_r1, "posts"."created_at" AS t1_r2, "posts"."updated_at" AS t1_r3, "posts"."user_id" AS t1_r4 FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
=> #<ActiveRecord::Relation [#<User id: 1, name: "David", created_at: "2018-04-24 11:42:42", updated_at: "2018-04-24 11:42:42", post_id: 1>, #<User id: 2, name: "Gate", created_at: "2018-04-24 11:42:42", updated_at: "2018-04-24 11:42:42", post_id: 2>, #<User id: 3, name: "Jack", created_at: "2018-04-24 11:42:42", updated_at: "2018-04-24 11:42:42", post_id: 2>, #<User id: 4, name: "David", created_at: "2018-04-24 12:44:37", updated_at: "2018-04-24 12:44:37", post_id: 1>, #<User id: 5, name: "Gate", created_at: "2018-04-24 12:44:37", updated_at: "2018-04-24 12:44:37", post_id: 2>, #<User id: 6, name: "Jack", created_at: "2018-04-24 12:44:37", updated_at: "2018-04-24 12:44:37", post_id: 2>, #<User id: 7, name: "David", created_at: "2018-04-24 12:44:39", updated_at: "2018-04-24 12:44:39", post_id: 1>, #<User id: 8, name: "Gate", created_at: "2018-04-24 12:44:39", updated_at: "2018-04-24 12:44:39", post_id: 2>, #<User id: 9, name: "Jack", created_at: "2018-04-24 12:44:39", updated_at: "2018-04-24 12:44:39", post_id: 2>]>
include()
# デフォルトでは、 include は select in() メソッドを使用します
2.3.1 :112 > User.includes(:posts)
User Load (0.3ms) SELECT "users".* FROM "users"
Post Load (0.4ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9)
# ただし、必要な場合、includes は joins メソッドを使用するためのreferencesfds を追加します。
2.3.1 :113 > User.includes(:posts).references(:posts)
SQL (0.7ms) SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "users"."post_id" AS t0_r4, "posts"."id" AS t1_r0, "posts"."title" AS t1_r1, "posts"."created_at" AS t1_r2, "posts"."updated_at" AS t1_r3, "posts"."user_id" AS t1_r4 FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
=> #<ActiveRecord::Relation [#<User id: 1, name: "David", created_at: "2018-04-24 11:42:42", updated_at: "2018-04-24 11:42:42", post_id: 1>, #<User id: 2, name: "Gate", created_at: "2018-04-24 11:42:42", updated_at: "2018-04-24 11:42:42", post_id: 2>, #<User id: 3, name: "Jack", created_at: "2018-04-24 11:42:42", updated_at: "2018-04-24 11:42:42", post_id: 2>, #<User id: 4, name: "David", created_at: "2018-04-24 12:44:37", updated_at: "2018-04-24 12:44:37", post_id: 1>, #<User id: 5, name: "Gate", created_at: "2018-04-24 12:44:37", updated_at: "2018-04-24 12:44:37", post_id: 2>, #<User id: 6, name: "Jack", created_at: "2018-04-24 12:44:37", updated_at: "2018-04-24 12:44:37", post_id: 2>, #<User id: 7, name: "David", created_at: "2018-04-24 12:44:39", updated_at: "2018-04-24 12:44:39", post_id: 1>, #<User id: 8, name: "Gate", created_at: "2018-04-24 12:44:39", updated_at: "2018-04-24 12:44:39", post_id: 2>, #<User id: 9, name: "Jack", created_at: "2018-04-24 12:44:39", updated_at: "2018-04-24 12:44:39", post_id: 2>]>
ロシア人形のキャッシュ
ロシアン ドール キャッシュは、フラグメント キャッシュを使用して、ページまたは部分のレンダリングされた出力全体と、それに含まれるネストされた部分をキャッシュすることによって機能します。 ページのレンダリングに必要なデータベース クエリの数を減らし、HTML 出力の生成に必要な処理量を減らすことにより、パフォーマンスを最適化します。
Rails で N+1 クエリを防ぐにはどうすればよいですか?
Rails アプリケーションで N+1 クエリを防ぐために使用できる対策が以下となります。
bullet gem
を利用する方法
Bullet gem は、アプリケーション内の N+1 クエリを自動的に検出して警告します。 また、すでに述べたインクルードを使用するなど、クエリを最適化する方法も提案します。
strict_loadingを使う
厳密なローディングは Rails 6.1 で導入されました。 Rails 6.1 より前には、Rails アプリケーションで厳密な読み込みを可能にする組み込みの方法はありませんでした。 関連付けにアクセスする前に関連付けが読み込まれているかどうかを手動で確認するか、Bullet gem などのツールを使用して N+1 クエリを検出する必要があります。
たとえば、多数の Comment モデルを含む Post モデルがあり、コメントの関連付けに対して厳密な読み込みを有効にするとします。 strict_loading オプションは次のように設定できます。
# Enable strict loading for the comments association on the Post model
class Post < ApplicationRecord
has_many :comments, strict_loading: true
end
# This will raise a StrictLoadingError exception, because the comments have not been loaded
Post.first.comments.each do |comment|
puts comment.body
end
この関連付けからのすべてのクエリに対して厳密な読み込みモードをアクティブにしたくない場合は、クエリごとにアクティブにすることもできます。
post = Post.strict_loading.first
# This will raise an ActiveRecord::StrictLoadingViolationError
post.comments.to_a