चूंकि यह एक बहुत ही सामान्य प्रश्न है, इसलिए मैंने यह लेख लिखा है
, जिस पर यह उत्तर आधारित है।
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
ओपन-सोर्स प्रोजेक्ट के बारे में अधिक जानकारी के लिए , इस लेख को देखें ।