Apa "N + 1 memilih masalah" di ORM (Object-Relational Mapping)?

1598

"N + 1 memilih masalah" umumnya dinyatakan sebagai masalah dalam diskusi Object-Relational mapping (ORM), dan saya mengerti bahwa itu ada hubungannya dengan keharusan membuat banyak permintaan basis data untuk sesuatu yang tampaknya sederhana di objek. dunia.

Adakah yang punya penjelasan lebih rinci tentang masalah ini?

Lars A. Brekken
sumber
2
Ini adalah tautan yang bagus dengan penjelasan yang bagus untuk memahami masalah n +1 . Ini juga mencakup solusi untuk mengatasi masalah ini: architects.dzone.com/articles/how-identify-and-resilve-n1
ace.
Ada beberapa posting bermanfaat yang membicarakan masalah ini dan kemungkinan perbaikannya. Masalah Aplikasi Umum dan Cara Memperbaiki Mereka: Masalah Select N + 1 , The (Silver) Bullet untuk Masalah N + 1 , Pemuatan Malas - pemuatan yang bersemangat
cateyes
Untuk semua orang yang mencari solusi untuk masalah ini, saya menemukan posting yang menjelaskannya. stackoverflow.com/questions/32453989/…
damndemon
2
Mempertimbangkan jawaban, bukankah ini harus disebut sebagai masalah 1 + N? Karena ini sepertinya terminologi, saya tidak, secara khusus, menanyakan OP.
user1418717

Jawaban:

1018

Katakanlah Anda memiliki koleksi Carobjek (baris database), dan masing-masing Carmemiliki koleksi Wheelobjek (juga baris). Dengan kata lain, CarWheel adalah hubungan 1-ke-banyak.

Sekarang, katakanlah Anda perlu beralih ke semua mobil, dan untuk masing-masing, cetak daftar roda. Implementasi O / R yang naif akan melakukan hal berikut:

SELECT * FROM Cars;

Dan kemudian untuk masing-masing Car:

SELECT * FROM Wheel WHERE CarId = ?

Dengan kata lain, Anda memiliki satu pilih untuk Mobil, dan kemudian N tambahan memilih, di mana N adalah jumlah total mobil.

Atau, orang bisa mendapatkan semua roda dan melakukan pencarian di memori:

SELECT * FROM Wheel

Ini mengurangi jumlah bolak-balik ke database dari N +1 ke 2. Sebagian besar alat ORM memberi Anda beberapa cara untuk mencegah N + 1 memilih.

Referensi: Java Persistence with Hibernate , bab 13.

Matt Solnit
sumber
140
Untuk mengklarifikasi "Ini buruk" - Anda bisa mendapatkan semua roda dengan 1 pilih ( SELECT * from Wheel;), bukan N +1. Dengan N besar, hit kinerja bisa sangat signifikan.
tucuxi
212
@tucuxi Saya terkejut Anda mendapat begitu banyak kesalahan karena salah. Basis data sangat baik tentang indeks, melakukan kueri untuk CarID tertentu akan kembali sangat cepat. Tetapi jika Anda mendapatkan semua Roda sekali, Anda harus mencari CarID di aplikasi Anda, yang tidak diindeks, ini lebih lambat. Kecuali jika Anda memiliki masalah latensi besar yang mencapai basis data Anda menjadi n +1 sebenarnya lebih cepat - dan ya, saya membandingkannya dengan beragam kode dunia nyata.
Ariel
74
@ariel Cara 'benar' adalah untuk mendapatkan semua roda, dipesan oleh CarId (1 pilih), dan jika lebih banyak rincian dari CarId diperlukan, buat kueri kedua untuk semua mobil (total 2 kueri). Mencetak semuanya sekarang optimal, dan tidak diperlukan indeks atau penyimpanan sekunder (Anda dapat mengulangi hasilnya, tidak perlu mengunduh semuanya). Anda membuat tolok ukur hal yang salah. Jika Anda masih yakin dengan tolok ukur Anda, apakah Anda keberatan memposting komentar yang lebih panjang (atau jawaban lengkap) yang menjelaskan eksperimen dan hasil Anda?
tucuxi
92
"Hibernate (saya tidak terbiasa dengan kerangka kerja ORM lainnya) memberi Anda beberapa cara untuk menanganinya." dan begini?
Tima
58
@Ririel Coba jalankan benchmark Anda dengan database dan server aplikasi pada mesin yang terpisah. Dalam pengalaman saya, pulang pergi ke database lebih mahal di biaya daripada permintaan itu sendiri. Jadi ya, pertanyaannya sangat cepat, tapi itu adalah perjalanan pulang-pergi yang menimbulkan kekacauan. Saya telah mengonversi "WHERE Id = const " menjadi "WHERE Id IN ( const , const , ...)" dan mendapatkan pesanan sebesar meningkat dari itu.
Hans
110
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Itu membuat Anda menetapkan hasil di mana baris anak di table2 menyebabkan duplikasi dengan mengembalikan hasil table1 untuk setiap baris anak di table2. Pemetaan O / R harus membedakan instance table1 berdasarkan pada bidang kunci yang unik, kemudian menggunakan semua kolom table2 untuk mengisi instance anak.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N + 1 adalah tempat kueri pertama mengisi objek utama dan kueri kedua mengisi semua objek anak untuk setiap objek primer unik yang dikembalikan.

Mempertimbangkan:

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

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

dan tabel dengan struktur yang serupa. Permintaan tunggal untuk alamat "22 Valley St" dapat kembali:

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

O / RM harus mengisi instance Home dengan ID = 1, Address = "22 Valley St" dan kemudian mengisi array Penduduk dengan instance People untuk Dave, John, dan Mike dengan hanya satu permintaan.

Kueri N + 1 untuk alamat yang sama dengan yang digunakan di atas akan menghasilkan:

Id Address
1  22 Valley St

dengan permintaan terpisah seperti

SELECT * FROM Person WHERE HouseId = 1

dan menghasilkan seperangkat data terpisah seperti

Name    HouseId
Dave    1
John    1
Mike    1

dan hasil akhirnya sama seperti di atas dengan permintaan tunggal.

Keuntungan memilih tunggal adalah Anda mendapatkan semua data di muka yang mungkin Anda inginkan. Keuntungan N + 1 adalah kompleksitas kueri berkurang dan Anda dapat menggunakan lazy loading di mana set hasil anak hanya dimuat berdasarkan permintaan pertama.

cfeduke
sumber
4
Keuntungan lain dari n +1 adalah lebih cepat karena database dapat mengembalikan hasil langsung dari indeks. Melakukan penggabungan dan kemudian menyortir membutuhkan tabel temp, yang lebih lambat. Satu-satunya alasan untuk menghindari n +1 adalah jika Anda memiliki banyak latensi yang berbicara dengan basis data Anda.
Ariel
17
Bergabung dan menyortir bisa sangat cepat (karena Anda akan bergabung di bidang yang diindeks dan mungkin diurutkan). Seberapa besar 'n +1' Anda? Apakah Anda serius percaya bahwa masalah n +1 hanya berlaku untuk koneksi database latensi tinggi?
tucuxi
9
@ariel - Saran Anda bahwa N +1 adalah "tercepat" salah, meskipun tolok ukur Anda mungkin benar. Bagaimana mungkin? Lihat en.wikipedia.org/wiki/Anecdotal_evidence , dan juga komentar saya di jawaban lain untuk pertanyaan ini.
whitneyland
7
@Riel - Saya pikir saya mengerti dengan baik :). Saya hanya mencoba menunjukkan bahwa hasil Anda hanya berlaku untuk satu set kondisi. Saya bisa dengan mudah membuat contoh balasan yang menunjukkan sebaliknya. Apakah itu masuk akal?
whitneyland
13
Untuk mengulangi, masalah SELECT N + 1 adalah, pada intinya: Saya memiliki 600 catatan untuk diambil. Apakah lebih cepat mendapatkan 600 semuanya dalam satu kueri, atau 1 sekaligus dalam 600 kueri. Kecuali jika Anda menggunakan MyISAM dan / atau Anda memiliki skema yang dinormalkan / diindeks dengan buruk (dalam hal ini ORM bukan masalahnya), db yang disetel dengan benar akan mengembalikan 600 baris dalam 2 ms, sementara mengembalikan masing-masing baris dalam masing-masing sekitar 1 ms. Jadi kita sering melihat N +1 mengambil ratusan milidetik di mana bergabung hanya membutuhkan pasangan
Anjing
64

Pemasok dengan hubungan satu-ke-banyak dengan Produk. Satu Pemasok memiliki (persediaan) banyak Produk.

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

Faktor:

  • Mode malas untuk Pemasok diatur ke "true" (default)

  • Mode pengambilan yang digunakan untuk kueri pada Produk adalah Pilih

  • Mode pengambilan (default): Informasi pemasok diakses

  • Caching tidak memainkan peran untuk pertama kalinya

  • Pemasok diakses

Mode pengambilan adalah Pilih Ambil (default)

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

Hasil:

  • 1 pernyataan pilihan untuk Produk
  • N pilih pernyataan untuk Pemasok

Ini adalah N + 1 masalah pilih!

Summy
sumber
3
Apakah seharusnya 1 pilih untuk Pemasok kemudian N memilih untuk Produk?
bencampbell_14
@ bencampbell_ Ya, awalnya saya merasakan hal yang sama. Tetapi kemudian dengan teladannya, itu adalah Satu produk bagi banyak pemasok.
Mohd Faizan Khan
38

Saya tidak dapat mengomentari jawaban lain secara langsung, karena saya tidak memiliki reputasi yang cukup. Tetapi perlu dicatat bahwa masalah tersebut pada dasarnya hanya muncul karena, secara historis, banyak dbms sangat buruk dalam menangani penggabungan (MySQL menjadi contoh yang sangat penting). Jadi n +1 sering, lebih cepat dari gabungan. Dan kemudian ada cara untuk meningkatkan pada n +1 tetapi masih tanpa perlu bergabung, itulah yang terkait dengan masalah aslinya.

Namun, MySQL sekarang jauh lebih baik daripada biasanya ketika datang untuk bergabung. Ketika saya pertama kali belajar MySQL, saya sering menggunakan gabungan. Lalu saya menemukan betapa lambatnya mereka, dan beralih ke n +1 dalam kode sebagai gantinya. Tapi, baru-baru ini, saya telah pindah kembali ke bergabung, karena MySQL sekarang jauh lebih baik dalam menangani mereka daripada ketika saya pertama kali mulai menggunakannya.

Hari-hari ini, gabungan sederhana pada set tabel yang diindeks dengan benar jarang menjadi masalah, dalam hal kinerja. Dan jika memang memberikan hit kinerja, maka penggunaan petunjuk indeks sering memecahkannya.

Ini dibahas di sini oleh salah satu tim pengembangan MySQL:

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

Jadi rangkumannya adalah: Jika Anda telah menghindari bergabung di masa lalu karena kinerja buruk MySQL dengan mereka, maka coba lagi pada versi terbaru. Anda mungkin akan terkejut.

Mark Goodge
sumber
7
Memanggil versi awal MySQL sebagai DBMS relasional cukup berat ... Jika orang yang menemui masalah itu menggunakan database nyata, mereka tidak akan menemui masalah seperti itu. ;-)
Craig
2
Menariknya, banyak dari jenis masalah ini diselesaikan di MySQL dengan pengenalan dan optimalisasi mesin INNODB berikutnya, tetapi Anda masih akan bertemu dengan orang yang mencoba mempromosikan MYISAM karena mereka pikir lebih cepat.
Craig
5
FYI, salah satu dari 3 JOINalgoritma umum yang digunakan dalam RDBMS 'disebut loop bersarang. Ini pada dasarnya adalah pilihan N +1 di bawah tenda. Satu-satunya perbedaan adalah DB membuat pilihan cerdas untuk menggunakannya berdasarkan statistik dan indeks, daripada kode klien yang memaksanya menuruni jalur itu secara kategoris.
Brandon
2
@ Merkon Ya! Sama seperti petunjuk JOIN dan petunjuk INDEX, memaksa jalur eksekusi tertentu dalam semua kasus jarang mengalahkan basis data. Basis data hampir selalu sangat baik dalam memilih pendekatan optimal untuk mendapatkan data. Mungkin pada hari-hari awal dbs Anda perlu 'frase' pertanyaan Anda dengan cara yang aneh untuk membujuk db, tetapi setelah puluhan tahun rekayasa kelas dunia, Anda sekarang bisa mendapatkan kinerja terbaik dengan mengajukan database Anda pertanyaan relasional dan membiarkannya memilah cara mengambil dan mengumpulkan data untuk Anda.
Anjing
3
Basis data tidak hanya menggunakan indeks dan statistik, semua operasi juga I / O lokal, banyak yang sering beroperasi melawan cache yang sangat efisien daripada disk. Pemrogram basis data mencurahkan banyak perhatian untuk mengoptimalkan hal-hal ini.
Craig
27

Kami pindah dari ORM di Django karena masalah ini. Pada dasarnya, jika Anda mencoba dan melakukannya

for p in person:
    print p.car.colour

ORM akan dengan senang hati mengembalikan semua orang (biasanya sebagai instance dari objek Person), tetapi kemudian perlu meminta tabel mobil untuk setiap Person.

Pendekatan sederhana dan sangat efektif untuk ini adalah sesuatu yang saya sebut " fanfolding ", yang menghindari ide tidak masuk akal bahwa hasil query dari database relasional harus dipetakan kembali ke tabel asli dari mana kueri disusun.

Langkah 1: Pilih secara luas

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

Ini akan mengembalikan sesuatu seperti

  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

Langkah 2: Tujuan

Sedot hasilnya menjadi pembuat objek generik dengan argumen untuk dipecah setelah item ketiga. Ini berarti bahwa objek "jones" tidak akan dibuat lebih dari sekali.

Langkah 3: Render

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

Lihat halaman web ini untuk implementasi fanfolding untuk python.

rorycl
sumber
10
Saya sangat senang saya tersandung pada posting Anda, karena saya pikir saya akan menjadi gila. ketika saya mengetahui tentang masalah N +1, pemikiran langsung saya adalah- yah, mengapa Anda tidak hanya membuat tampilan yang berisi semua informasi yang Anda butuhkan, dan menarik dari pandangan itu? Anda telah memvalidasi posisi saya. Terima kasih Pak.
pengembang
14
Kami pindah dari ORM di Django karena masalah ini. Hah? Django memiliki select_related, yang dimaksudkan untuk menyelesaikan ini - pada kenyataannya, dokumennya dimulai dengan contoh yang mirip dengan p.car.colourcontoh Anda .
Adrian17
8
Ini adalah jawaban lama, kita miliki select_related()dan prefetch_related()di Django sekarang.
Mariusz Jamro
1
Keren. Tapi select_related()dan teman sepertinya tidak melakukan ekstrapolasi yang jelas berguna dari bergabung seperti LEFT OUTER JOIN. Masalahnya bukan masalah antarmuka, tetapi masalah yang berkaitan dengan gagasan aneh bahwa objek dan data relasional dapat dipetakan .... dalam pandangan saya.
rorycl
26

Karena ini adalah pertanyaan yang sangat umum, saya menulis artikel ini , yang menjadi dasar jawaban ini.

Apa masalah permintaan N + 1

Masalah permintaan N + 1 terjadi ketika kerangka kerja akses data dieksekusi N pernyataan SQL tambahan untuk mengambil data yang sama yang bisa diambil ketika mengeksekusi query SQL primer.

Semakin besar nilai N, semakin banyak permintaan akan dieksekusi, semakin besar dampak kinerja. Dan, tidak seperti log kueri lambat yang dapat membantu Anda menemukan kueri berjalan lambat, masalah N +1 tidak akan spot karena setiap kueri tambahan individu berjalan cukup cepat untuk tidak memicu log kueri lambat.

Masalahnya adalah mengeksekusi sejumlah besar pertanyaan tambahan yang, secara keseluruhan, membutuhkan waktu yang cukup untuk memperlambat waktu respons.

Mari kita pertimbangkan kita memiliki tabel database post dan post_comments berikut yang membentuk hubungan tabel satu-ke-banyak :

Tabel <code> post </code> dan <code> post_comments </code>

Kami akan membuat 4 postbaris berikut :

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)

Dan, kami juga akan membuat 4 post_commentcatatan anak:

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)

N + 1 masalah permintaan dengan SQL biasa

Jika Anda memilih post_commentsmenggunakan kueri SQL ini:

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();

Dan, nanti, Anda memutuskan untuk mengambil yang terkait post titleuntuk masing-masing 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
    );
}

Anda akan memicu masalah permintaan N + 1 karena, alih-alih satu permintaan SQL, Anda mengeksekusi 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'

Memperbaiki masalah permintaan N + 1 sangat mudah. Yang perlu Anda lakukan adalah mengekstrak semua data yang Anda butuhkan dalam query SQL asli, seperti ini:

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
    );
}

Kali ini, hanya satu query SQL yang dijalankan untuk mengambil semua data yang kami tertarik untuk menggunakannya.

Masalah permintaan N + 1 dengan JPA dan Hibernate

Saat menggunakan JPA dan Hibernate, ada beberapa cara Anda dapat memicu masalah permintaan N + 1, jadi sangat penting untuk mengetahui bagaimana Anda bisa menghindari situasi ini.

Untuk contoh berikutnya, pertimbangkan kami memetakan postdan post_commentstabel ke entitas berikut:

entitas <code> Posting </code> dan <code> PostComment </code>

Pemetaan JPA terlihat seperti ini:

@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

Menggunakan FetchType.EAGERbaik secara implisit atau eksplisit untuk asosiasi JPA Anda adalah ide yang buruk karena Anda akan mengambil lebih banyak data yang Anda butuhkan. Selain itu, FetchType.EAGERstrategi ini juga rentan terhadap masalah permintaan N +1.

Sayangnya, @ManyToOnedan @OneToOneasosiasi digunakan FetchType.EAGERsecara default, jadi jika pemetaan Anda terlihat seperti ini:

@ManyToOne
private Post post;

Anda menggunakan FetchType.EAGERstrategi, dan, setiap kali Anda lupa menggunakan JOIN FETCHketika memuat beberapa PostCommententitas dengan permintaan JPQL atau API Kriteria:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Anda akan memicu masalah kueri 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

Perhatikan pernyataan SELECT tambahan yang dieksekusi karena postasosiasi harus diambil sebelum mengembalikan ListdariPostComment entitas.

Tidak seperti rencana pengambilan default, yang Anda gunakan saat memanggil findmetode EnrityManager, kueri JPQL atau Kriteria API menentukan rencana eksplisit yang tidak dapat diubah Hibernate dengan menyuntikkan JOIN FETCH secara otomatis. Jadi, Anda perlu melakukannya secara manual.

Jika Anda tidak memerlukan postasosiasi sama sekali, Anda kurang beruntung ketika menggunakan FetchType.EAGERkarena tidak ada cara untuk menghindari mengambilnya. Itu sebabnya lebih baik digunakanFetchType.LAZY secara default.

Tetapi, jika Anda ingin menggunakan postasosiasi, maka Anda dapat menggunakan JOIN FETCHuntuk merinci masalah permintaan 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()
    );
}

Kali ini, Hibernate akan menjalankan satu pernyataan 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'

Untuk detail lebih lanjut tentang mengapa Anda harus menghindari FetchType.EAGERstrategi pengambilan, lihat artikel ini juga.

FetchType.LAZY

Bahkan jika Anda beralih menggunakan FetchType.LAZYsecara eksplisit untuk semua asosiasi, Anda masih bisa menabrak masalah N +1.

Kali ini, postasosiasi dipetakan seperti ini:

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

Sekarang, ketika Anda mengambil PostCommententitas:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Hibernate akan menjalankan pernyataan SQL tunggal:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

Tetapi, jika sesudahnya, Anda akan merujuk postasosiasi yang malas :

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

Anda akan mendapatkan masalah permintaan 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'

Karena postasosiasi diambil malas, pernyataan SQL sekunder akan dieksekusi ketika mengakses asosiasi malas untuk membangun pesan log.

Sekali lagi, perbaikan terdiri dalam menambahkan JOIN FETCHklausa ke permintaan 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()
    );
}

Dan, seperti dalam FetchType.EAGERcontoh ini, permintaan JPQL ini akan menghasilkan pernyataan SQL tunggal.

Bahkan jika Anda menggunakan FetchType.LAZYdan tidak mereferensikan asosiasi anak dari @OneToOnehubungan JPA dua arah , Anda masih dapat memicu masalah kueri N +1.

Untuk detail lebih lanjut tentang bagaimana Anda bisa mengatasi masalah kueri N + 1 yang dihasilkan oleh @OneToOneasosiasi, lihat artikel ini .

Cara mendeteksi secara otomatis masalah kueri N + 1

Jika Anda ingin secara otomatis mendeteksi masalah kueri N + 1 di lapisan akses data Anda, artikel ini menjelaskan bagaimana Anda bisa melakukannya dengan menggunakan proyek db-utilopen-source.

Pertama, Anda perlu menambahkan ketergantungan Maven berikut:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>${db-util.version}</version>
</dependency>

Setelah itu, Anda hanya perlu menggunakan SQLStatementCountValidatorutilitas untuk menegaskan pernyataan SQL yang mendasari yang dihasilkan:

SQLStatementCountValidator.reset();

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

SQLStatementCountValidator.assertSelectCount(1);

Jika Anda menggunakan FetchType.EAGERdan menjalankan test case di atas, Anda akan mendapatkan kegagalan test case berikut:

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!

Untuk detail lebih lanjut tentang proyek db-utilopen-source, lihat artikel ini .

Vlad Mihalcea
sumber
Tetapi sekarang Anda memiliki masalah dengan pagination. Jika Anda memiliki 10 mobil, masing-masing mobil dengan 4 roda dan Anda ingin mobil pagination dengan 5 mobil per halaman. Jadi pada dasarnya Anda punya SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5. Tetapi yang Anda dapatkan adalah 2 mobil dengan 5 roda (mobil pertama dengan semua 4 roda dan mobil kedua dengan hanya 1 roda), karena LIMIT akan membatasi seluruh hasil, bukan hanya klausa root.
CappY
2
Saya punya artikel untuk itu juga.
Vlad Mihalcea
Terima kasih atas artikelnya. Saya akan membacanya. Dengan scroll cepat - saya melihat bahwa solusinya adalah Window Function, tetapi mereka cukup baru di MariaDB - jadi masalahnya tetap ada di versi yang lebih lama. :)
CappY
@VladMihalcea, saya tunjukkan baik dari artikel Anda atau dari pos setiap kali Anda merujuk ke kasus ManyToOne sambil menjelaskan masalah N +1. Namun sebenarnya sebagian besar orang tertarik pada kasus OneToMany yang berkaitan dengan masalah N +1. Bisakah Anda merujuk dan menjelaskan kasus OneToMany?
JJ Beam
18

Misalkan Anda memiliki PERUSAHAAN dan KARYAWAN. PERUSAHAAN memiliki banyak KARYAWAN (yaitu KARYAWAN memiliki bidang COMPANY_ID).

Dalam beberapa konfigurasi O / R, ketika Anda memiliki objek Perusahaan yang dipetakan dan pergi untuk mengakses objek Karyawannya, alat O / R akan melakukan satu pilih untuk setiap karyawan, sedangkan jika Anda hanya melakukan hal-hal dalam SQL lurus, Anda bisa select * from employees where company_id = XX. Jadi N (# karyawan) ditambah 1 (perusahaan)

Ini adalah cara kerja versi awal EJB Entity Beans. Saya percaya hal-hal seperti Hibernate telah menghilangkan ini, tapi saya tidak terlalu yakin. Sebagian besar alat biasanya menyertakan info tentang strategi pemetaan mereka.

davetron5000
sumber
18

Berikut adalah deskripsi masalahnya

Sekarang setelah Anda memahami masalahnya, biasanya hal itu dapat dihindari dengan melakukan pengambilan gabungan dalam kueri Anda. Ini pada dasarnya memaksa pengambilan objek bermuatan malas sehingga data diambil dalam satu kueri alih-alih n + 1 kueri. Semoga ini membantu.

Joe Dean
sumber
17

Periksa pos Ayende pada topik: Memerangi Masalah Select N + 1 Di NHibernate .

Pada dasarnya, ketika menggunakan ORM seperti NHibernate atau EntityFramework, jika Anda memiliki hubungan satu-ke-banyak (detail-master), dan ingin mencantumkan semua detail per setiap catatan master, Anda harus membuat N + 1 permintaan panggilan ke database, "N" menjadi jumlah catatan master: 1 permintaan untuk mendapatkan semua catatan master, dan N kueri, satu per catatan master, untuk mendapatkan semua detail per catatan master.

Lebih banyak panggilan permintaan basis data → lebih banyak waktu latensi → penurunan kinerja aplikasi / basis data.

Namun, ORM memiliki opsi untuk menghindari masalah ini, terutama menggunakan GABUNGAN.

Nathan
sumber
3
bergabung bukanlah solusi yang baik (sering), karena mereka dapat menghasilkan produk kartesius, artinya jumlah baris hasil adalah jumlah hasil tabel akar dikalikan dengan jumlah hasil di setiap tabel anak. sangat buruk pada beberapa level herarki. Memilih 20 "blog" dengan 100 "posting" di masing-masing dan 10 "komentar" pada setiap posting akan menghasilkan 20.000 baris hasil. NHibernate memiliki solusi, seperti "ukuran batch" (pilih anak-anak dengan klausa id orang tua) atau "subselect".
Erik Hart
14

Jauh lebih cepat untuk mengeluarkan 1 kueri yang mengembalikan 100 hasil daripada mengeluarkan 100 kueri yang masing-masing mengembalikan 1 hasil.

jj_
sumber
13

Menurut pendapat saya, artikel yang ditulis dalam Hibernate Pitfall: Mengapa Relationships Should Be Lazy adalah kebalikan dari masalah N + 1 sebenarnya.

Jika Anda membutuhkan penjelasan yang benar, silakan merujuk ke Hibernate - Bab 19: Meningkatkan Kinerja - Strategi Pengambilan

Pilih pengambilan (standar) sangat rentan terhadap N + 1 memilih masalah, jadi kami mungkin ingin mengaktifkan penggabungan pengambilan

Anoop Isaac
sumber
2
saya membaca halaman hibernasi. Ia tidak mengatakan apa N + 1 menyeleksi masalah sebenarnya adalah . Tetapi dikatakan Anda bisa menggunakan gabungan untuk memperbaikinya.
Ian Boyd
3
ukuran batch diperlukan untuk memilih pengambilan, untuk memilih objek anak untuk beberapa orang tua dalam satu pernyataan pilih. Subselect bisa menjadi alternatif lain. Bergabung dapat menjadi sangat buruk jika Anda memiliki beberapa tingkat hierarki dan produk kartesius dibuat.
Erik Hart
10

Tautan yang disediakan memiliki contoh yang sangat sederhana tentang masalah n +1. Jika Anda menerapkannya ke Hibernate pada dasarnya berbicara tentang hal yang sama. Saat Anda meminta objek, entitas dimuat tetapi asosiasi apa pun (kecuali yang dikonfigurasi sebaliknya) akan dimuat dengan malas. Oleh karena itu satu permintaan untuk objek root dan permintaan lainnya untuk memuat asosiasi untuk masing-masing. 100 objek yang dikembalikan berarti satu permintaan awal dan kemudian 100 permintaan tambahan untuk mendapatkan asosiasi untuk masing-masing, n +1.

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


sumber
9

Seorang jutawan memiliki N mobil. Anda ingin mendapatkan semua (4) roda.

Satu (1) kueri memuat semua mobil, tetapi untuk setiap (N) mobil kueri terpisah dikirimkan untuk memuat roda.

Biaya:

Asumsikan indeks masuk ke ram.

1 + N kueri penguraian dan perencanaan + pencarian indeks DAN 1 + N + (N * 4) akses pelat untuk memuat muatan.

Asumsikan indeks tidak cocok dengan ram.

Biaya tambahan dalam kasus terburuk 1 + N plat akses untuk memuat indeks.

Ringkasan

Leher botol adalah akses plat (kira-kira 70 kali per detik akses acak pada hdd) Pilihan join yang bersemangat juga akan mengakses plat 1 + N + (N * 4) kali untuk muatan. Jadi jika indeks cocok dengan ram - tidak ada masalah, cukup cepat karena hanya operasi ram yang terlibat.

hans wurst
sumber
9

Masalah pilihan N +1 adalah menyakitkan, dan masuk akal untuk mendeteksi kasus-kasus tersebut dalam unit test. Saya telah mengembangkan perpustakaan kecil untuk memverifikasi jumlah pertanyaan yang dieksekusi dengan metode pengujian yang diberikan atau hanya blok kode yang sewenang-wenang - JDBC Sniffer

Cukup tambahkan aturan JUnit khusus ke kelas pengujian Anda dan tempatkan anotasi dengan jumlah kueri yang diharapkan pada metode pengujian Anda:

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

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

Masalah yang orang lain nyatakan lebih elegan adalah Anda memiliki produk Cartesian dari kolom OneToMany atau Anda melakukan N + 1 Selects. Entah mungkin hasil raksasa atau mengobrol dengan database, masing-masing.

Saya terkejut ini tidak disebutkan tetapi ini bagaimana saya mengatasi masalah ini ... Saya membuat tabel id semi-sementara . Saya juga melakukan ini ketika Anda memiliki IN ()batasan klausa .

Ini tidak bekerja untuk semua kasus (mungkin bahkan tidak mayoritas) tetapi ini bekerja sangat baik jika Anda memiliki banyak objek anak sehingga produk Cartesian akan lepas kendali (yaitu banyak OneToMany kolom jumlah hasil akan menjadi perkalian kolom) dan lebih dari batch seperti pekerjaan.

Pertama Anda memasukkan id objek orangtua Anda sebagai batch ke dalam tabel id. Batch_id ini adalah sesuatu yang kami hasilkan di aplikasi kami dan terus.

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

Sekarang untuk setiap OneToManykolom Anda hanya perlu melakukan SELECTpada tabel id tabel INNER JOINanak dengan aWHERE batch_id= (atau sebaliknya). Anda hanya ingin memastikan bahwa Anda memesan dengan kolom id karena akan membuat kolom hasil gabungan lebih mudah (jika tidak, Anda akan memerlukan HashMap / Tabel untuk seluruh rangkaian hasil yang mungkin tidak terlalu buruk).

Maka Anda cukup membersihkan tabel id secara berkala.

Ini juga bekerja dengan sangat baik jika pengguna memilih mengatakan 100 atau lebih item yang berbeda untuk beberapa jenis pemrosesan massal. Letakkan 100 id yang berbeda di tabel sementara.

Sekarang jumlah pertanyaan yang Anda lakukan adalah dengan jumlah kolom OneToMany.

Adam Gent
sumber
1

Ambil contoh Matt Solnit, bayangkan Anda mendefinisikan hubungan antara Mobil dan Roda sebagai LAZY dan Anda memerlukan beberapa bidang Roda. Ini berarti bahwa setelah pemilihan pertama, hibernate akan melakukan "Select * from Wheels di mana car_id =: id" FOR EACH Car.

Ini membuat pilih pertama dan lebih banyak 1 pilih oleh setiap mobil N, itu sebabnya ini disebut masalah n +1.

Untuk menghindarinya, buat asosiasi itu sebagai bersemangat, sehingga hibernasi memuat data dengan gabungan.

Tetapi perhatian, jika berkali-kali Anda tidak mengakses Roda terkait, lebih baik tetap LAZY atau ubah jenis pengambilan dengan Kriteria.

martins.tuga
sumber
1
Sekali lagi, bergabung bukanlah solusi yang baik, terutama ketika lebih dari 2 level hierarki dapat dimuat. Sebagai gantinya, pilih "subselect" atau "batch-size"; yang terakhir akan memuat anak-anak dengan ID induk dalam klausa "in", seperti "pilih ... dari roda tempat car_id in (1,3,4,6,7,8,11,13)".
Erik Hart