"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?
orm
select-n-plus-1
Lars A. Brekken
sumber
sumber
Jawaban:
Katakanlah Anda memiliki koleksi
Car
objek (baris database), dan masing-masingCar
memiliki koleksiWheel
objek (juga baris). Dengan kata lain,Car
→Wheel
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:
Dan kemudian untuk masing-masing
Car
: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:
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.
sumber
SELECT * from Wheel;
), bukan N +1. Dengan N besar, hit kinerja bisa sangat signifikan.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.
N + 1 adalah tempat kueri pertama mengisi objek utama dan kueri kedua mengisi semua objek anak untuk setiap objek primer unik yang dikembalikan.
Mempertimbangkan:
dan tabel dengan struktur yang serupa. Permintaan tunggal untuk alamat "22 Valley St" dapat kembali:
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:
dengan permintaan terpisah seperti
dan menghasilkan seperangkat data terpisah seperti
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.
sumber
Pemasok dengan hubungan satu-ke-banyak dengan Produk. Satu Pemasok memiliki (persediaan) banyak Produk.
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)
Hasil:
Ini adalah N + 1 masalah pilih!
sumber
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.
sumber
JOIN
algoritma 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.Kami pindah dari ORM di Django karena masalah ini. Pada dasarnya, jika Anda mencoba dan melakukannya
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
Ini akan mengembalikan sesuatu seperti
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
Lihat halaman web ini untuk implementasi fanfolding untuk python.
sumber
select_related
, yang dimaksudkan untuk menyelesaikan ini - pada kenyataannya, dokumennya dimulai dengan contoh yang mirip denganp.car.colour
contoh Anda .select_related()
danprefetch_related()
di Django sekarang.select_related()
dan teman sepertinya tidak melakukan ekstrapolasi yang jelas berguna dari bergabung sepertiLEFT OUTER JOIN
. Masalahnya bukan masalah antarmuka, tetapi masalah yang berkaitan dengan gagasan aneh bahwa objek dan data relasional dapat dipetakan .... dalam pandangan saya.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 :
Kami akan membuat 4
post
baris berikut :Dan, kami juga akan membuat 4
post_comment
catatan anak:N + 1 masalah permintaan dengan SQL biasa
Jika Anda memilih
post_comments
menggunakan kueri SQL ini:Dan, nanti, Anda memutuskan untuk mengambil yang terkait
post
title
untuk masing-masingpost_comment
:Anda akan memicu masalah permintaan N + 1 karena, alih-alih satu permintaan SQL, Anda mengeksekusi 5 (1 + 4):
Memperbaiki masalah permintaan N + 1 sangat mudah. Yang perlu Anda lakukan adalah mengekstrak semua data yang Anda butuhkan dalam query SQL asli, seperti ini:
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
post
danpost_comments
tabel ke entitas berikut:Pemetaan JPA terlihat seperti ini:
FetchType.EAGER
Menggunakan
FetchType.EAGER
baik 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.EAGER
strategi ini juga rentan terhadap masalah permintaan N +1.Sayangnya,
@ManyToOne
dan@OneToOne
asosiasi digunakanFetchType.EAGER
secara default, jadi jika pemetaan Anda terlihat seperti ini:Anda menggunakan
FetchType.EAGER
strategi, dan, setiap kali Anda lupa menggunakanJOIN FETCH
ketika memuat beberapaPostComment
entitas dengan permintaan JPQL atau API Kriteria:Anda akan memicu masalah kueri N +1:
Perhatikan pernyataan SELECT tambahan yang dieksekusi karena
post
asosiasi harus diambil sebelum mengembalikanList
dariPostComment
entitas.Tidak seperti rencana pengambilan default, yang Anda gunakan saat memanggil
find
metodeEnrityManager
, 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
post
asosiasi sama sekali, Anda kurang beruntung ketika menggunakanFetchType.EAGER
karena tidak ada cara untuk menghindari mengambilnya. Itu sebabnya lebih baik digunakanFetchType.LAZY
secara default.Tetapi, jika Anda ingin menggunakan
post
asosiasi, maka Anda dapat menggunakanJOIN FETCH
untuk merinci masalah permintaan N + 1:Kali ini, Hibernate akan menjalankan satu pernyataan SQL:
FetchType.LAZY
Bahkan jika Anda beralih menggunakan
FetchType.LAZY
secara eksplisit untuk semua asosiasi, Anda masih bisa menabrak masalah N +1.Kali ini,
post
asosiasi dipetakan seperti ini:Sekarang, ketika Anda mengambil
PostComment
entitas:Hibernate akan menjalankan pernyataan SQL tunggal:
Tetapi, jika sesudahnya, Anda akan merujuk
post
asosiasi yang malas :Anda akan mendapatkan masalah permintaan N + 1:
Karena
post
asosiasi diambil malas, pernyataan SQL sekunder akan dieksekusi ketika mengakses asosiasi malas untuk membangun pesan log.Sekali lagi, perbaikan terdiri dalam menambahkan
JOIN FETCH
klausa ke permintaan JPQL:Dan, seperti dalam
FetchType.EAGER
contoh ini, permintaan JPQL ini akan menghasilkan pernyataan SQL tunggal.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-util
open-source.Pertama, Anda perlu menambahkan ketergantungan Maven berikut:
Setelah itu, Anda hanya perlu menggunakan
SQLStatementCountValidator
utilitas untuk menegaskan pernyataan SQL yang mendasari yang dihasilkan:Jika Anda menggunakan
FetchType.EAGER
dan menjalankan test case di atas, Anda akan mendapatkan kegagalan test case berikut:sumber
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.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.
sumber
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.
sumber
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.
sumber
Jauh lebih cepat untuk mengeluarkan 1 kueri yang mengembalikan 100 hasil daripada mengeluarkan 100 kueri yang masing-masing mengembalikan 1 hasil.
sumber
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
sumber
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
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.
sumber
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:
sumber
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.
Sekarang untuk setiap
OneToMany
kolom Anda hanya perlu melakukanSELECT
pada tabel id tabelINNER JOIN
anak 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.
sumber
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.
sumber