चूंकि यह एक बहुत ही सामान्य प्रश्न है, इसलिए मैंने यह लेख लिखा है
, जिस पर यह उत्तर आधारित है।
N + 1 क्वेरी समस्या क्या है
N + 1 क्वेरी समस्या तब होती है जब डेटा एक्सेस फ्रेमवर्क ने N को अतिरिक्त SQL स्टेटमेंट निष्पादित किया वही डेटा लाने के लिए जिसे प्राथमिक SQL क्वेरी निष्पादित करते समय पुनर्प्राप्त किया जा सकता था।
N का मान जितना बड़ा होगा, उतने अधिक प्रश्नों का निष्पादन होगा, प्रदर्शन प्रभाव उतना ही बड़ा होगा। और, धीमी क्वेरी लॉग के विपरीत, जो धीमी गति से चलने वाले प्रश्नों को खोजने में आपकी मदद कर सकता है, N + 1 समस्या हाजिर नहीं होगी क्योंकि प्रत्येक अलग-अलग अतिरिक्त क्वेरी धीमी क्वेरी लॉग को ट्रिगर न करने के लिए पर्याप्त रूप से तेज़ चलती है।
समस्या बड़ी संख्या में अतिरिक्त प्रश्नों को निष्पादित कर रही है, जो कुल मिलाकर, प्रतिक्रिया समय को धीमा करने के लिए पर्याप्त समय लेती है।
आइए विचार करें कि हमारे पास निम्नलिखित पोस्ट और पोस्ट_काम डेटाबेस टेबल हैं जो एक से कई टेबल संबंध बनाते हैं :

हम निम्नलिखित 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
);
}
आप N + 1 क्वेरी समस्या को ट्रिगर करने जा रहे हैं, क्योंकि एक SQL क्वेरी के बजाय, आपने 5 (1 + 4) निष्पादित किया है:
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
);
}
इस बार, केवल एक एसक्यूएल क्वेरी को निष्पादित किया जाता है ताकि हम उन सभी डेटा को प्राप्त कर सकें जिनका हम आगे उपयोग करने में रुचि रखते हैं।
JPA और हाइबरनेट के साथ N + 1 क्वेरी समस्या
JPA और हाइबरनेट का उपयोग करते समय, ऐसे कई तरीके हैं जिनसे आप N + 1 क्वेरी समस्या को ट्रिगर कर सकते हैं, इसलिए यह जानना बहुत महत्वपूर्ण है कि आप इन स्थितियों से कैसे बच सकते हैं।
अगले उदाहरणों के लिए, विचार करें कि हम postऔर post_commentsतालिकाओं को निम्न संस्थाओं के लिए मैप कर रहे हैं :

जेपीए मैपिंग इस तरह दिखती है:
@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रणनीति एन + 1 क्वेरी मुद्दों के लिए भी प्रवण है।
दुर्भाग्य से, @ManyToOneऔर @OneToOneएसोसिएशन FetchType.EAGERडिफ़ॉल्ट रूप से उपयोग करते हैं , इसलिए यदि आपके मैपिंग इस तरह दिखते हैं:
@ManyToOne
private Post post;
आप FetchType.EAGERरणनीति का उपयोग कर रहे हैं , और, हर बार जब आप एक JPQL या मानदंड एपीआई क्वेरी के साथ JOIN FETCHकुछ PostCommentसंस्थाओं को लोड करते समय उपयोग करना भूल जाते हैं :
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
निष्पादित किए जाने वाले अतिरिक्त चयन कथनों पर ध्यान दें क्योंकि postएसोसिएशन को वापस लौटने से पहले लाया जाना ListहैPostComment संस्थाओं।
डिफ़ॉल्ट लाने की योजना के विपरीत, जिसे आप उपयोग कर रहे हैं , जेपीएनएल या मानदंड एपीआई क्वेरी की findविधि को बुलाते हुए EnrityManager, एक स्पष्ट योजना को परिभाषित करता है कि हाइबरनेट स्वचालित रूप से एक जॉय फ़च को इंजेक्ट करके नहीं बदल सकता है। तो, आपको इसे मैन्युअल रूप से करने की आवश्यकता है।
यदि आपको postएसोसिएशन की बिल्कुल भी आवश्यकता नहीं है , तो आप उपयोग करते समय भाग्य से बाहर हैं FetchType.EAGERक्योंकि इसे लाने से बचने का कोई तरीका नहीं है। इसलिए इसका उपयोग करना बेहतर हैFetchType.LAZY डिफ़ॉल्ट रूप ।
लेकिन, यदि आप postएसोसिएशन का उपयोग करना चाहते हैं , तो आप JOIN FETCHएन + 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()
);
}
इस बार, हाइबरनेट एकल 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सभी संघों के लिए स्पष्ट रूप से उपयोग करने के लिए स्विच करते हैं , तो भी आप एन + 1 मुद्दे से टकरा सकते हैं।
इस बार, post एसोसिएशन को इस तरह मैप किया गया है:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
अब, जब आप प्राप्त करते हैं PostComment संस्थाओं को :
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
हाइबरनेट एकल 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एसोसिएशन को लेज़ीली लिया जाता है, लॉग संदेश बनाने के लिए आलसी एसोसिएशन तक पहुँचने पर एक माध्यमिक एसक्यूएल स्टेटमेंट निष्पादित किया जाएगा।
फिर से, फिक्स 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 क्वेरी समस्या का स्वचालित रूप से पता लगाने के लिए कैसे
यदि आप अपने डेटा एक्सेस लेयर में N + 1 क्वेरी समस्या का स्वचालित रूप से पता लगाना चाहते हैं, तो यह आलेख बताता है कि आप db-utilओपन-सोर्स प्रोजेक्ट का उपयोग करके कैसे कर सकते हैं ।
सबसे पहले, आपको निम्नलिखित मावेन निर्भरता को जोड़ने की आवश्यकता है:
<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ओपन-सोर्स प्रोजेक्ट के बारे में अधिक जानकारी के लिए , इस लेख को देखें ।