web-dev-qa-db-ja.com

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

「N + 1選択問題」は一般にオブジェクトリレーショナルマッピング(ORM)の議論で問題として述べられています、そしてそれはオブジェクトの中で単純に思われるもののためにたくさんのデータベース問い合わせをしなければならないことと関係があると私は理解します世界。

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

1435
Lars A. Brekken

Carオブジェクト(データベース行)のコレクションがあり、それぞれのCarWheelオブジェクト(行も)のコレクションがあるとしましょう。つまり、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選択を防ぐためのいくつかの方法があります。

参照:HibernateによるJavaの持続性、第13章.

878
Matt Solnit
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"に対する単一の照会は、以下を戻すことがあります。

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"を入力してから、Dave、John、およびMikeのPeopleインスタンスを含むInabbitants配列に1つのクエリを追加します。

上記で使用されているものと同じアドレスに対するN + 1クエリは、次のようになります。

Id Address
1  22 Valley St

のような別のクエリで

SELECT * FROM Person WHERE HouseId = 1

その結果、次のような別のデータセットになります。

Name    HouseId
Dave    1
John    1
Mike    1

そして最終的な結果は、単一のクエリを使用した場合と同じです。

単一選択の利点は、すべてのデータを事前に取得できることです。これは、最終的に必要なものになる可能性があります。 N + 1の利点は、クエリの複雑さが軽減され、子結果セットが最初の要求時にのみロードされる遅延ロードを使用できることです。

103
cfeduke

製品と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です。

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

  • キャッシングは初めて役割を果たすことはありません。

  • サプライヤにアクセス

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

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

結果:

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

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

60
Summy

十分な評判がないので、他の答えに直接コメントすることはできません。しかし、この問題が本質的に発生するのは注目に値します。歴史的に見れば、結合の処理に関して多くのdbmsがかなり貧弱だったためです(MySQLは特に注目に値する例です)。そのため、n + 1は結合よりもはるかに高速です。そして、n + 1を改善する方法がありますが、それでも結合を必要としません。これが、元の問題に関するものです。

しかし、MySQLは今では結合に関しては以前よりはるかに優れています。私が初めてMySQLを学んだとき、私は結合をよく使いました。それから私はそれらがどれくらい遅いかを発見し、代わりにコードでn + 1に切り替えました。しかし、最近、私は結合に戻ってきました。なぜなら、MySQLは現在、初めて使用したときよりも結合の処理がはるかに優れているからです。

最近では、適切に索引付けされた一連の表に対する単純な結合が、パフォーマンスの面でめったに問題になることはありません。そしてそれがパフォーマンス上の打撃を与えるのであれば、それからインデックスヒントの使用はしばしばそれらを解決します。

これはMySQL開発チームの一人によってここで議論されます:

http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html

つまり、要約は次のとおりです。過去にMySQLのパフォーマンスが非常に悪かったために過去の参加を避けた場合は、最新バージョンをもう一度試してください。あなたはおそらくうれしい驚きでしょう。

36
Mark Goodge

この問題のため、DjangoのORMから離れました。基本的に、試してみると

for p in person:
    print p.car.colour

ORMは(通常はPersonオブジェクトのインスタンスとして)すべての人を喜んで返しますが、その後、Personごとにcarテーブルを照会する必要があります。

これに対する単純で非常に効果的なアプローチは、私が " fanfolding "と呼ぶもので、リレーショナルデータベースからのクエリ結果が、クエリを構成する元のテーブルにマッピングし直されるという無意味な考えを避けます。

ステップ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"オブジェクトが2回以上作成されないことを意味します。

ステップ3:レンダリング

for p in people:
    print p.car.colour # no more car queries

Pythonの fanfolding の実装については このWebページ を参照してください。

26
rorycl

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

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

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

17
davetron5000

これは問題の良い説明です - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=なぜ怠け者

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

15
Joe Dean

私の意見では、 Hibernate Pitfallに書かれた記事はなぜ関係が怠惰であるべきか は、実際のN + 1の問題と正反対です。

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

選択フェッチ(デフォルト)はN + 1の選択問題に対して非常に脆弱であるため、結合フェッチを有効にすることをお勧めします。

13
Anoop Isaac

このトピックについてのAyendeの投稿をチェックする: NHibernateのSelect N + 1問題との闘い

基本的に、NHibernateやEntityFrameworkのようなORMを使用しているときに、1対多の(マスター/詳細)関係があり、各マスターレコードごとにすべての詳細をリストしたい場合は、N + 1クエリ呼び出しを行う必要があります。 "N"はマスターレコードの数です。1つのクエリーですべてのマスターレコードを取得し、1つのクエリーでマスターレコードごとに1つずつ取得してマスターレコードごとにすべての詳細を取得します。

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

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

13
Nathan

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エンティティを二次クエリで取得する必要があります。NPostCommentエンティティの場合、さらにN個のクエリが実行されます(したがってN + 1クエリの問題)。

まず、この問題を特定できるようにするには、 適切なSQLロギングとモニタリング が必要です。

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

N + 1クエリの問題を識別したときは、 N ではなく、1つのクエリで子の関連付けを取得するようにJOIN FETCHを使用する必要があります。複数の子の関連付けを取得する必要がある場合は、最初のクエリで1つのコレクションを取得し、2番目のコレクションを2番目のSQLクエリで取得することをお勧めします。

12
Vlad Mihalcea

提供されたリンクはn + 1問題の非常に単純な例を持っています。あなたがHibernateにそれを適用するならば、それは基本的に同じことについて話しています。オブジェクトを照会すると、エンティティはロードされますが、関連付けが他に設定されていない限り、遅延ロードされます。したがって、ルートオブジェクトに対するクエリと、これらのそれぞれに対する関連付けを読み込むための別のクエリがあります。 100個のオブジェクトが返されるということは、最初のクエリが1つあり、次に100個の追加クエリがあるため、それぞれn + 1の関連付けができます。

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

10
Jeff Mills

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

9
jj_

億万長者はN台の車を持っています。あなたはすべての(4)車輪を手に入れたいです。

1つのクエリがすべての車をロードしますが、各(N)台の車両について、ロードホイールについて別々のクエリが送信されます。

費用:

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

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

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

ローディングインデックスに対する最悪の場合の1 + Nプレートアクセスにおける追加費用。

概要

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

8
hans wurst

N + 1の選択した問題は苦痛であり、そしてユニットテストでそのようなケースを検出することは理にかなっています。私は与えられたテストメソッドまたは単にコードの任意のブロックによって実行されたクエリの数を検証するための小さなライブラリを開発しました - JDBC Sniffer

テストクラスに特別なJUnitルールを追加し、テストメソッドに予想される数のクエリで注釈を付けます。

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

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

他の人がよりエレガントに述べている問題は、あなたがOneToManyコラムのデカルト積を持っているか、あなたがN + 1の選択をしているかのどちらかです。それぞれ可能性のある巨大な結果セットかデータベースとのおしゃべり。

私はこれが言及されていないことに驚いていますが、これは私がこの問題を回避した方法です... 私は半一時的なidテーブルを作成します 私はあなたが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列に対して、idsテーブルでSELECTを実行し、INNER JOINで子テーブルをWHERE batch_id=(またはその逆に)します。結果列のマージが容易になるため、id列で並べ替えるようにしてください(そうでない場合は、結果セット全体に対してHashMap/Tableが必要になりますが、それほど悪くない場合があります)。

その後、定期的にIDテーブルをクリーンアップします。

これは、ユーザーがある種の一括処理のために100個程度の項目を選択した場合にも特に有効です。一時テーブルに100個の異なるIDを入れます。

これで、実行しているクエリの数はOneToMany列の数になります。

5
Adam Gent

たとえば、Matt Solnitを例にして、CarとWheelsの間の関連付けをLAZYとして定義し、いくつかのWheelsフィールドが必要であるとします。これは、最初の選択の後、Hibernateが "Select * from Wheels where car_id =:id"をそれぞれのCarに対して行うことを意味します。

これにより、N台の自動車ごとに最初の選択と複数の選択が行われます。それがn + 1問題と呼ばれる理由です。

これを回避するには、関連付けを積極的に取得して、hibernateが結合でデータをロードするようにします。

ただし、関連するWheelsにアクセスしないことが多い場合は、LAZYにしておくか、Criteriaでフェッチタイプを変更することをお勧めします。

1
martins.tuga