NPlus1
N + 1
は、単一のオブジェクトグラフをロードするのに N + 1 個の SQL クエリが必要になる状況を表す、ORM で使用される一般的な用語です。データベースがネットワーク経由でリモートにある場合 (最も一般的な状況)、この状況はパフォーマンスの低下につながる可能性があります。優れた ORM は、このシナリオを軽減または回避するための優れたメカニズムを提供します。
デフォルトでは、Ebean ORM はバッチサイズ 10 のバッチ遅延ロードを適用するため、完全にチューニングされていないクエリの場合、代わりに 1 + N/10
個の SQL クエリが観測されるでしょう。
N とは何ですか?
遅延ロードが OneToMany または ManyToMany 関係で呼び出される場合、N は個別の親 ID の数です。たとえば、100 件の注文をクエリし、注文詳細 (OneToMany) を遅延ロードする場合、N は個別の注文 ID の数です。
ManyToOne または OneToOne 関係を遅延ロードする場合、N はその Bean 型の個別の ID の数です。たとえば、100 件の注文をクエリし、顧客を遅延ロードする場合、N はそれらの注文に関連する個別の顧客 ID の数です。
N は「パス」ごとです
遅延ロードは、オブジェクトグラフのどの「パス」がたどられたかに応じて呼び出されます。注文を取得し、注文から「顧客」と「詳細」の両方をたどる場合、それは実際には 2 つの異なるパスであり、2 つの異なる遅延ロードクエリが実行されます。
たとえば、100 件の注文を取得し、各注文から「顧客」と「詳細」をたどると、遅延ロードのバッチサイズに応じて、3 から 201 個の SQL クエリが実行される可能性があります。
1 + (顧客数)/バッチサイズ + (注文数)/バッチサイズ
SQL クエリ。
- バッチサイズ 1 の場合、最大: 1 + 100/1 + 100/1 = 最大 201 個の SQL クエリ
- バッチサイズ 10 の場合、最大: 1 + 100/10 + 100/10 = 最大 21 個の SQL クエリ
- バッチサイズ 100 の場合、最大: 1 + 100/100 + 100/100 = 最大 3 個の SQL クエリ
N + 1 の問題は、たどられたパス / オブジェクトグラフのどの部分が使用されたかに依存します。
遅延ロードのバッチサイズ
Ebean では、デフォルトの遅延ロードバッチサイズは 10
です。遅延ロードのバッチサイズは変更できます。
- ServerConfig.setLazyLoadBatchSize() を使用してグローバルに
- Query.setLazyLoadBatchSize() を使用してクエリごとに
- Query.fetch() および FetchConfig を使用してクエリパスごとに
グローバルデフォルト
グローバルデフォルトの遅延ロードバッチサイズを 100 に変更する場合は、ServerConfig.setLazyLoadBatchSize()
を使用して設定できます。
serverConfig.setLazyLoadBatchSize(100);
... または ebean.properties を使用
ebean.lazyLoadBatchSize=100
クエリごと
特定のクエリで遅延ロードのバッチサイズを変更する場合は、Query.setLazyLoadBatchSize()
を使用して設定できます。
List<Order> orders = database.find(Order.class)
// set lazy loading batch size for this query
.setLazyLoadBatchSize(100)
...
.findList();
パスごと
特定のクエリで遅延ロードのバッチサイズを変更する場合は、FetchConfig
を使用して設定できます。
List<Order> orders = database.find(Order.class)
// lazy fetch customer using batchSize of 10
.fetch("customer", new FetchConfig().lazy(10))
// eager fetch details using batchSize of 100
.fetch("details", new FetchConfig().query(100))
...
.findList();
早期ロード
Ebean クエリ言語では、早期に取得したい「パス」を指定できます。オプションで、使用するバッチサイズを指定でき、指定しない場合、バッチサイズはデフォルトで 100 になります。
List<Order> orders = database.find(Order.class)
// specify some predicates
.status.eq(Order.Status.NEW)
.orderDate.after(since)
// specify 'what to fetch'
// eagerly fetch customer and details using batchSize of 100
.fetch("customer")
.fetch("details")
.findList();
ORM クエリから SQL クエリへ
ORM クエリはどのようにして複数の SQL クエリに分割されるのですか
Ebean は、次の目的で、単一の ORM クエリを複数の SQL クエリに自動的に分割します。
- SQL デカルト積が生成されないようにする
- FirstRows / MaxRows は常に尊重される
Ebean は常に firstRows/maxRows を尊重し、パフォーマンス上の理由から、SQL デカルト積を生成することはありません。これは、Ebean がクエリに定義されているすべての「取得パス」を分析し、どのパスに OneToMany または ManyToMany 関係が含まれているかを検出することを意味します。Ebean の場合、単一の SQL クエリには、OneToMany/ManyToMany を含むパスを最大 1 つ含めることができ、Ebean はこれに基づいて ORM クエリを自動的に分割します。
クエリ結合を参照してください。
質疑応答
Q: グローバルな lazyLoadBatchSize を 100 にするのは妥当ですか?
A: はい、妥当だと思いますし、10 よりも優れたデフォルトである可能性があります (100 が Ebean のデフォルトであるべきであるという点で)。小さいバッチサイズの利点は、オブジェクトグラフの使用が「対称」ではないシナリオです。つまり、「ルートレベルオブジェクト」のリストを反復処理する場合、トラバース/使用されるオブジェクトグラフの部分は同じではありません。これはかなりまれなようですが、使用されるオブジェクトグラフの部分はすべてのルートレベルオブジェクトで同じである方がはるかに一般的です。
バッチサイズが 100 でも、バッチに含まれる数がそれよりはるかに少ない場合は、まったく問題ありません。それを説明すると、生成された SQL IN 句のバインド値の数は、1、5、10、20、50、および 100 のバケットに分類され、バケットを埋めるために一部のバインド値が繰り返されます。なぜこれを行うのですか? データベース自体が各 SQL ステートメントの実行プランを解析してキャッシュするため、個別の SQL ステートメントが多すぎることは望ましくありません。そのため、バケットを制限すると、SQL 実行プランのデータベースキャッシュでのヒット率が向上します。
Q: これは AutoTune とどのように関係しますか?
A: Ebean の AutoTune 機能は、オブジェクトグラフのどの部分が使用されているかをプロファイルし、これを使用して fetch() および select() 句の自動クエリチューニングを提供し、この方法で遅延ロードを減らし、早期取得に置き換えることができます。ManyToOne および OneToOne をトラバースするパスを組み合わせることで、実行される SQL クエリの総数を減らすことができ、これにより、高いバッチサイズのバッチ遅延ロードよりも優れている可能性があります。
Q: N + 1 の問題を特定するにはどうすればよいですか?
A: Ebean の用語では、1 は 元のクエリ
に関連し、N は 二次クエリ
に関連します。ロギングでは、org.avaje.ebean.SUM
の TRACE
レベルのロギングを有効にすることができ、ログエントリには origin
という属性が含まれています。 origin は、二次クエリを元のクエリにリンクするために使用されるハッシュです。そのオリジンキーのログをローテクで grep すると、元のクエリと関連するすべての遅延ロードクエリが返されます。
まもなく、ダッシュボード/監視サービスが利用可能になり、それによって、多くの遅延ロードを引き起こす元のクエリを特定できるようになります。
AutoTune プロファイリングは、過度の遅延ロードを修正するチューニングされたクエリを提供することに重点を置いていますが、別の代替手段も提供します。
Q: JPA FetchType.EAGER についてはどうですか?
A: デプロイメントアノテーションとして、FetchType は単一のユースケースに適したヒントを提供します。問題は、Bean とそのプロパティ/関係がサポートする必要があるユースケースが多数あることです。デプロイメントアノテーションによる最適化は、1 つのユースケースを最適化できる可能性がありますが、他の多くのユースケースに悪影響を及ぼす可能性があります。
つまり、ORM は、開発者が各ユースケースのオブジェクトグラフコンストラクターを最適化できるようにするメカニズム/クエリ言語を提供する必要があり、固定されたデプロイメントアノテーションではそれを行うことはできません。
Q: JPA フェッチグループについてはどうですか?
A: フェッチグループは JPA ユーザーにとって良いものであり、Ebean の fetch()
および select()
と同様の方法で、オブジェクトグラフのどの部分を取得するかを制御する機能を提供します。
JPA フェッチグループに関する個人的な失望は次のとおりです。
- フェッチグループは冗長で使いにくいようです
- フェッチグループは JPQL クエリ言語の一部であるべきでした
- フェッチグループには、次の制御がありません: Eager/Lazy、Batch Size/ReadOnly、L2/L3 の使用
- フェッチグループはヒントですか? なんてこった?
Q: JPQL についてはどうですか?
A: JPQL はオブジェクトグラフの構築を最適化するための適切なクエリ言語ではなく、Ebean はこの理由で JPQL を採用しませんでした。つまり、Ebean でオブジェクトグラフを構築する場合、開発者に次の機能を提供したいと考えています。
- オブジェクトグラフのどの部分を(どのパスを)設定するかを制御する
- 早期/遅延ロードに対するパスごとの制御を持つ
- ロードバッチサイズに対するパスごとの制御を持つ
- readOnly と L2/L3 キャッシュの使用に対するパスごとの制御を持つ
Ebean の ORM クエリ言語は、オブジェクトグラフの構築を制御および最適化するための強力かつシンプルな方法を提供するように設計されています。ORM クエリ言語が適していない場合 (集計クエリ、レポート、再帰クエリなど)、Ebean は SQL への優れた統合を提供します。ORM クエリ言語は OLTP クエリの 90% 以上をカバーするかもしれませんが、言語を拡張して複雑にすることは望ましくなく、代わりに raw SQL と統合するための優れたメカニズムを推進しています。