「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の選択を防ぐいくつかの方法を提供します。
参照:Hibernateを使用したJava Persistence、第13章。
SELECT * from Wheel;
N + 1ではなく1つの選択()ですべてのホイールを取得できます。Nが大きい場合、パフォーマンスへの影響は非常に大きくなります。
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の利点は、クエリの複雑さが軽減され、子結果セットが最初の要求時にのみロードされる遅延ロードを使用できることです。
製品と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=?
結果:
これはN + 1選択問題です!
十分な評判がないため、他の回答に直接コメントすることはできません。しかし、この問題が本質的に発生するのは注目に値します。これまで、結合の処理に関しては、歴史的に多くの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のパフォーマンスが非常に悪いために過去に結合を回避していた場合は、最新バージョンでもう一度試してください。あなたはおそらく喜んで驚かれることでしょう。
JOIN
、RDBMSで使用される3つの一般的なアルゴリズムの1つは、ネストされたループと呼ばれます。基本的には、内部でN + 1を選択します。唯一の違いは、DBがクライアントのコードにそのパスを明確に強制するのではなく、統計とインデックスに基づいて使用するインテリジェントな選択をしたことです。
この問題のため、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ページを参照してください。
select_related
、これはこれを解決するためのものです-実際、そのドキュメントはあなたのp.car.colour
例に似た例から始まります。
select_related()
とprefetch_related()
今ジャンゴに。
select_related()
、友人は、のような明らかに有用な結合の外挿を行っていないようですLEFT OUTER JOIN
。問題はインターフェースの問題ではありませんが、オブジェクトとリレーショナルデータはマップ可能であるという奇妙な考えに関係する問題です。
これは非常に一般的な質問であるため、 この回答に基づいてこの記事を作成しました。
N + 1クエリの問題は、データアクセスフレームワークがN個の追加のSQLステートメントを実行して、プライマリSQLクエリの実行時に取得できたのと同じデータをフェッチすると発生します。
Nの値が大きいほど、実行されるクエリが多くなり、パフォーマンスへの影響が大きくなります。また、実行速度の遅いクエリを見つけるのに役立つスロークエリログとは異なり、N + 1の問題は特定されません。個々の追加クエリは、スロークエリログをトリガーしないほど速く実行されるためです。
問題は、全体として、応答時間を遅くするのに十分な時間がかかる多数の追加クエリを実行していることです。
1対多のテーブルの関係を形成する次のpostおよびpost_commentsデータベーステーブルがあるとします。
次の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)
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クエリの問題をトリガーする方法がいくつかあるため、これらの状況を回避する方法を知ることは非常に重要です。
次の例では、post
およびpost_comments
テーブルを次のエンティティにマッピングしていると考えてください。
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.EAGER
JPAアソシエーションに暗黙的または明示的に使用することは、必要なデータをより多くフェッチするため、悪い考えです。さらに、この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
関連が戻る前にフェッチなければならないList
のPostComment
エンティティを。
のfind
メソッドを呼び出すときに使用するデフォルトのフェッチプランとは異なり、EnrityManager
JPQLまたは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 FETCH
JPQLクエリに句を追加することです。
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
していて、双方向@OneToOne
JPA関係の子関連付けを参照していない場合でも、N + 1クエリの問題をトリガーできます。
@OneToOne
関連付けによって生成される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!
SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5
ます。ただし、LIMITはルート句だけでなく、結果セット全体を制限するため、5輪の2台の車(最初の車はすべて4輪、2番目の車は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 Entity Beanの初期バージョンが機能した方法です。Hibernateのようなものがこれで解消したと思いますが、よくわかりません。ほとんどのツールには、通常、マッピングの戦略に関する情報が含まれています。
問題を理解したので、通常はクエリで結合フェッチを実行することで問題を回避できます。これは基本的に、遅延ロードされたオブジェクトのフェッチを強制するため、データはn + 1クエリではなく1つのクエリで取得されます。お役に立てれば。
トピックにAyendeの投稿を確認してください:選択N + 1問題でNHibernateの闘い。
基本的に、NHibernateやEntityFrameworkなどのORMを使用する場合、1対多(マスター/詳細)の関係があり、各マスターレコードごとにすべての詳細を一覧表示するには、N + 1クエリ呼び出しをデータベース、 "N"はマスターレコードの数:すべてのマスターレコードを取得する1つのクエリ、およびマスターレコードごとに1つ、マスターレコードごとのすべての詳細を取得するNクエリ。
データベースクエリ呼び出しの増加→待機時間の増加→アプリケーション/データベースのパフォーマンスの低下。
ただし、ORMには、主にJOINを使用して、この問題を回避するオプションがあります。
私の意見では、Hibernate Pitfall:なぜ関係は怠惰でなければならないのかという記事は、実際のN + 1問題と正反対です。
正しい説明が必要な場合は、Hibernate-章19:パフォーマンスの向上-フェッチ戦略を参照してください。
選択フェッチ(デフォルト)はN + 1選択問題に対して非常に脆弱であるため、結合フェッチを有効にすることができます。
提供されているリンクには、n + 1問題の非常に単純な例があります。これをHibernateに適用すると、基本的に同じことを話していることになります。オブジェクトに対してクエリを実行すると、エンティティは読み込まれますが、関連付けが(特に設定されていない限り)遅延読み込みされます。したがって、ルートオブジェクトに対する1つのクエリと、これらのそれぞれの関連付けをロードするための別のクエリです。100個のオブジェクトが返された場合、最初のクエリが1つあり、その後、n + 1の関連付けを取得するために100個のクエリが追加されます。
1つの億万長者がN台の車を持っています。すべての(4)ホイールを取得します。
1つのクエリがすべての車をロードしますが、各(N)の車に対して、ホイールをロードするための個別のクエリが送信されます。
費用:
インデックスがRAMに収まると仮定します。
1 + Nクエリの解析と計画+インデックス検索、および1 + N +(N * 4)プレートアクセスによるペイロードのロード。
インデックスがRAMに適合しないと仮定します。
最悪の場合の追加コスト1 + Nプレートアクセスインデックスをロードするためのアクセス。
概要
ボトルネックはプレートアクセスです(hddでは毎秒70回ランダムアクセス)熱心な結合選択では、ペイロードのプレートに1 + N +(N * 4)回アクセスします。したがって、インデックスがramに収まる場合-問題ありません。ram操作のみが関与するため、十分に高速です。
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
}
他の人がよりエレガントに述べている問題は、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
列SELECT
について、idsテーブルでa を実行しINNER JOIN
、子テーブルにa WHERE batch_id=
(またはその逆)を実行します。結果列のマージが簡単になるので、id列で順序付けすることを確認するだけです(それ以外の場合は、結果セット全体のHashMap / Tableが必要ですが、それほど悪くはありません)。
次に、idsテーブルを定期的にクリーンアップします。
これは、ユーザーがある種のバルク処理のために、たとえば100程度の個別のアイテムを選択した場合にも特にうまく機能します。一時テーブルに100個の異なるIDを入れます。
これで、実行するクエリの数はOneToMany列の数になります。
Matt Solnitの例で、CarとWheelsの間の関連付けをLAZYとして定義し、いくつかのWheelsフィールドが必要だと想像してください。つまり、最初の選択の後、Hibernateは各車に対して「Select * from Wheels where car_id =:id」を実行します。
これにより、N台ごとに最初の選択と1つ以上の選択が行われます。これが、n + 1問題と呼ばれる理由です。
これを回避するには、関連付けを積極的にフェッチして、Hibernateが結合でデータをロードするようにします。
ただし、関連付けられているWheelに何度もアクセスしない場合は、遅延させるか、Criteriaを使用してフェッチタイプを変更することをお勧めします。