ORM'de (Nesne-İlişkisel Eşleme) “N + 1 problem seçer” nedir?


1596

"N + 1 sorunu seçer" genellikle Nesne İlişkisel eşleme (ORM) tartışmalarında bir sorun olarak belirtilir ve bunun nesnede basit görünen bir şey için çok sayıda veritabanı sorgusu yapmak zorunda kaldığını anlıyorum. dünya.

Sorunun daha ayrıntılı bir açıklaması olan var mı?


2
Bu, n + 1 problemini anlama konusunda güzel bir açıklama ile harika bir bağlantıdır . Ayrıca, bu sorunun
üstesinden


Bu soruna çözüm arayan herkes için, bunu açıklayan bir yazı buldum. stackoverflow.com/questions/32453989/…
damndemon

2
Cevaplar düşünüldüğünde, bu 1 + N problemi olarak adlandırılmamalı mı? Bu bir terminoloji gibi göründüğü gibi, ben özellikle OP'ye sormuyorum.
user1418717

Yanıtlar:


1015

Diyelim ki bir Carnesne koleksiyonunuz var (veritabanı satırları) ve her Carbirinde bir Wheelnesne koleksiyonu (satırlar da var) var. Başka bir deyişle, CarWheel1'den çoğa ilişkisidir.

Şimdi, tüm arabaları tekrarlamanız gerektiğini ve her biri için tekerleklerin bir listesini yazdırdığınızı varsayalım. Saf O / R uygulaması aşağıdakileri yapacaktır:

SELECT * FROM Cars;

Ve sonra her biri için Car:

SELECT * FROM Wheel WHERE CarId = ?

Başka bir deyişle, Otomobiller için bir seçiminiz var ve N ek seçim var, burada N toplam otomobil sayısıdır.

Alternatif olarak, tüm tekerlekler alınabilir ve bellekte arama yapılabilir:

SELECT * FROM Wheel

Bu, veritabanına gidiş-dönüş sayısını N + 1'den 2'ye düşürür. Çoğu ORM aracı, N + 1 seçimlerini önlemek için size çeşitli yollar sunar.

Referans: Hazırda Bekletme ile Java Kalıcılığı , bölüm 13.


140
"Bu kötü" hakkında netleştirmek için - tüm tekerlekleri SELECT * from Wheel;N + 1 yerine 1 select ( ) ile alabilirsiniz . Büyük bir N ile performans isabeti çok önemli olabilir.
tucuxi

211
@tucuxi Yanlış yaptığınız için çok fazla oy aldığınız için şaşırdım. Bir veritabanı dizinler hakkında çok iyi, belirli bir CarID için sorgu yapmak çok hızlı dönecekti. Ancak tüm Tekerlekler bir kez varsa, uygulamanızda dizinlenmemiş olan CarID'yi aramak zorunda kalırsınız, bu daha yavaştır. Veritabanınıza ulaşan büyük gecikme sorunlarınız olmadığı sürece n + 1 aslında daha hızlıdır - ve evet, çok çeşitli gerçek dünya kodu ile karşılaştırdım.
Ariel

74
@ariel 'Doğru' yol, CarId tarafından sipariş edilen tüm tekerlekleri elde etmektir (1 seçim) ve CarId'den daha fazla ayrıntı gerekiyorsa, tüm arabalar için ikinci bir sorgu yapın (toplam 2 sorgu). Bir şeyleri yazdırmak artık en uygun ve hiçbir dizin veya ikincil depolama gerekli değildi (sonuçları tekrarlayabilirsiniz, hepsini indirmenize gerek yok). Yanlış şeyi kıyasladın. Kriterlerinizden hala eminseniz, denemenizi ve sonuçlarınızı açıklayan daha uzun bir yorum (veya tam bir yanıt) yayınlamak ister misiniz?
tucuxi

92
"Hazırda Bekletme (diğer ORM çerçevelerine aşina değilim) size bunu işlemek için çeşitli yollar sunar." ve bu şekilde mi?
Tima

58
@Ariel Kriterlerinizi veritabanı ve uygulama sunucularıyla ayrı makinelerde çalıştırmayı deneyin. Benim tecrübelerime göre, veritabanına gidiş-dönüş gezisi, sorgudan daha fazla maliyete neden olmaktadır. Yani evet, sorgular gerçekten hızlı, ama hasara yol açan yuvarlak geziler. Ben "NEREDE ID = const " dönüştürdüm "NEREDE ID IN ( const , const , ...)" ve aldım büyüklük siparişleri ondan artış.
Hans

110
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Bu, table2'deki her alt satır için table1 sonuçlarını döndürerek, table2 içindeki alt satırların çoğaltmaya neden olduğu bir sonuç kümesi sağlar. O / R eşleyicileri, table1 örneklerini benzersiz bir anahtar alanına göre ayırmalı, ardından alt örnekleri doldurmak için tüm table2 sütunlarını kullanmalıdır.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N + 1, ilk sorgunun birincil nesneyi, ikinci sorgunun da döndürülen benzersiz birincil nesnelerin her biri için tüm alt nesneleri doldurduğu yerdir.

Düşünmek:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

ve benzer bir yapıya sahip masalar. "22 Valley St" adresi için tek bir sorgu olabilir:

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

O / RM, bir Home örneğini ID = 1, Address = "22 Valley St" ile doldurmalı ve ardından Inhabitants dizisini Dave, John ve Mike için People örnekleriyle tek bir sorgu ile doldurmalıdır.

Yukarıda kullanılan aynı adres için bir N + 1 sorgusu şunlarla sonuçlanır:

Id Address
1  22 Valley St

gibi ayrı bir sorgu ile

SELECT * FROM Person WHERE HouseId = 1

gibi ayrı bir veri kümesiyle sonuçlanır.

Name    HouseId
Dave    1
John    1
Mike    1

ve nihai sonuç, tek sorgu ile yukarıdakiyle aynıdır.

Tek seçimin avantajları, tüm verileri en üst düzeye çıkarmanızdır. N + 1'in avantajları sorgu karmaşıklığının azalmasıdır ve alt sonuç kümelerinin yalnızca ilk istek üzerine yüklendiği tembel yüklemeyi kullanabilirsiniz.


4
N + 1'in diğer avantajı, veritabanının sonuçları doğrudan bir dizinden döndüğü için daha hızlı olmasıdır. Birleştirmeyi ve ardından sıralama yapmak, daha yavaş bir geçici tablo gerektirir. N + 1'den kaçınmanın tek nedeni, veritabanınızla konuşmada çok fazla gecikmenin olması.
Ariel

17
Birleştirme ve sıralama oldukça hızlı olabilir (çünkü dizine eklenen ve muhtemelen sıralanan alanlara katılacaksınız). 'N + 1' ne kadar büyük? N + 1 sorununun yalnızca yüksek gecikmeli veritabanı bağlantıları için geçerli olduğuna inanıyor musunuz?
tucuxi

9
@ariel - Karşılaştırma ölçütleriniz doğru olsa bile N + 1'in "en hızlı" olduğuna dair öneriniz yanlış. Bu nasıl mümkün olabilir? Bkz. En.wikipedia.org/wiki/Anecdotal_evidence ve ayrıca bu sorunun diğer cevabındaki yorumum.
whitneyland

7
@Ariel - Bence iyi anladım :). Sadece sonucunuzun sadece bir dizi koşul için geçerli olduğunu belirtmeye çalışıyorum. Kolayca bunun tersini gösteren bir karşı örnek oluşturabilirim. bu mantıklı mı?
whitneyland

13
Tekrarlamak gerekirse, SELECT N + 1 problemi özünde: Alınacak 600 kaydım var. Tek bir sorguda 600 veya 600 sorguda bir kerede 1 almak daha hızlı mı? MyISAM'de değilseniz ve / veya zayıf normalleştirilmiş / zayıf endekslenmiş bir şemanız yoksa (bu durumda ORM sorun değildir), düzgün ayarlanmış bir db, 2 ms'de 600 satırı döndürürken, tek tek satırları döndürür her biri yaklaşık 1 ms. Sıklıkla N + 1'in yüzlerce milisaniye sürdüğünü görüyoruz, bir birleşim sadece bir çift alır
Köpekler

64

Ürünle bire çok ilişkisi olan tedarikçi. Bir Tedarikçi birçok Ürüne sahiptir (tedarik eder).

***** 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      |
+-----+-----------+--------------------+-------+------------+

Faktörler:

  • Tedarikçi için tembel mod “true” olarak ayarlandı (varsayılan)

  • Ürün üzerinde sorgulama için kullanılan getirme modu Seçildi

  • Getirme modu (varsayılan): Tedarikçi bilgilerine erişilir

  • Önbellekleme ilk kez rol oynamaz

  • Tedarikçiye erişildi

Getirme modu Getirmeyi Seç (varsayılan)

// 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=?

Sonuç:

  • Ürün için 1 select ifadesi
  • Tedarikçi için N select ifadeleri

Bu N + 1 seçim problemi!


3
Tedarikçi için 1 seçim olması ve ardından Ürün için N'nin seçilmesi gerekiyor mu?
bencampbell_14

@bencampbell_ Evet, başlangıçta aynısını hissettim. Ama sonra örneği ile birçok tedarikçiye bir ürün.
Mohd Faizan Khan

38

Diğer cevaplara doğrudan yorum yapamam, çünkü yeterince itibarım yok. Ancak, sorunun esasen sadece ortaya çıktığını belirtmek gerekir, çünkü tarihsel olarak, birleştirmelerle uğraşmak söz konusu olduğunda birçok dbm oldukça zayıftır (MySQL özellikle dikkate değer bir örnektir). Bu yüzden n + 1 genellikle bir birleştirmeden çok daha hızlı olmuştur. Ve sonra n + 1'de iyileşmenin yolları var, ancak yine de katılmaya gerek kalmadan, orijinal sorunun anlamı bu.

Ancak, MySQL artık birleşme söz konusu olduğunda olduğundan çok daha iyi. MySQL'i ilk öğrendiğimde, bir çok birleşim kullandım. Sonra ne kadar yavaş olduklarını keşfettim ve kodda n + 1'e geçtim. Ancak, son zamanlarda, birleşimlere geri dönüyorum, çünkü MySQL artık onları kullanmaya başladığımdan çok daha iyi bir halt.

Günümüzde, uygun şekilde dizine alınmış bir tablo kümesine basit bir birleşim, performans açısından nadiren bir sorundur. Ve eğer bir performans isabeti verirse, indeks ipuçlarının kullanımı genellikle bunları çözer.

Bu, burada MySQL geliştirme ekibinden biri tarafından tartışılmaktadır:

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

Özet olarak: MySQL'in onlarla yaptığı kötü performans nedeniyle geçmişte birleşmelerden kaçındıysanız, en son sürümleri tekrar deneyin. Muhtemelen şaşıracaksınız.


7
MySQL'in ilk sürümlerini ilişkisel bir DBMS olarak adlandırmak oldukça esnektir ... Bu sorunlarla karşılaşan insanlar gerçek bir veritabanı kullanıyor olsaydı, bu tür sorunlarla karşılaşmazlardı. ;-)
Craig

2
İlginç bir şekilde, bu tür sorunların çoğu, INSODB motorunun tanıtımı ve müteakip optimizasyonu ile MySQL'de çözüldü, ancak yine de daha hızlı olduğunu düşündükleri için MYISAM'ı tanıtmaya çalışan insanlarla karşılaşacaksınız.
Craig

5
RDBMS'de JOINkullanılan 3 yaygın algoritmadan biri olan FYI, iç içe döngüler olarak adlandırılır. Temelde kaputun altında bir N + 1 seçimi. Tek fark, DB'nin onu kategorik olarak zorlayan istemci kodu yerine, istatistikleri ve dizinleri temel alarak kullanmak için akıllı bir seçim yapmasıdır.
Brandon

2
@Brandon Evet! JOIN ipuçları ve INDEX ipuçları gibi, her durumda belirli bir yürütme yolunu zorlamak veritabanını nadiren yener. Veri tabanı elde etmek için en uygun yaklaşımı seçmede veritabanı neredeyse her zaman çok, çok iyidir. Belki dbs'nin ilk günlerinde sorunuzu db'yi birlikte kandırmak için tuhaf bir şekilde 'ifade etmeniz' gerekiyordu, ancak onlarca yıllık dünya standartlarında mühendislikten sonra, veritabanınıza ilişkisel bir soru sorarak ve izin vererek en iyi performansı elde edebilirsiniz. bu verileri sizin için nasıl getireceğinizi ve birleştireceğinizi sıralayın.
köpekler

3
Veritabanı sadece dizinler ve istatistikler kullanmakla kalmaz, aynı zamanda tüm işlemler yerel G / Ç'dir ve çoğu diskten ziyade yüksek verimli önbellekle çalışır. Veritabanı programcıları bu tür şeyleri optimize etmeye çok dikkat ediyorlar.
Craig

27

Bu sorun nedeniyle Django'daki ORM'den uzaklaştık. Temel olarak, denerseniz ve yaparsanız

for p in person:
    print p.car.colour

ORM tüm insanları mutlu bir şekilde iade edecektir (genellikle bir Person nesnesinin örnekleri olarak), ancak daha sonra her Kişi için araba tablosunu sorgulaması gerekir.

Buna basit ve çok etkili bir yaklaşım, " hayran ağılama " dediğim şeydir , bir ilişkisel veritabanından gelen sorgu sonuçlarının, sorgunun oluşturulduğu orijinal tablolarla dair saçma fikirden kaçınan " olarak .

Adım 1: Geniş seçim

  select * from people_car_colour; # this is a view or sql function

Bu gibi bir şey döndürecek

  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. Adım: Nesneleştirme

Sonuçları üçüncü öğeden sonra bölünecek bir argümanla genel bir nesne oluşturucuya alın. Bu, "jones" nesnesinin birden fazla yapılmayacağı anlamına gelir.

3. Adım: Oluşturma

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

Python için fan katlama uygulaması için bu web sayfasına bakın .


10
Gönderinize tökezlediğim için çok memnunum, çünkü delirdiğimi sanıyordum. N + 1 problemini öğrendiğimde, hemen düşündüm - neden sadece ihtiyacınız olan tüm bilgileri içeren bir görünüm oluşturmuyorsunuz ve bu görünümden çekmiyorsunuz? benim konumumu doğruladın. teşekkürler bayım.
bir geliştirici

14
Bu sorun nedeniyle Django'daki ORM'den uzaklaştık. Ha? Django, select_relatedbunu çözmek içindir - aslında, belgeleri örneğinize benzer bir örnekle başlar p.car.colour.
Adrian17

8
Bu eski bir anwswer, şimdi select_related()ve Django'da prefetch_related().
Mariusz Jamro

1
Güzel. Ama select_related()ve arkadaş gibi bir katılmanın bariz şekilde yararlı tahminler yapmak gibi görünmüyor LEFT OUTER JOIN. Sorun bir arayüz sorunu değil, ama benim görüşüme göre nesnelerin ve ilişkisel verilerin eşlenebilir .... olduğu garip fikri ile ilgili bir sorun.
rorycl

26

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 :

<code> post </code> ve <code> post_comments </code> tabloları

Aşağıdaki 4 postsatı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_commentsBu 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 titleher 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 postve post_commentstablolarını aşağıdaki varlıklarla eşleştirdiğimizi düşünün :

<code> Post </code> ve <code> PostComment </code> varlıkları

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.EAGERJPA 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.EAGERstrateji N + 1 sorgu sorunlarına da yatkındır.

Ne yazık ki, @ManyToOneve @OneToOneilişkilendirmeler FetchType.EAGERvarsayılan olarak kullanılır, bu nedenle eşlemeleriniz şöyle görünürse:

@ManyToOne
private Post post;

FetchType.EAGERStratejiyi kullanıyorsunuz ve JPQL veya Criteria API sorgusuyla JOIN FETCHbazı PostCommentvarlı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 postdernek öncesinde geri dönen getirilen olmak zorunda ListarasındaPostComment kuruluşlar.

findYö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.EAGERyoksa, kullanırken şansınız kalmaz çünkü getirmekten kaçınmanın bir yolu yoktur. Bu yüzden FetchType.LAZYvarsayılan olarak kullanmak daha iyidir .

Ancak, postiliş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.EAGERGetirme stratejisinden neden kaçınmanız gerektiği hakkında daha fazla bilgi için bu makaleye de göz atın.

FetchType.LAZY

FetchType.LAZYTüm ilişkilendirmeler için açıkça kullanmaya geçseniz bile , N + 1 sorunuyla karşılaşabilirsiniz.

Bu kez, postilişkilendirme şu şekilde eşlenir:

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

Şimdi, PostCommentvarlı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 postiliş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 FETCHJPQL 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.LAZYyönlü bir @OneToOneJPA 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-utilaçı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, SQLStatementCountValidatoroluş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.EAGERYukarı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-utilAçık kaynaklı proje hakkında daha fazla bilgi için bu makaleye göz atın .


Ama şimdi sayfalandırma ile ilgili bir sorununuz var. 10 arabanız varsa, her biri 4 tekerlekli araba ve sayfa başına 5 araba ile araba sayfalamak istiyorsunuz. Yani temeldeSELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5 . Ama aldığınız 5 tekerlekli 2 araba (4 tekerlekli tüm araba ve sadece 1 tekerlekli ikinci araba), çünkü LIMIT sadece sonuç cümlesini değil, tüm sonuç kümesini sınırlayacaktır.
CappY

2
Bunun için bir de makalem var.
Vlad Mihalcea

Yazı için teşekkürler. Onu okuyacağım. Hızlı kaydırma ile - çözümün Pencere İşlevi olduğunu gördüm, ancak MariaDB'de oldukça yeniler - bu yüzden sorun eski sürümlerde devam ediyor. :)
CappY

@VladMihalcea, N + 1 problemini açıklarken ManyToOne davasına her başvuruda bulunduğunuzda ya makalenizden ya da gönderiden bahsetmiştim. Ama aslında N + 1 sorunu ile ilgili olarak çoğunlukla OneToMany davasıyla ilgilenen insanlar. Lütfen OneToMany vakasına başvurabilir ve açıklayabilir misiniz?
JJ Beam

18

ŞİRKET ve İŞVEREN olduğunu varsayalım. COMPANY'de birçok EMPLOYEES var (örn. EMPLOYEE şirketinin COMPANY_ID alanı var).

Bazı O / R yapılandırmalarında, eşlenmiş bir Şirket nesnesine sahip olduğunuzda ve onun Çalışan nesnelerine eriştiğinizde, O / R aracı her çalışan için bir seçim yapar, yalnızca düz SQL'de bir şeyler yapıyorsanız, select * from employees where company_id = XX ; Böylece N (çalışan sayısı) artı 1 (şirket)

EJB Entity Beans'ın ilk sürümleri böyle çalıştı. Hibernate gibi şeylerin bunu başardığına inanıyorum, ama çok emin değilim. Çoğu araç genellikle harita stratejileri hakkında bilgi içerir.


18

İşte sorunun iyi bir açıklaması

Sorunu anladığınıza göre, genellikle sorgunuzda birleştirme getirme işlemi ile önlenebilir. Bu temelde, tembel yüklü nesnenin getirilmesini zorlar, böylece veriler n + 1 sorguları yerine bir sorguda alınır. Bu yardımcı olur umarım.


17

Konuyla ilgili Ayende yazısını kontrol edin: NHibernate'de N + 1 Seçimi Sorunu ile Mücadele .

Temel olarak, NHibernate veya EntityFramework gibi bir ORM kullanırken, bir-çok (ana-detay) ilişkiniz varsa ve her bir ana kayıt başına tüm ayrıntıları listelemek istiyorsanız, N + 1 sorgu çağrıları yapmanız gerekir. veritabanı, "N" ana kayıtların sayısıdır: tüm ana kayıtları almak için 1 sorgu ve ana kayıt başına tüm ayrıntıları almak için ana kayıt başına bir adet N sorgusu.

Daha fazla veritabanı sorgu çağrısı → daha fazla gecikme süresi → daha az uygulama / veritabanı performansı.

Bununla birlikte, ORM'lerin, çoğunlukla JOIN'leri kullanarak bu sorunu önleme seçenekleri vardır.


3
eklemler iyi bir çözüm değildir (çoğu zaman), çünkü kartezyen bir ürünle sonuçlanabilir, yani sonuç satırlarının sayısı, her bir alt tablodaki sonuç sayısıyla çarpılan kök tablo sonuçlarının sayısıdır. özellikle birden fazla herarşi seviyesinde kötüdür. Her birinde 100 "yayın" ve "her yayın" için 10 "yorum" içeren 20 "blog" seçildiğinde, 20000 sonuç satırı elde edilir. NHibernate, "toplu boyut" (üst kimliklerde yan tümcesi bulunan çocukları seçin) veya "alt seçici" gibi geçici çözümlere sahiptir.
Erik Hart

14

100 sonuç döndüren 1 sorguyu yayınlamak, her biri 1 sonuç döndüren 100 sorgu yayınlamaktan çok daha hızlıdır.


13

Kanımca Hibernate Pitfall'da yazılan makale : İlişkiler Neden Tembel Olmalı? Gerçek N + 1 sorununun tam tersidir.

Doğru açıklamaya ihtiyacınız varsa lütfen Hazırda Bekletme - Bölüm 19: Performansı Artırma - Stratejileri Getirme konusuna bakın.

Seçme getirme (varsayılan), N + 1'in seçtiği sorunlara son derece açıktır, bu nedenle birleştirme getirmeyi etkinleştirmek isteyebiliriz


2
Hazırda bekletme sayfasını okudum. Bu neyi söylemez N + 1 seçer sorunu aslında olduğunu . Ama düzeltmek için birleşimleri kullanabileceğinizi söylüyor.
Ian Boyd

3
bir seçme deyiminde birden çok ebeveyn için alt nesneleri seçmek üzere, seçme getirme için toplu boyut gereklidir. Alt seçim başka bir alternatif olabilir. Birden çok hiyerarşi seviyeniz varsa ve kartezyen bir ürün oluşturulduysa, birleştirmeler gerçekten kötüleşebilir.
Erik Hart

10

Sağlanan bağlantı n + 1 sorununun çok basit bir örneğine sahiptir. Hazırda Bekletme'ye uygularsanız, temel olarak aynı şeyden bahseder. Bir nesneyi sorgularken varlık yüklenir, ancak tüm ilişkilendirmeler (aksi belirtilmedikçe) tembel olarak yüklenir. Bu nedenle, kök nesneler için bir sorgu ve bunların her biri için ilişkilendirmeleri yüklemek için başka bir sorgu. Döndürülen 100 nesne, bir ilk sorgu ve ardından her biri için ilişkilendirmeyi almak için 100 ek sorgu anlamına gelir, n + 1.

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


9

Bir milyonerin N arabası var. Tüm (4) tekerlekleri almak istiyorsunuz.

Bir (1) sorgu tüm arabaları yükler, ancak her (N) araba için tekerleklerin yüklenmesi için ayrı bir sorgu gönderilir.

Maliyetler:

Dizinlerin koç içine sığdığını varsayın.

1 + N sorgu ayrıştırma ve planlama + dizin arama VE yükleme yükü için 1 + N + (N * 4) plaka erişimi.

Dizinlerin ram ile uyumlu olmadığını varsayın.

Yükleme endeksi için en kötü durumda ek maliyetler 1 + N plaka erişim.

özet

Şişe boynu plaka erişimidir (hdd'de yaklaşık 70 kez rasgele erişim) İstekli bir birleştirme seçimi de yük için plakaya 1 + N + (N * 4) kez erişir. Yani endeksler koç içine sığarsa - sorun yok, çünkü sadece koç operasyonları yeterli.


9

N + 1 seçme sorunu bir acıdır ve birim testlerde bu tür vakaları tespit etmek mantıklıdır. Belirli bir test yöntemi veya yalnızca rastgele bir kod bloğu tarafından yürütülen sorgu sayısını doğrulamak için küçük bir kütüphane geliştirdim - JDBC Sniffer

Test sınıfınıza özel bir JUnit kuralı eklemeniz ve test yöntemlerinize beklenen sayıda sorgu ek açıklaması koymanız yeterlidir:

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

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

5

Diğerlerinin daha zarif bir şekilde ifade ettiği gibi, OneToMany sütunlarının Kartezyen ürününe sahip olmanız veya N + 1 Seçimleri gerçekleştirmenizdir. Ya olası devasa sonuç kümesi ya da veritabanı ile sohbet.

Bundan bahsetmediğim için şaşırdım ama bu sorunu bu şekilde ele aldım ... Yarı geçici kimlikler tablosu yapıyorum . Bunu, IN ()yan tümce sınırlamanız olduğunda da yaparım .

Bu, tüm durumlar için işe yaramıyor (muhtemelen çoğunluk bile değil), ancak Kartezyen ürünün kontrolden çıkması için çok sayıda alt nesneniz varsa (yani çok sayıda OneToManysütun, sonuçların bir sütunların çarpımı) ve daha çok toplu iş gibi.

İlk önce üst nesne kimliklerinizi bir ids tablosuna toplu olarak eklersiniz. Bu batch_id bizim app oluşturmak ve tutunmak bir şeydir.

INSERT INTO temp_ids 
    (product_id, batch_id)
    (SELECT p.product_id, ? 
    FROM product p ORDER BY p.product_id
    LIMIT ? OFFSET ?);

Şimdi her OneToManysütun için alt tablodaki SELECTkimlik tablosunda INNER JOINbir a WHERE batch_id=(veya tersi) yapın. Kimlik sütununa göre sipariş verdiğinizden emin olmak istersiniz, çünkü sonuç sütunlarını birleştirmeyi kolaylaştırır (aksi takdirde sonuç kümesinin tamamı için o kadar da kötü olmayabilecek bir HashMap / Tabloya ihtiyacınız olacaktır).

Sonra sadece periyodik olarak ids tablosunu temizleyin.

Bu, özellikle kullanıcı bir tür toplu işlem için 100 kadar farklı öğe seçerse de iyi çalışır. 100 farklı kimliği geçici tabloya yerleştirin.

Şimdi yaptığınız sorgu sayısı OneToMany sütunlarının sayısına göre.


1

Matt Solnit örneğini ele alalım, Araba ve Tekerlekler arasında LAZY olarak bir ilişki tanımladığınızı ve bazı Tekerlekler alanlarına ihtiyacınız olduğunu hayal edin. Bu, ilk seçimden sonra, hazırda bekletme modunun HER BİR ARAÇ İÇİN car_id =: id "Tekerleklerinden" Seç * yapacağını gösterir.

Bu, her N aracı tarafından ilk seçimi ve daha fazlasını 1 seçim yapar, bu yüzden n + 1 sorunu denir.

Bunu önlemek için, hazırda bekletme modunun bir birleştirmeyle veri yüklemesi için ilişkilendirmeyi istekli hale getirin.

Ancak, ilgili Tekerleklere birçok kez erişmezseniz, onu LAZY olarak tutmak veya getirme türünü Kriterler ile değiştirmek daha iyidir.


1
Yine, özellikle 2'den fazla hiyerarşi seviyesi yüklenebildiğinde, birleşimler iyi bir çözüm değildir. Bunun yerine "alt seçimi" veya "toplu boyutu" kontrol edin; sonuncusu, çocukları "in" yan tümcesinde, örneğin "car_id'in (1,3,4,6,7,8,11,13)" içindeki tekerleklerden "select ..." gibi ana kimliklerine göre yükler.
Erik Hart
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.