ORM (オブジェクト リレーショナル マッピング) における「N+1 選択問題」とは何ですか?

StackOverflow https://stackoverflow.com/questions/97197

  •  01-07-2019
  •  | 
  •  

質問

「N+1 選択問題」は、一般にオブジェクト リレーショナル マッピング (ORM) の議論の問題として述べられており、オブジェクト内で単純に見えるものに対して大量のデータベース クエリを実行する必要があることに関係があることを私は理解しています。世界。

誰かが問題についてより詳細な説明を持っていますか?

役に立ちましたか?

解決

のコレクションがあるとします。 Car オブジェクト (データベース行) とそれぞれ Car のコレクションを持っています Wheel オブジェクト (行も)。言い換えると、 Car -> Wheel は 1 対多の関係です。

ここで、すべての車を反復処理し、それぞれの車についてホイールのリストを出力する必要があるとします。単純な O/R 実装では次のことが行われます。

SELECT * FROM Cars;

その後 それぞれに Car:

SELECT * FROM Wheel WHERE CarId = ?

言い換えれば、車を 1 つ選択し、さらに N つを選択します (N は車の総数)。

あるいは、すべてのホイールを取得してメモリ内で検索を実行することもできます。

SELECT * FROM Wheel

これにより、データベースへの往復回数が N+1 から 2 に減ります。ほとんどの ORM ツールには、N+1 選択を防ぐいくつかの方法が用意されています。

参照: Hibernate による Java 永続化, 、第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 インスタンスを InHBidentants 配列に設定する必要があります。

上記で使用したのと同じアドレスに対する 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」に設定 (デフォルト)

  • Product のクエリに使用されるフェッチ モードは Select です

  • フェッチモード (デフォルト):サプライヤー情報にアクセスされる

  • キャッシュは初めての場合は役割を果たしません。

  • サプライヤーにアクセスしました

フェッチ モードは選択フェッチ (デフォルト) です。

// 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=?

結果:

  • 製品に対する 1 つの選択ステートメント
  • サプライヤーの N 個の選択ステートメント

これは N+1 選択問題です。

十分な評判がないため、他の回答について直接コメントすることはできません。ただし、この問題は本質的に、歴史的に多くの DBMS が結合の処理に関して非常に貧弱であったためにのみ発生するということは注目に値します (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

見る このウェブページ の実装のために 扇形 パイソン用。

COMPANY と EMPLOYEE があるとします。会社には多くの従業員がいます(つまり、EMPLOYEE にはフィールド COMPANY_ID があります)。

一部の O/R 構成では、マッピングされた Company オブジェクトがあり、その Employee オブジェクトにアクセスすると、O/R ツールは従業員ごとに 1 つの選択を実行しますが、単純な SQL で処理を行っている場合は、 select * from employees where company_id = XX. 。したがって、N (従業員数) プラス 1 (会社)

これが、EJB Entity Bean の初期バージョンの動作方法です。Hibernate のようなものによってこの問題は解消されたと思いますが、よくわかりません。通常、ほとんどのツールには、マッピングの戦略に関する情報が含まれています。

問題の詳しい説明は次のとおりです - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

問題は理解できましたが、通常はクエリで結合フェッチを実行することで回避できます。これは基本的に、遅延ロードされたオブジェクトのフェッチを強制するため、データは n+1 クエリではなく 1 クエリで取得されます。お役に立てれば。

私の意見では、に書かれた記事は ハイバネートの落とし穴:なぜ人間関係は怠惰であるべきなのか は実際の N+1 問題とはまったく逆です。

正しい説明が必要な場合は参照してください 冬眠 - 第 19 章:パフォーマンスの向上 - フェッチ戦略

Select Fetching(デフォルト)はn+1の問題に対して非常に脆弱です。

このトピックに関する Ayende の投稿をチェックしてください。 Hibernate での Select N + 1 問題への対処

基本的に、NHibernate や EntityFramework のような ORM を使用する場合、1 対多 (マスターと詳細) の関係があり、各マスター レコードごとにすべての詳細をリストしたい場合は、データベース。「N」はマスター レコードの数です。すべてのマスター レコードを取得するには 1 つのクエリ、マスター レコードごとにすべての詳細を取得するにはマスター レコードごとに 1 つのクエリ (N クエリ)。

データベース クエリ呼び出しの増加 --> 待ち時間の増加 --> アプリケーション/データベースのパフォーマンスの低下。

ただし、ORM には、主に「結合」を使用してこの問題を回避するオプションがあります。

N+1 クエリの問題は、関連付けを取得するのを忘れて、関連付けにアクセスする必要がある場合に発生します。

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

LOGGER.info("Loaded {} comments", comments.size());

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

これにより、次の SQL ステートメントが生成されます。

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

まず、Hibernate は JPQL クエリを実行し、 PostComment エンティティが取得されます。

次に、それぞれについて、 PostComment, 、関連する post プロパティは、次の内容を含むログ メッセージを生成するために使用されます。 Post タイトル。

なぜなら post 関連付けが初期化されていないため、Hibernate が Post 二次クエリを持つエンティティ、およびnの場合 PostComment エンティティの場合、さらに N 個のクエリが実行されることになります (したがって、N+1 クエリの問題が発生します)。

まず、必要なのは 適切な SQL ログと監視 この問題を特定できるように。

第二に、この種の問題は統合テストで検出する方が良いです。使用できます 生成される SQL ステートメントの予想数を検証するための自動 JUnit アサート. 。の db ユニット プロジェクト はすでにこの機能を提供しており、オープンソースです。

N+1 クエリの問題を特定したら、 子の関連付けが N ではなく 1 つのクエリでフェッチされるように、JOIN FETCH を使用する必要があります。. 。複数の子の関連付けをフェッチする必要がある場合は、最初のクエリで 1 つのコレクションをフェッチし、2 番目の SQL クエリで 2 番目のコレクションをフェッチすることをお勧めします。

提供されたリンクには、n + 1 問題の非常に単純な例が含まれています。これを Hibernate に適用すると、基本的には同じことについて話します。オブジェクトをクエリすると、エンティティはロードされますが、関連付けは (別途設定されていない限り) 遅延ロードされます。したがって、ルート オブジェクトに対する 1 つのクエリと、これらのそれぞれの関連付けをロードするための別のクエリが必要になります。100 個のオブジェクトが返されるということは、1 つの最初のクエリと、それぞれの関連付けを取得するための 100 個の追加クエリ (n + 1) を意味します。

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/

一人の億万長者は N 台の車を持っています。4 つのホイールをすべて入手したいと考えています。

1 つのクエリですべての車がロードされますが、車輪をロードするために各 (N 台) の車に対して個別のクエリが送信されます。

費用:

インデックスが RAM に収まると仮定します。

1 + N クエリの解析と計画 + インデックス検索、およびペイロードをロードするための 1 + N + (N * 4) プレート アクセス。

インデックスが RAM に適合しないと仮定します。

最悪の場合、インデックスをロードするために 1 + N プレートにアクセスすると追加コストが発生します。

まとめ

ボトルネックはプレートへのアクセスです(約HDDの1秒あたり70回のランダムアクセス)熱心な結合選択は、ペイロードのプレート1 + n +(n * 4)回もアクセスします。したがって、インデックスが RAM に収まる場合は問題ありません。関係するのは RAM 操作のみであるため、十分に高速です。

それぞれ 1 つの結果を返す 100 個のクエリを発行するよりも、100 個の結果を返す 1 つのクエリを発行する方がはるかに高速です。

N+1 選択の問題は面倒なので、単体テストでそのようなケースを検出することは理にかなっています。私は、特定のテストメソッドまたは任意のコードブロックによって実行されるクエリの数を検証するための小さなライブラリを開発しました。 JDBCスニファー

特別な JUnit ルールをテスト クラスに追加し、予想されるクエリ数を示すアノテーションをテスト メソッドに配置するだけです。

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

他の人がよりエレガントに述べているように、問題は、OneToMany 列のデカルト積があるか、N + 1 選択を行っていることです。それぞれ、巨大な結果セットまたはデータベースとのやり取りの可能性があります。

これが言及されていないことに驚いていますが、これが私がこの問題を回避した方法です... 半一時的なIDSテーブルを作成します. を持っているときもこれを行います 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 IDテーブル上で INNER JOIN子テーブルを WHERE batch_id= (またはその逆)。結果列のマージが容易になるため、id 列で順序付けするようにしてください (そうしないと、結果セット全体の HashMap/Table が必要になりますが、それほど悪くないかもしれません)。

その後、定期的に ids テーブルをクリーンアップするだけです。

これは、ユーザーが、ある種の一括処理のために、たとえば 100 個程度の個別のアイテムを選択した場合にも特にうまく機能します。100 個の異なる ID を一時テーブルに置きます。

実行しているクエリの数は、OneToMany 列の数によって決まります。

Matt Solnit の例を考えてみましょう。Car と Wheel の間の関連付けを LAZY として定義し、いくつかの Wheel フィールドが必要だと想像してください。これは、最初の選択の後、休止状態は各車に対して「* from Wheel where car_id = :id」を実行することを意味します。

これにより、N 台の車ごとに最初の選択とさらに 1 つの選択が行われるため、n+1 問題と呼ばれます。

これを回避するには、関連付けフェッチを積極的に行い、Hibernate が結合を使用してデータをロードするようにします。

ただし、関連する Wheel に何度もアクセスしない場合は、LAZY のままにするか、Criteria でフェッチ タイプを変更することをお勧めします。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top