Bu çok yaygın bir soru olduğundan, bu cevabın dayandığı bu makaleyi yazdım
.
N + 1 sorgu sorunu nedir
N + 1 sorgu sorunu, veri erişim çerçevesi birincil SQL sorgusu yürütülürken alınabilecek verileri almak için N ek SQL deyimi yürüttüğünde ortaya çıkar.
N değeri ne kadar büyük olursa, o kadar çok sorgu yürütülür, performans etkisi o kadar büyük olur. Yavaş çalışan sorguları bulmanıza yardımcı olabilecek yavaş sorgu günlüğünden farklı olarak , N + 1 sorunu her bir ek sorgu yavaş sorgu günlüğünü tetiklememek için yeterince hızlı çalıştığından nokta olmayacaktır.
Sorun genel olarak yanıt süresini yavaşlatmak için yeterli zaman alan çok sayıda ek sorgu yürütmektir.
Bir -çok tablo ilişkisi oluşturan aşağıdaki post ve post_comments veritabanı tablolarımız olduğunu düşünelim :
Aşağıdaki 4 post
satırı oluşturacağız :
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)
Ayrıca 4 post_comment
çocuk kaydı da oluşturacağız :
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)
Düz SQL'de N + 1 sorgu sorunu
post_comments
Bu SQL sorgusunu kullanarak seçerseniz :
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();
Ve daha sonra, post
title
her biri için ilişkili almaya karar verdiniz 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
);
}
Bir SQL sorgusu yerine 5 (1 + 4) yürüttüğünüz için N + 1 sorgu sorununu tetikleyeceksiniz:
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 sorgu sorununu çözmek çok kolaydır. Tek yapmanız gereken orijinal SQL sorgusunda ihtiyacınız olan tüm verileri aşağıdaki gibi çıkarmaktır:
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
);
}
Bu kez, daha fazla ilgilendiğimiz tüm verileri almak için sadece bir SQL sorgusu yürütülür.
JPA ve Hazırda Bekletme ile ilgili N + 1 sorgu sorunu
JPA ve Hibernate kullanırken, N + 1 sorgu sorununu tetiklemenin birkaç yolu vardır, bu nedenle bu durumlardan nasıl kaçınabileceğinizi bilmek çok önemlidir.
Sonraki örnekler için post
ve post_comments
tablolarını aşağıdaki varlıklarla eşleştirdiğimizi düşünün :
JPA eşlemeleri şöyle görünür:
@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 ilişkilendirmeleriniz için örtülü veya açık bir şekilde kullanmak kötü bir fikirdir, çünkü ihtiyacınız olan daha fazla veri alacaksınız. Dahası, FetchType.EAGER
strateji N + 1 sorgu sorunlarına da yatkındır.
Ne yazık ki, @ManyToOne
ve @OneToOne
ilişkilendirmeler FetchType.EAGER
varsayılan olarak kullanılır, bu nedenle eşlemeleriniz şöyle görünürse:
@ManyToOne
private Post post;
FetchType.EAGER
Stratejiyi kullanıyorsunuz ve JPQL veya Criteria API sorgusuyla JOIN FETCH
bazı PostComment
varlıkları yüklerken kullanmayı her unuttuğunuzda :
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
N + 1 sorgu sorununu tetikleyeceksiniz:
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
Çünkü yürütür ek SEÇ ifadeleri dikkat post
dernek öncesinde geri dönen getirilen olmak zorunda List
arasındaPostComment
kuruluşlar.
find
Yöntemini çağırırken kullandığınız varsayılan getirme planından farklı olarak EnrityManager
, bir JPQL veya Ölçüt API'si sorgusu, Hazırda Bekletme öğesinin otomatik olarak bir JOIN FETCH enjekte ederek değiştiremeyeceği açık bir plan tanımlar. Yani, manuel olarak yapmanız gerekir.
post
İlişkilendirmeye hiç ihtiyacınız FetchType.EAGER
yoksa, kullanırken şansınız kalmaz çünkü getirmekten kaçınmanın bir yolu yoktur. Bu yüzden FetchType.LAZY
varsayılan olarak kullanmak daha iyidir .
Ancak, post
ilişkilendirmeyi kullanmak istiyorsanız JOIN FETCH
, N + 1 sorgu sorununu çözmek için kullanabilirsiniz:
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()
);
}
Hazırda Bekleme, tek bir SQL deyimi yürütür:
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
Getirme stratejisinden neden kaçınmanız gerektiği hakkında daha fazla bilgi için bu makaleye de göz atın.
FetchType.LAZY
FetchType.LAZY
Tüm ilişkilendirmeler için açıkça kullanmaya geçseniz bile , N + 1 sorunuyla karşılaşabilirsiniz.
Bu kez, post
ilişkilendirme şu şekilde eşlenir:
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
Şimdi, PostComment
varlıkları getirdiğinizde :
List<PostComment> comments = entityManager
.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
Hazırda Beklet, tek bir SQL deyimi yürütür:
SELECT
pc.id AS id1_1_,
pc.post_id AS post_id3_1_,
pc.review AS review2_1_
FROM
post_comment pc
Ancak, daha sonra, tembel yüklenen post
ilişkilendirmeye başvuracaksınız :
for(PostComment comment : comments) {
LOGGER.info(
"The Post '{}' got this review '{}'",
comment.getPost().getTitle(),
comment.getReview()
);
}
N + 1 sorgu sorunuyla karşılaşırsınız:
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'
Çünkü post
dernek lazily getirilirse günlük mesaj oluşturmak için tembel dernek erişirken, ikincil bir SQL deyimi çalıştırılacaktır.
Yine, düzeltme JOIN FETCH
JPQL sorgusuna bir cümle eklemekten ibarettir :
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()
);
}
Ve tıpkı FetchType.EAGER
örnekte olduğu gibi , bu JPQL sorgusu tek bir SQL ifadesi üretecektir.
Çift FetchType.LAZY
yönlü bir @OneToOne
JPA ilişkisinin alt ilişkilendirmesini kullanıyor ve buna başvurmasanız bile, N + 1 sorgu sorununu tetikleyebilirsiniz.
@OneToOne
İlişkilendirmeler tarafından oluşturulan N + 1 sorgu sorununu nasıl aşabileceğiniz hakkında daha fazla bilgi için bu makaleye göz atın .
N + 1 sorgu sorunu otomatik olarak nasıl algılanır
Veri erişim katmanınızdaki N + 1 sorgu sorununu otomatik olarak algılamak istiyorsanız, bu makalede bunu db-util
açık kaynaklı projeyi kullanarak nasıl yapabileceğiniz açıklanmaktadır .
İlk olarak, aşağıdaki Maven bağımlılığını eklemeniz gerekir:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>db-util</artifactId>
<version>${db-util.version}</version>
</dependency>
Daha sonra, SQLStatementCountValidator
oluşturulan temel SQL deyimlerini desteklemek için yardımcı programı kullanmanız yeterlidir:
SQLStatementCountValidator.reset();
List<PostComment> comments = entityManager.createQuery("""
select pc
from PostComment pc
""", PostComment.class)
.getResultList();
SQLStatementCountValidator.assertSelectCount(1);
FetchType.EAGER
Yukarıdaki test senaryosunu kullanıyorsanız ve çalıştırırsanız, aşağıdaki test senaryosu hatasını alırsınız:
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
Açık kaynaklı proje hakkında daha fazla bilgi için bu makaleye göz atın .