Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

ActiveRecordのパフォーマンス:N+1クエリのアンチパターン

AppSignalでは、開発者のアプリケーションパフォーマンスを支援します。数十億のリクエストを送信する膨大な数のアプリを監視しています。 Rubyとパフォーマンスに関するいくつかのブログ投稿でも少し役立つと思いました。 N + 1クエリの問題は、Railsアプリケーションでよく見られるアンチパターンです。

RailsのActiveRecordなどの多くのORMには遅延読み込みが組み込まれているため、クエリの関連付けを必要になるまで延期できます。この決定をビューにオフロードすることで、どの関連付けをロードする必要があるかを暗黙的に示すことができます。

N + 1クエリの問題は一般的ですが、通常は簡単に見つけられるパフォーマンスのアンチパターンであり、関連付けごとにクエリが実行されるため、データベースから多数の関連付けをクエリするときにオーバーヘッドが発生します。

👋ちなみに、この記事が気に入ったら、Ruby(on Rails)のパフォーマンスについて書いたものがたくさんあります。Rubyのパフォーマンス監視チェックリストを確認してください。

ActiveRecordでの遅延読み込み

ActiveRecordは、暗黙的な遅延読み込みを使用して、リレーションの操作を容易にします。各製品が存在するウェブショップの例を考えてみましょう。 バリアントはいくつでも持つことができます たとえば、製品の色やサイズが含まれています。

# app/models/product.rb
class Product < ActiveRecord::Base
  has_many :variants
end

ProductsController#show内 、製品の1つの詳細ビューでは、Product.find(params[:id])を使用します 製品を入手して@productに割り当てます 変数。

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
  end
end

このアクションのビューでは、variantsを呼び出して、製品のバリアントをループします。 @productのメソッド コントローラから受け取った変数。

# app/views/products/show.html.erb
<h1><%= @product.title %></h1>
 
<ul>
<%= @product.variants.each do |variant| %>
  <li><%= variant.name %></li>
<% end %>
</ul>

@product.variantsを呼び出す ビューでは、Railsはデータベースにクエリを実行して、ループオーバーするバリアントを取得します。コントローラで行った明示的なクエリとは別に、このリクエストについてRailsのログを確認すると、バリアントをフェッチするために別のクエリが実行されていることがわかります。

Started GET "/products/1" for 127.0.0.1 at 2018-04-19 08:49:13 +0200
Processing by ProductsController#show as HTML
  Parameters: {"id"=>"1"}
  Product Load (1.1ms)  SELECT  "products".* FROM "products" WHERE "products"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Rendering products/show.html.erb within layouts/application
  Variant Load (1.1ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ?  [["product_id", 1]]
  Rendered products/show.html.erb within layouts/application (4.4ms)
Completed 200 OK in 64ms (Views: 56.4ms | ActiveRecord: 2.3ms)

このリクエストは2つのクエリを実行して、すべてのバリエーションを持つ商品を表示しました。

  1. SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1
  2. SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 1

ループ遅延読み込み

これまでのところ、遅延読み込みは素晴らしいものでした。暗黙的なクエリを使用することで、たとえば、このビューにバリアントを表示したくないと判断した場合に、コントローラーからクエリを削除することを覚えておく必要がありません。

ProductsController#indexに取り組んでいるとしましょう 、ここでは、すべての製品とその各バリエーションのリストを表示します。以前と同じ方法で遅延読み込みを使用して実装できます。

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
  end
end
# app/views/products/index.html.erb
<h1>Products</h1>
 
<% @products.each do |product| %>
<article>
  <h1><%= product.title %></h1>
 
  <ul>
    <% product.variants.each do |variant| %>
      <li><%= variant.description %></li>
    <% end %>
  </ul>
</article>
<% end %>

最初の例とは異なり、単一の製品ではなく、コントローラーから製品のリストを取得するようになりました。次に、ビューは各製品をループし、各製品の各バリアントをレイジーロードします。

これは機能しますが、1つの問題があります。クエリ数はN+ 1になりました 。

N+1クエリ

最初の例では、単一の製品とそのバリエーションのビューをレンダリングしました。 クエリ数 2つのクエリを実行したため、2でした。このリクエストは、データベースからすべての製品(この例では3つ)とその各バリアントを返し、2つではなく4つのクエリを実行しました。

Started GET "/products" for 127.0.0.1 at 2018-04-19 09:49:02 +0200
Processing by ProductsController#index as HTML
  Rendering products/index.html.erb within layouts/application
  Product Load (0.3ms)  SELECT "products".* FROM "products"
  Variant Load (0.2ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ?  [["product_id", 1]]
  Variant Load (0.2ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ?  [["product_id", 2]]
  Variant Load (0.1ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = ?  [["product_id", 3]]
  Rendered products/index.html.erb within layouts/application (5.6ms)
Completed 200 OK in 36ms (Views: 32.6ms | ActiveRecord: 0.8ms)
  1. SELECT "products".* FROM "products"
  2. SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 1
  3. SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 2
  4. SELECT "variants".* FROM "variants" WHERE "variants"."product_id" = 3

Product.allへの明示的な呼び出しによって実行される最初のクエリ コントローラで、すべての製品を検索します。後続の製品は、ビュー内の各製品をループしながら遅延実行されます。

この例では、クエリ数がN + 1になります。ここで、Nは製品の数であり、追加されたものはすべての製品をフェッチした明示的なクエリです。言い換えると;この例では、1つのクエリを実行してから、最初のクエリの結果ごとに別のクエリを実行します。この例ではN=3であるため、結果のクエリ数はN + 1 = 3 + 1 = 4になります。 。

製品が3つしかない場合、これは実際には問題にならないかもしれませんが、クエリ数は製品の数とともに増加します。このリクエストにはN+1クエリがあることがわかっているため、100個の商品がある場合(N + 1 = 100 + 1 = 101)、クエリ数101を予測できます。 )、たとえば。

積極的な読み込みの関連付け

現在のように製品の数でクエリの数を増やすのではなく、このビューでリクエストの数を静的にしたいと考えています。これを行うには、ビューをレンダリングする前に、コントローラーにバリアントを明示的にプリロードします。

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all.includes(:variants)
  end
end

ActiveRecordのincludes queryメソッドは、関連付けられたバリアントがそれらの製品とともにロードされていることを確認します。どのバリアントを事前にロードする必要があるかを知っているため、1つのクエリで要求されたすべての製品のすべてのバリアントをフェッチできます。

Started GET "/products" for 127.0.0.1 at 2018-04-19 10:33:59 +0200
Processing by ProductsController#index as HTML
  Rendering products/index.html.erb within layouts/application
  Product Load (0.3ms)  SELECT "products".* FROM "products"
  Variant Load (0.4ms)  SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (?, ?, ?)  [["product_id", 1], ["product_id", 2], ["product_id", 3]]
  Rendered products/index.html.erb within layouts/application (5.9ms)
  Completed 200 OK in 45ms (Views: 40.8ms | ActiveRecord: 0.7ms)

バリアントをプリロードすることで、将来的に商品の数が増えても、クエリ数は2に戻ります。

  1. SELECT "products".* FROM "products"
  2. SELECT "variants".* FROM "variants" WHERE "variants"."product_id" IN (1, 2, 3)

怠惰ですか、それとも熱心ですか?

ほとんどの場合、1回のクエリでデータベースから関連するすべてのレコードを取得する方が、遅延読み込みよりもはるかに高速です。

このサンプルアプリケーションでは、データベースのパフォーマンスの違いは、それぞれが10のバリアントを持つ3つの製品のみで測定できます。平均して、製品リストの積極的な読み込みは、遅延読み込みよりも約12.5%高速です(0.7ミリ秒対0.8ミリ秒)。 10個の製品を使用すると、その差は59%に跳ね上がります(1.22ミリ秒対2.98ミリ秒)。 1000個の製品では、熱心なクエリが58.4ミリ秒でクロックインするのに対し、遅延読み込みには約290.12ミリ秒かかるため、差はほぼ80%です。

遅延ロードされた関連付けにより、コントローラーを更新しなくてもビューの柔軟性が高まりますが、経験則として、データをビューに渡す前に、コントローラーにデータのロードを処理させることをお勧めします。

ビューからの遅延読み込みは、1つのモデルオブジェクトとその関連付け(ProductsController#showなど)を表示するビューで機能します 最初の例では)、たとえば、同じコントローラーからの異なるデータを必要とする複数のビューがある場合に役立ちます。

猫と人形

猫は同意しないかもしれませんが、怠惰ではなく熱心であることが報われることがあります。この投稿では、ActiveRecordでの遅延読み込みについて詳しく説明し、パフォーマンスの問題が発生する可能性のある状況の例を示しました。 N+1クエリの問題が発生する場合と同様です。

つまり、開発ログまたはAppSignalのイベントタイムラインを常に監視して、遅延読み込みが発生する可能性のあるクエリを実行していないことを確認し、特に処理されるデータの量が増加した場合に応答時間を追跡します。 。

これが気に入った場合は、パフォーマンスとモニタリングについて書いたものをさらにチェックしてください。たとえば、ロシアの人形のキャッシングに関するこのお気に入りや、条件付き取得リクエストに関するこのお気に入りなどです。


  1. Windows PC のパフォーマンスを向上させる手順

    パソコンの動作が遅い?これは、世界中の何百万人もの人々が直面している問題であり、Microsoft でさえ Windows の更新がリリースされるたびに修正に苦労しています。幸いなことに、コンピューターが非常に古いものであっても、コンピューターの速度を上げることができる一連の簡単な手順があります。これがあなたがする必要があることです… コンピュータを高速に動作させることは、コンピュータの動作を遅くしている問題を修正することにすぎません。実際、すべて コンピュータは、そのためのパワーとリソースがあれば高速に実行できます。ただし、多くのコンピューターは、Windows でよく発生するさまざま

  2. Windows 11 で最高のパフォーマンスを得る方法

    Windows 11 オペレーティング システムがコンシューマ向けにリリースされ、多くの新機能が導入されました。 [スタート] メニューをカスタマイズして、ユーザーのお気に入りのプログラムを表示したり、UI の側面を変更して、ユーザーに新鮮な体験を提供したりできます。 メモリ管理、ディスク使用、および CPU 使用とバッテリー寿命を扱うその他の要因を改善するための Microsoft の取り組みのおかげで、OS のパフォーマンスもここ数か月でいくつかの改善が見られました。 ただし、Windows 11 は新しいコンピューターでよりスムーズかつ高速に実行されますが、Windows 11 の強