「N+1 選択の問題」は、オブジェクト リレーショナル マッピング (ORM) の議論では一般的に問題として述べられています。オブジェクト内で単純に見えるものに対して、多くのデータベース クエリを作成する必要があることと関係があることを理解しています。世界。
誰かが問題のより詳細な説明を持っていますか?
「N+1 選択の問題」は、オブジェクト リレーショナル マッピング (ORM) の議論では一般的に問題として述べられています。オブジェクト内で単純に見えるものに対して、多くのデータベース クエリを作成する必要があることと関係があることを理解しています。世界。
誰かが問題のより詳細な説明を持っていますか?
オブジェクトのコレクションCar
(データベース行) があり、それぞれCar
にオブジェクトのコレクションWheel
(行も) があるとします。つまり、Car
→Wheel
は 1 対多の関係です。
ここで、すべての車を反復処理し、それぞれの車について車輪のリストを出力する必要があるとします。単純な O/R 実装は、次のことを行います。
SELECT * FROM Cars;
そして、それぞれについてCar
:
SELECT * FROM Wheel WHERE CarId = ?
つまり、Cars に対して 1 つの選択があり、次に N の追加の選択があります。ここで、N は車の総数です。
または、すべてのホイールを取得して、メモリ内でルックアップを実行することもできます。
SELECT * FROM Wheel
これにより、データベースへの往復回数が N+1 から 2 に減少します。ほとんどの ORM ツールには、N+1 選択を防ぐ方法がいくつか用意されています。
参照: Java Persistence with Hibernate、第 13 章。
SELECT
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId
これにより、table2 の子行ごとに table1 の結果が返され、table2 の子行が重複する結果セットが得られます。O/R マッパーは、一意のキー フィールドに基づいて table1 インスタンスを区別し、すべての table2 列を使用して子インスタンスを設定する必要があります。
SELECT table1.*
SELECT table2.* WHERE SomeFkId = #
N+1 は、最初のクエリがプライマリ オブジェクトを設定し、2 番目のクエリが返された一意のプライマリ オブジェクトごとにすべての子オブジェクトを設定する場所です。
検討:
class House
{
int Id { get; set; }
string Address { get; set; }
Person[] Inhabitants { get; set; }
}
class Person
{
string Name { get; set; }
int HouseId { get; set; }
}
同様の構造を持つテーブル。住所「22 Valley St」に対する 1 回のクエリで、次の結果が返される場合があります。
Id Address Name HouseId
1 22 Valley St Dave 1
1 22 Valley St John 1
1 22 Valley St Mike 1
O/RM は、Home のインスタンスに ID=1、Address="22 Valley St" を入力してから、たった 1 つのクエリで、Dave、John、Mike の People インスタンスを Inhabitants 配列に入力する必要があります。
上で使用したのと同じアドレスに対する N+1 クエリの結果は次のようになります。
Id Address
1 22 Valley St
のような別のクエリで
SELECT * FROM Person WHERE HouseId = 1
そして、次のような別のデータセットになります
Name HouseId
Dave 1
John 1
Mike 1
最終結果は、単一のクエリで上記と同じです。
単一選択の利点は、すべてのデータを前もって取得できることです。これは、最終的に望むものになる可能性があります。N+1 の利点は、クエリの複雑さが軽減され、子の結果セットが最初の要求時にのみ読み込まれる遅延読み込みを使用できることです。
製品と1対多の関係にあるサプライヤー。1つのサプライヤーが多くの製品を(供給)しています。
***** Table: Supplier *****
+-----+-------------------+
| ID | NAME |
+-----+-------------------+
| 1 | Supplier Name 1 |
| 2 | Supplier Name 2 |
| 3 | Supplier Name 3 |
| 4 | Supplier Name 4 |
+-----+-------------------+
***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID | NAME | DESCRIPTION | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1 | Product 1 | Name for Product 1 | 2.0 | 1 |
|2 | Product 2 | Name for Product 2 | 22.0 | 1 |
|3 | Product 3 | Name for Product 3 | 30.0 | 2 |
|4 | Product 4 | Name for Product 4 | 7.0 | 3 |
+-----+-----------+--------------------+-------+------------+
要因:
サプライヤのレイジーモードが「true」に設定されている(デフォルト)
製品のクエリに使用されるフェッチモードはSelectです
フェッチモード(デフォルト):サプライヤー情報にアクセスします
キャッシングは初めての役割を果たしません
サプライヤーにアクセスします
フェッチモードはSelectFetch(デフォルト)です
// It takes Select fetch mode as a default
Query query = session.createQuery( "from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);
select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
結果:
これはN+1選択の問題です!
十分な評判がないため、他の回答について直接コメントすることはできません。しかし、この問題が本質的に発生するのは、歴史的に多くの dbm が結合の処理に関して非常に貧弱であったためであることに注意してください (MySQL は特に注目に値する例です)。そのため、n+1 はしばしば結合よりも著しく高速でした。そして、n+1 を改善する方法がありますが、結合を必要とせずに元の問題が関係しています。
ただし、結合に関しては、MySQL は以前よりもはるかに優れています。初めて MySQL を学んだとき、私は結合をよく使いました。次に、それらがいかに遅いかを発見し、代わりにコードで n+1 に切り替えました。しかし、最近、私はジョインに戻ってきました。なぜなら、MySQL は、私が最初に使い始めたときよりもはるかにうまく処理できるようになったからです。
最近では、適切にインデックスが作成された一連のテーブルでの単純な結合が、パフォーマンス面で問題になることはめったにありません。また、パフォーマンス ヒットが発生する場合は、インデックス ヒントを使用すると解決することがよくあります。
これについては、MySQL 開発チームの 1 人がここで説明しています。
http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html
つまり、要約は次のとおりです。MySQL のひどいパフォーマンスのために過去に結合を避けてきた場合は、最新バージョンでもう一度試してください。きっと嬉しい驚きでしょう。
この問題のため、Django の ORM から離れました。基本的に、やってみると
for p in person:
print p.car.colour
ORM は問題なくすべての人を (通常は Person オブジェクトのインスタンスとして) 返しますが、その場合、各 Person の car テーブルにクエリを実行する必要があります。
これに対する単純で非常に効果的なアプローチは、私が「ファンフォールディング」と呼んでいるものです。これにより、リレーショナル データベースからのクエリ結果を、クエリを構成する元のテーブルにマップし直す必要があるという無意味な考えが回避されます。
ステップ 1: ワイドセレクト
select * from people_car_colour; # this is a view or sql function
これは次のようなものを返します
p.id | p.name | p.telno | car.id | car.type | car.colour
-----+--------+---------+--------+----------+-----------
2 | jones | 2145 | 77 | ford | red
2 | jones | 2145 | 1012 | toyota | blue
16 | ashby | 124 | 99 | bmw | yellow
ステップ 2: オブジェクト化する
3 番目の項目の後に分割する引数を使用して、結果をジェネリック オブジェクト クリエーターに吸い込みます。これは、「jones」オブジェクトが複数回作成されないことを意味します。
ステップ 3: レンダリング
for p in people:
print p.car.colour # no more car queries
Python のファンフォールディングの実装については、この Web ページを参照してください。
問題を理解したので、通常はクエリで結合フェッチを実行することで回避できます。これは基本的に、遅延ロードされたオブジェクトのフェッチを強制するため、データは n+1 クエリではなく 1 つのクエリで取得されます。お役に立てれば。
COMPANY と EMPLOYEE があるとします。COMPANY には多くの EMPLOYEES があります (つまり、EMPLOYEE には COMPANY_ID フィールドがあります)。
一部の O/R 構成では、マッピングされた Company オブジェクトがあり、その Employee オブジェクトにアクセスすると、O/R ツールはすべての従業員に対して 1 つの選択を行いますが、単純な SQL で処理を行っている場合は、select * from employees where company_id = XX
. したがって、N (従業員数) + 1 (会社)
これが、EJB エンティティ Bean の初期バージョンがどのように機能したかです。Hibernate のようなものがこれを排除したと思いますが、よくわかりません。ほとんどのツールには通常、マッピングの戦略に関する情報が含まれています。
トピックに関する Ayende の投稿を確認してください: Combating the Select N + 1 Problem In NHibernate。
基本的に、NHibernate や EntityFramework などの ORM を使用する場合、1 対多 (マスターと詳細) の関係があり、各マスター レコードごとにすべての詳細を一覧表示する場合は、N + 1 回のクエリ呼び出しを行う必要があります。データベース、「N」はマスター レコードの数です。すべてのマスター レコードを取得するための 1 つのクエリと、マスター レコードごとに 1 つの N クエリを使用して、マスター レコードごとにすべての詳細を取得します。
データベース クエリ呼び出しの増加 → 待ち時間の増加 → アプリケーション/データベースのパフォーマンスの低下。
ただし、ORM には、主に JOIN を使用して、この問題を回避するオプションがあります。
私の意見では、Hibernate Pitfall: Why Relationships Should Be Lazyに書かれた記事は、実際の N+1 の問題とは正反対です。
正しい説明が必要な場合は、Hibernate - Chapter 19: Improving Performance - Fetching Strategiesを参照してください。
選択フェッチ (デフォルト) は N+1 選択の問題に対して非常に脆弱であるため、結合フェッチを有効にすることをお勧めします。
提供されたリンクには、n + 1 問題の非常に単純な例があります。それをHibernateに適用すると、基本的に同じことを話している. オブジェクトのクエリを実行すると、エンティティが読み込まれますが、関連付けは (別の方法で構成されていない限り) 遅延読み込みされます。したがって、ルート オブジェクトの 1 つのクエリと、これらのそれぞれの関連付けをロードする別のクエリです。100 個のオブジェクトが返されるということは、最初のクエリが 1 回、次にそれぞれの関連付けを取得するための追加のクエリが 100 回 (n + 1) あることを意味します。
N+1 選択の問題は苦痛であり、単体テストでそのようなケースを検出することは理にかなっています。特定のテストメソッドまたは任意のコードブロックによって実行されたクエリの数を検証するための小さなライブラリを開発しました - JDBC Sniffer
特別な JUnit ルールをテスト クラスに追加し、テスト メソッドに予想されるクエリ数の注釈を配置するだけです。
@Rule
public final QueryCounter queryCounter = new QueryCounter();
@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
// your JDBC or JPA code
}
他の人がよりエレガントに述べている問題は、OneToMany 列のデカルト積があるか、N+1 選択を行っていることです。それぞれ、巨大な結果セットまたはデータベースとおしゃべりの可能性があります。
これが言及されていないことに驚いていますが、これは私がこの問題を回避した方法です...私は半一時的な ids table を作成します。句制限がある場合もこれを行いますIN ()
。
これはすべてのケースで機能するわけではありません (おそらく過半数でさえありません) が、デカルト積が手に負えなくなるような子オブジェクトが多数ある場合 (つまり、OneToMany
列が多く、結果の数が列の乗算) と、より多くのバッチのようなジョブです。
最初に、親オブジェクト ID をバッチとして ID テーブルに挿入します。この batch_id は、アプリで生成して保持するものです。
INSERT INTO temp_ids
(product_id, batch_id)
(SELECT p.product_id, ?
FROM product p ORDER BY p.product_id
LIMIT ? OFFSET ?);
OneToMany
これで、列ごとSELECT
に ids テーブルで a を実行INNER JOIN
し、子テーブルを a で処理しますWHERE batch_id=
(またはその逆)。結果列のマージが容易になるため、id 列で並べ替えるようにしてください (そうしないと、結果セット全体に HashMap/Table が必要になりますが、それほど悪くはありません)。
次に、ids テーブルを定期的にクリーンアップします。
これは、ユーザーがある種の一括処理のために、たとえば 100 ほどの個別のアイテムを選択する場合にも特にうまく機能します。100 個の個別の ID を一時テーブルに入れます。
実行しているクエリの数は、OneToMany 列の数です。