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


1598

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

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


2
これは、n + 1の問題を理解するための素晴らしい説明との素晴らしいリンクです。また、この問題に対処するためのソリューションについても説明します:architects.dzone.com/articles/how-identify-and-resilve-n1
エース。

この問題と可能な修正について話しているいくつかの役立つ投稿があります。一般的なアプリケーションの問題とその修正方法:Select N + 1 ProblemThe(Silver)Bullet for the N + 1 ProblemLazy loading-eager loading
cateyes

この問題の解決策を探しているすべての人に、それを説明する投稿を見つけました。stackoverflow.com/questions/32453989/...
damndemon

2
答えを考えると、これは1 + N問題と呼ばれるべきではありませんか?これは専門用語のようですので、具体的にはOPに質問することはしません。
user1418717

回答:


1018

Carオブジェクトのコレクション(データベース行)があり、それぞれCarWheelオブジェクトのコレクション(行も含む)があるとします。つまり、CarWheelは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 Persistence、第13章。


140
「これは悪い」を明確にするために、SELECT * from Wheel;N + 1ではなく1つの選択()ですべてのホイールを取得できます。Nが大きい場合、パフォーマンスへの影響は非常に大きくなります。
tucuxi 2010

212
@tucuxi私はあなたが間違っていることに対して非常に多くの賛成票を獲得したことに驚いています。データベースはインデックスに関して非常に優れており、特定のCarIDに対してクエリを実行すると非常に高速に返されます。ただし、すべてのWheelが1度である場合、アプリケーションでCarIDを検索する必要があります。これはインデックス付けされていません。これは遅くなります。データベースに到達するまでのレイテンシに大きな問題がない限り、n + 1の方が実際には高速です。そしてもちろん、さまざまな実世界のコードでベンチマークを行いました。
アリエル

74
@ariel「正しい」方法は、すべてのホイールを取得、CarId(1選択)で順序付けします。CarIdよりも詳細が必要な場合は、すべての車に対して2番目のクエリを実行します(合計2クエリ)。物事を印刷することが最適になり、インデックスや2次ストレージは不要になりました(結果を繰り返し処理でき、すべてをダウンロードする必要はありません)。あなたは間違ったことをベンチマークしました。それでもベンチマークに自信がある場合は、実験と結果を説明する長いコメント(または完全な回答)を投稿していただけませんか?
tucuxi

92
「Hibernate(私は他のORMフレームワークに精通していません)はそれを処理するいくつかの方法を提供します。」そしてこれらの方法は何ですか?
Tima

58
@Arielデータベースサーバーとアプリケーションサーバーでベンチマークを別々のマシンで実行してみてください。私の経験では、データベースへの往復には、クエリ自体よりもオーバーヘッドのコストがかかります。つまり、クエリは非常に高速ですが、大混乱を招くのは往復です。「WHERE Id = const」を「WHERE Id IN(constconst、...)」に変換し、それから桁違いに増加しました。
Hans

110
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インスタンスを1つのクエリで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の利点は、クエリの複雑さが軽減され、子結果セットが最初の要求時にのみロードされる遅延ロードを使用できることです。


4
n + 1のもう1つの利点は、データベースがインデックスから直接結果を返すことができるため、処理が高速になることです。結合を実行してから並べ替えを行うには、一時テーブルが必要です。これは低速です。n + 1を回避する唯一の理由は、データベースとの通信に多くの待ち時間がある場合です。
アリエル

17
結合と並べ替えは非常に高速です(インデックス付けされたフィールドと可能性のある並べ替えフィールドで結合するため)。「n + 1」の大きさは?n + 1の問題は待ち時間の長いデータベース接続にのみ当てはまると真剣に考えていますか?
tucuxi

9
@ariel-ベンチマークは正しいかもしれませんが、N + 1が「最速」であるというあなたのアドバイスは間違っています。そんなことがあるものか?en.wikipedia.org/wiki/Anecdotal_evidence、およびこの質問に対する他の回答での私のコメントも参照してください。
ホイットニーランド

7
@アリエル-私はそれをうまく理解したと思います:)。私はあなたの結果が一組の条件にのみ適用されることを指摘しようとしています。私は反対を示す反例を簡単に作成できました。それは理にかなっていますか?
ホイットニーランド2012

13
繰り返しますが、SELECT N + 1の問題は、その核心にあります。取得するレコードが600あります。1つのクエリで600個すべてを取得する方が速いですか、それとも600個のクエリで一度に1つ取得する方が速いですか。MyISAMを使用していない場合、および/または正規化が不十分であるか、インデックスが不十分なスキーマがない場合(この場合、ORMは問題ではありません)、適切に調整されたdbは2ミリ秒で600行を返し、個々の行を返しますそれぞれ約1ミリ秒。したがって、N + 1は数百ミリ秒かかることがよくありますが、結合にはカップルしかかかりません
Dogs

64

製品と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」に設定(デフォルト)

  • 製品のクエリに使用されるフェッチモードは選択です

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

  • キャッシングは初めて役割を果たしません

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

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

// 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つのselectステートメント
  • サプライヤーのN選択ステートメント

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


3
サプライヤーの選択は1で、製品の選択はNである必要がありますか?
bencampbell_14

@bencampbell_ええ、最初は同じと感じました。しかし、彼の例では、それは多くのサプライヤーにとって1つの製品です。
Mohd Faizan Khan、2018

38

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

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

最近では、適切にインデックスが作成されたテーブルのセットに対する単純な結合が、パフォーマンスの観点から問題になることはほとんどありません。また、パフォーマンスに影響がある場合は、インデックスヒントを使用することで解決できます。

これについては、MySQL開発チームの1人がここで説明しています。

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

つまり、MySQLのパフォーマンスが非常に悪いために過去に結合を回避していた場合は、最新バージョンでもう一度試してください。あなたはおそらく喜んで驚かれることでしょう。


7
MySQLの初期バージョンをリレーショナルDBMSと呼ぶのは非常に困難です...これらの問題に遭遇した人々が実際のデータベースを使用していたとしたら、そのような問題には遭遇しなかっただろう。;-)
クレイグ、

2
興味深いことに、これらのタイプの問題の多くは、INNODBエンジンの導入とその後の最適化によってMySQLで解決されましたが、MYISAMを促進しようとする人々は、それがより高速であると考えるため、依然として遭遇します。
Craigが

5
参考までにJOIN、RDBMSで使用される3つの一般的なアルゴリズムの1つは、ネストされたループと呼ばれます。基本的には、内部でN + 1を選択します。唯一の違いは、DBがクライアントのコードにそのパスを明確に強制するのではなく、統計とインデックスに基づいて使用するインテリジェントな選択をしたことです。
Brandon

2
@ブランドンはい!JOINヒントやINDEXヒントと同様に、すべての場合に特定の実行パスを強制しても、データベースに勝ることはめったにありません。データベースは、ほとんどの場合、データを取得するための最適なアプローチを選択するのに非常に優れています。おそらく、DBの初期の頃は、DBを特別な方法で「表現」して、それに沿ってDBを調整する必要がありましたが、何十年にもわたる世界クラスのエンジニアリングの後、データベースにリレーショナルな質問をして、それを許可することで、最高のパフォーマンスを得ることができます。そのデータをフェッチしてアセンブルする方法を整理してください。

3
データベースはインデックスと統計を利用するだけでなく、すべての操作もローカルI / Oであり、その多くはディスクではなく非常に効率的なキャッシュに対して実行されます。データベースプログラマは、これらの種類の最適化に多大な注意を払っています。
Craig

27

この問題のため、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」オブジェクトが2回以上作成されないことを意味します。

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

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

Python のファンフォールディングの実装については、このWebページを参照してください。


10
クレイジーになると思ったので、あなたの投稿につまずいたのはとても嬉しいです。N + 1の問題を見つけたときの私の直感は、必要なすべての情報を含むビューを作成して、そのビューから引き出してみませんか。あなたは私の立場を確認しました。ありがとうございます。
開発者

14
この問題のため、DjangoのORMから離れました。えっ?Djangoにはがありselect_related、これはこれを解決するためのものです-実際、そのドキュメントはあなたのp.car.colour例に似た例から始まります。
Adrian17 2015

8
これは、私たちが持っている、古いanwswerであるselect_related()prefetch_related()今ジャンゴに。
Mariusz Jamro

1
涼しい。しかしselect_related()、友人は、のような明らかに有用な結合の外挿を行っていないようですLEFT OUTER JOIN。問題はインターフェースの問題ではありませんが、オブジェクトとリレーショナルデータはマップ可能であるという奇妙な考えに関係する問題です。
rorycl 2018

26

これは非常に一般的な質問であるため、 この回答に基づいてこの記事を作成しました。

N + 1クエリの問題とは

N + 1クエリの問題は、データアクセスフレームワークがN個の追加のSQLステートメントを実行して、プライマリSQLクエリの実行時に取得できたのと同じデータをフェッチすると発生します。

Nの値が大きいほど、実行されるクエリが多くなり、パフォーマンスへの影響が大きくなります。また、実行速度の遅いクエリを見つけるのに役立つスロークエリログとは異なり、N + 1の問題は特定されません。個々の追加クエリは、スロークエリログをトリガーしないほど速く実行されるためです。

問題は、全体として、応答時間を遅くするのに十分な時間がかかる多数の追加クエリを実行していることです。

1対多のテーブルの関係を形成する次のpostおよびpost_commentsデータベーステーブルがあるとします。

<code> post </ code>および<code> post_comments </ code>テーブル

次の4つのpost行を作成します。

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)

また、4 post_commentつの子レコードも作成します。

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)

INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)

INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)

INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)

プレーンSQLでのN + 1クエリの問題

post_commentsこのSQLクエリを使用して選択した場合:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        pc.post_id AS postId
    FROM post_comment pc
    """, Tuple.class)
.getResultList();

そして、後で、post titleそれぞれに関連付けられたものをフェッチすることにしますpost_comment

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    Long postId = ((Number) comment.get("postId")).longValue();

    String postTitle = (String) entityManager.createNativeQuery("""
        SELECT
            p.title
        FROM post p
        WHERE p.id = :postId
        """)
    .setParameter("postId", postId)
    .getSingleResult();

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

1つのSQLクエリの代わりに5(1 + 4)を実行したため、N + 1クエリの問題をトリガーします。

SELECT
    pc.id AS id,
    pc.review AS review,
    pc.post_id AS postId
FROM post_comment pc

SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'

SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'

SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'

SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'

N + 1クエリの問題の修正は非常に簡単です。次のように、元のSQLクエリで必要なすべてのデータを抽出するだけです。

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        p.title AS postTitle
    FROM post_comment pc
    JOIN post p ON pc.post_id = p.id
    """, Tuple.class)
.getResultList();

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    String postTitle = (String) comment.get("postTitle");

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

今回は、SQLクエリを1つだけ実行して、さらに使用したいすべてのデータをフェッチします。

JPAおよびHibernateでのN + 1クエリの問題

JPAとHibernateを使用する場合、N + 1クエリの問題をトリガーする方法がいくつかあるため、これらの状況を回避する方法を知ることは非常に重要です。

次の例では、postおよびpost_commentsテーブルを次のエンティティにマッピングしていると考えてください。

<code> Post </ code>および<code> PostComment </ code>エンティティ

JPAマッピングは次のようになります。

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    private Long id;

    @ManyToOne
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

FetchType.EAGER

FetchType.EAGERJPAアソシエーションに暗黙的または明示的に使用することは、必要なデータをより多くフェッチするため、悪い考えです。さらに、このFetchType.EAGER戦略ではN + 1クエリの問題が発生しやすくなります。

残念ながら、@ManyToOneとの@OneToOne関連付けはFetchType.EAGERデフォルトで使用されるため、マッピングが次のようになっている場合:

@ManyToOne
private Post post;

あなたはFetchType.EAGER戦略を使用JOIN FETCHしてPostCommentおり、JPQLまたはCriteria APIクエリで一部のエンティティをロードするときに使用するのを忘れるたびに、

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

N + 1クエリの問題をトリガーします。

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

ために実行される追加のSELECT文に注目してくださいpost関連が戻る前にフェッチなければならないListPostCommentエンティティを。

findメソッドを呼び出すときに使用するデフォルトのフェッチプランとは異なり、EnrityManagerJPQLまたはCriteria APIクエリは、JOIN FETCHを自動的に挿入してHibernateが変更できない明示的なプランを定義します。そのため、手動で行う必要があります。

postアソシエーションがまったく必要なかった場合FetchType.EAGERは、フェッチを回避する方法がないため、使用することはできません。そのためFetchType.LAZY、デフォルトで使用することをお勧めします。

ただし、post関連付けを使用したい場合は、を使用JOIN FETCHしてN + 1クエリの問題を解決できます。

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

今回は、Hibernateが単一のSQLステートメントを実行します。

SELECT 
    pc.id as id1_1_0_, 
    pc.post_id as post_id3_1_0_, 
    pc.review as review2_1_0_, 
    p.id as id1_0_1_, 
    p.title as title2_0_1_ 
FROM 
    post_comment pc 
INNER JOIN 
    post p ON pc.post_id = p.id

-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

FetchType.EAGERフェッチ戦略を避けるべき理由の詳細については、こちらの記事もご覧ください。

FetchType.LAZY

FetchType.LAZYすべての関連付けを明示的に使用するように切り替えた場合でも、N + 1の問題にぶつかることがあります。

今回、post関連付けは次のようにマッピングされます。

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

次に、PostCommentエンティティをフェッチすると、

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Hibernateは単一のSQLステートメントを実行します。

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

ただし、後で、遅延ロードされたpost関連付けを参照する場合は、次のようにします。

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

N + 1クエリの問題が発生します。

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

のでpost関連付けが遅延フェッチされたログメッセージを構築するために遅延関連にアクセスするとき、二次SQL文が実行されます。

この場合も、修正はJOIN FETCHJPQLクエリに句を追加することです。

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

そして、FetchType.EAGER例のように、このJPQLクエリは単一のSQLステートメントを生成します。

使用FetchType.LAZYしていて、双方向@OneToOneJPA関係の子関連付けを参照していない場合でも、N + 1クエリの問題をトリガーできます。

@OneToOne関連付けによって生成されるN + 1クエリの問題を解決する方法の詳細については、こちらの記事をご覧ください。

N + 1クエリの問題を自動的に検出する方法

データアクセスレイヤーでN + 1クエリの問題を自動的に検出する場合、この記事では、db-utilオープンソースプロジェクトを使用してその方法を説明します。

まず、次のMaven依存関係を追加する必要があります。

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>${db-util.version}</version>
</dependency>

その後、SQLStatementCountValidatorユーティリティを使用して、生成される基になるSQLステートメントをアサートするだけです。

SQLStatementCountValidator.reset();

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

SQLStatementCountValidator.assertSelectCount(1);

FetchType.EAGER上記のテストケースを使用して実行している場合、次のテストケースエラーが発生します。

SELECT 
    pc.id as id1_1_, 
    pc.post_id as post_id3_1_, 
    pc.review as review2_1_ 
FROM 
    post_comment pc

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2


-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!

db-utilオープンソースプロジェクトの詳細については、こちらの記事をご覧ください。


しかし、今ではページネーションに問題があります。10台の車があり、各車に4つの車輪があり、ページごとに5台の車で車にページ番号を付けたいとします。だからあなたは基本的にあなたが持っていSELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5ます。ただし、LIMITはルート句だけでなく、結果セット全体を制限するため、5輪の2台の車(最初の車はすべて4輪、2番目の車は1輪のみ)が得られます。
CappY 2017

2
私が持っている記事、あまりにもそのためです。
Vlad Mihalcea

記事有難うございます。読みます。高速スクロールによって-その解決策はウィンドウ関数であることがわかりましたが、それらはMariaDBでかなり新しいものであるため、古いバージョンでも問題は解決しません。:)
CappY 2017

@VladMihalcea、私はあなたがN + 1問題を説明しながらManyToOneケースを参照するたびにあなたの記事または投稿から指摘しました。しかし実際には、N + 1の問題に関連するOneToManyのケースに主に関心がある人々です。OneToManyのケースを参照して説明してもらえますか?
JJビーム

18

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 Entity Beanの初期バージョンが機能した方法です。Hibernateのようなものがこれで解消したと思いますが、よくわかりません。ほとんどのツールには、通常、マッピングの戦略に関する情報が含まれています。


18

これは問題の良い説明です

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


17

トピックにAyendeの投稿を確認してください:選択N + 1問題でNHibernateの闘い

基本的に、NHibernateやEntityFrameworkなどのORMを使用する場合、1対多(マスター/詳細)の関係があり、各マスターレコードごとにすべての詳細を一覧表示するには、N + 1クエリ呼び出しをデータベース、 "N"はマスターレコードの数:すべてのマスターレコードを取得する1つのクエリ、およびマスターレコードごとに1つ、マスターレコードごとのすべての詳細を取得するNクエリ。

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

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


3
結合はデカルト積になる可能性があるため、(多くの場合)結合は適切な解決策ではありません。つまり、結果行の数は、ルートテーブルの結果の数に各子テーブルの結果の数を掛けたものになります。複数の階層レベルで特に悪い。20の「ブログ」を選択し、それぞれに100の「投稿」と、各投稿に10の「コメント」を選択すると、結果は20000行になります。NHibernateには、「バッチサイズ」(親IDのin句で子を選択)または「副選択」などの回避策があります。
Erik Hart

14

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


13

私の意見では、Hibernate Pitfall:なぜ関係怠惰なければならないのかという記事は、実際のN + 1問題と正反対です。

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

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


2
休止状態のページを読みます。それは何を言っていないN + 1セレクト問題は実際にあります。ただし、結合を使用して修正できるとあります。
Ian Boyd

3
1つのselectステートメントで複数の親の子オブジェクトを選択するには、select fetchingにbatch-sizeが必要です。副選択は別の代替手段になる可能性があります。複数の階層レベルがあり、デカルト積が作成されると、結合は非常に悪くなります。
エリックハート

10

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

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


9

1つの億万長者がN台の車を持っています。すべての(4)ホイールを取得します。

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

費用:

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

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

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

最悪の場合の追加コスト1 + Nプレートアクセスインデックスをロードするためのアクセス。

概要

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


9

N + 1 selectの問題は厄介であり、ユニットテストでそのようなケースを検出することは理にかなっています。特定のテストメソッドまたは任意のコードブロックによって実行されるクエリの数を確認するための小さなライブラリを開発しました-JDBC Sniffer

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

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

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

5

他の人がよりエレガントに述べている問題は、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 ?);

次に、各OneToManySELECTについて、idsテーブルでa を実行しINNER JOIN、子テーブルにa WHERE batch_id=(またはその逆)を実行します。結果列のマージが簡単になるので、id列で順序付けすることを確認するだけです(それ以外の場合は、結果セット全体のHashMap / Tableが必要ですが、それほど悪くはありません)。

次に、idsテーブルを定期的にクリーンアップします。

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

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


1

Matt Solnitの例で、CarとWheelsの間の関連付けをLAZYとして定義し、いくつかのWheelsフィールドが必要だと想像してください。つまり、最初の選択の後、Hibernateは各車に対して「Select * from Wheels where car_id =:id」を実行します。

これにより、N台ごとに最初の選択と1つ以上の選択が行われます。これが、n + 1問題と呼ばれる理由です。

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

ただし、関連付けられているWheelに何度もアクセスしない場合は、遅延させるか、Criteriaを使用してフェッチタイプを変更することをお勧めします。


1
この場合も、結合は良い解決策ではありません。特に、2つ以上の階層レベルがロードされる可能性がある場合はそうです。代わりに「subselect」または「batch-size」を確認してください。最後は、「select ... from wheel where car_id in(1,3,4,6,7,8,11,13)」のように、「in」句の親IDによって子をロードします。
エリックハート
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.