it-swarm-tr.com

ORM'de (Nesne İlişkisel Haritalama) "N + 1 problemi seçer" nedir?

“N + 1 problemi seçer” genellikle Nesne İlişkisel Haritalama (ORM) tartışmalarında bir problem olarak ifade edilir ve nesnede basit görünen bir şey için çok fazla veritabanı sorgusu yapmak zorunda kaldığı bir şey olduğunu anlıyorum. dünya.

Sorunun daha detaylı bir açıklaması var mı?

1435
Lars A. Brekken

Diyelim ki Car nesneleri (veritabanı satırları) koleksiyonunuz var ve her CarWheel nesneleri (ayrıca satırlar) koleksiyonuna sahip. Başka bir deyişle, Car -> Wheel bir-çok-bir ilişkidir.

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

SELECT * FROM Cars;

Ve sonra her Car için:

SELECT * FROM Wheel WHERE CarId = ?

Başka bir deyişle, Arabalar için bir tane seçtiniz ve ardından N toplam araba sayısı olan N ek seçimi.

Alternatif olarak, bir kişi bütün tekerlekleri alabilir ve aramaları bellekte gerçekleştirebilir:

SELECT * FROM Wheel

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

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

878
Matt Solnit
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Bu, tablo2'deki alt satırların tablo2'deki tablo1 sonuçlarını döndürerek sonuçlanmasını sağlar. O/R eşleyicileri, tablo1 örneklerini benzersiz bir anahtar alana göre ayırmalı ve alt örneklerini doldurmak için tüm tablo2 sütunlarını kullanmalıdır.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N + 1, birinci sorgunun birincil nesneyi doldurduğu ve ikinci sorgunun, 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 yapıya sahip tablolar. "22 Valley St" adresi için tek bir sorgu geri dönebilir:

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 daha sonra Inhabitants dizisini Dave, John ve Mike için People örnekleri ile birlikte tek bir sorgu ile doldurmalıdır.

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

Id Address
1  22 Valley St

ayrı bir sorgu ile

SELECT * FROM Person WHERE HouseId = 1

ve bunun gibi ayrı bir veri setiyle sonuçlanır

Name    HouseId
Dave    1
John    1
Mike    1

ve nihai sonuç, tekli sorgu ile yukarıda olduğu gibidir.

Tek seçimin avantajları, nihayetinde istediğiniz gibi olabilecek tüm verileri önceden elde etmenizdir. N + 1'in avantajları sorgu karmaşıklığı azaltılmış ve alt sonuç kümelerinin yalnızca ilk istek üzerine yüklendiği yerlerde tembel yükleme kullanabilirsiniz.

103
cfeduke

Ürün ile bire çok ilişki kurarak tedarikçi. Bir Tedarikçi, birçok ürüne (sarf malzemesine) sahiptir.

***** 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 modu “true” olarak ayarlandı (varsayılan)

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

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

  • Önbelleğe alma ilk defa bir rol oynamaz.

  • Tedarikçi erişilir

Alma modu, Al'ı seçin (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 seçim ifadesi
  • N Tedarikçi için ifadeler seçin

Bu N + 1 seçim problemi!

60
Summy

Doğrudan diğer cevapları yorumlayamıyorum, çünkü yeterince itibarım yok. Ancak, sorunun esas olarak ortaya çıktığını belirtmekte fayda var, çünkü, tarihsel olarak, bir çok dbms, katılımlarla ilgilenme konusunda oldukça zayıftı (MySQL özellikle dikkat çekici bir örnek.). Bu nedenle, n + 1, çoğu zaman, bir birleşmeden çok daha hızlı olmuştur. Ve sonra n + 1'de ilerlemenin yolları var ama yine de bir birleştirmeye ihtiyaç duymadan, asıl sorunun ne olduğu ile ilgili.

Ancak, MySQL artık katılım konusunda eskisinden çok daha iyi. MySQL’i ilk defa öğrendiğimde çok kullandım. Sonra ne kadar yavaş olduklarını keşfettim ve kodda n + 1 olarak değiştim. Ancak, son zamanlarda, katılımlara geri dönüyordum, çünkü MySQL şimdi ilk kez kullanmaya başladığımdan daha iyi durumda.

Bu günlerde, uygun bir şekilde indekslenmiş bir tablo setine basit bir katılım, performans açısından nadiren bir problemdir. Ve eğer bir performans baskısı verirse, endeks ipuçlarının kullanılması genellikle onları çözer.

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

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

Öyleyse özeti: MySQL'in onlarla yaptığı aşırı performans nedeniyle geçmişte birleşmekten kaçınıyorsanız, en son sürümlerde tekrar deneyin. Muhtemelen hoş bir sürpriz olacak.

36
Mark Goodge

Bu sorun nedeniyle Django'daki ORM'den uzaklaştık. Temel olarak, eğer denersen

for p in person:
    print p.car.colour

ORM tüm insanları mutlu bir şekilde geri getirecektir (genellikle bir Person nesnesinin örnekleri olarak), ancak daha sonra her bir Kişi için araç masasını sorgulaması gerekecektir.

Buna basit ve çok etkili bir yaklaşım, "fanfolding" olarak adlandırdığım, sorgunun ilişkisel bir veritabanındaki sonuçların sorgunun oluştuğu orijinal tablolarla eşleşmesi gerektiği saçma sapan fikrini engelleyen bir fikirdir.

1. Adım: Geniş seçim

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

Bu gibi bir şey dönecektir

  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

Adım 2: Nesnelleştir

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

Adım 3: Render

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

Python için fanfolding uygulaması için bu web sayfası bölümüne bakın.

26
rorycl

ŞİRKET ve ÇALIŞANINIZ olduğunu varsayalım. ŞİRKET'in birçok ÇALIŞANLARI vardır (yani, ÇALIŞANLARIN EMPLOYEE'nin bir ŞİRKET_KİMİ alanı vardır).

Bazı O/R yapılandırmalarında, eşlenen bir Şirket nesnesine sahip olduğunuz ve Çalışan nesnelerine eriştiğinizde, O/R aracı her çalışan için tek bir seçim yapacaktır; sadece SQL'de bir şeyler yapıyorsanız select * from employees where company_id = XX. Böylece N (çalışan sayısı) artı 1 (şirket)

Bu, EJB Varlık Fasulyesinin ilk sürümlerinin çalışma şeklidir. Hazırda Beklet gibi olayların bununla bitmiş olduğuna inanıyorum, ama pek emin değilim. Çoğu araç, haritalama stratejileri hakkında genellikle bilgi içerir.

17
davetron5000

İşte problemin güzel bir açıklaması - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name = neden-tembel

Artık sorunu anladığınıza göre, genellikle sorgunuzda bir birleştirme getirme işlemi yapılarak önlenebilir. Bu temel olarak tembel yüklü nesnenin getirilmesini zorlar, böylece veriler n + 1 sorguları yerine tek bir sorguda alınır. Bu yardımcı olur umarım.

15
Joe Dean

Kanımca Hibernate Pitfall'da yazılmış makale: Neden İlişkiler Tembel Olmalı? N + 1 konusunun tam tersi.

Doğru bir açıklamaya ihtiyacınız olursa, lütfen Hazırda Bekletme - Bölüm 19: Performansı Geliştirme - Getirme Stratejileri

Seçme getirme (varsayılan), N + 1 sorun seçtiği için çok hassastır, bu nedenle birleştirme almayı etkinleştirmek isteyebiliriz

13
Anoop Isaac

Konuyla ilgili Ayende yayınına bakın: NHibernate'de N + 1 Seçme Sorunu ile Mücadele

Temel olarak, NHibernate veya EntityFramework gibi bir ORM kullanırken, bir-çok (ana-detay) ilişkiniz varsa ve her ana kayıt başına tüm ayrıntıları listelemek istiyorsanız, N + 1 sorgusunu arama yapmak veritabanı, "N" ana kayıt 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 tane N sorgusu.

Daha fazla veritabanı sorgulaması -> daha fazla gecikme süresi -> uygulama/veritabanı performansını düşürdü.

Bununla birlikte, ORM'lerin bu problemden kaçınması için, çoğunlukla "birleştirme" kullanarak seçenekleri vardır.

13
Nathan

N + 1 sorgu sorunu, bir ilişkilendirme almayı unuttuğunuzda ortaya çıkar ve daha sonra erişmeniz gerekir:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

LOGGER.info("Loaded {} comments", comments.size());

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

Aşağıdaki SQL ifadelerini üreten:

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

İlk olarak, Hazırda Bekletme JPQL sorgusunu yürütür ve PostComment varlıklarının bir listesi alınır.

Daha sonra, her PostComment için, ilişkili post özelliği, Post başlığını içeren bir günlük mesajı oluşturmak için kullanılır.

post ilişkilendirmesi başlatılmadığından, Hibernate Post varlığını ikincil bir sorgu ile getirmelidir ve N PostComment varlıkları için, N daha fazla sorgu çalıştırılacaktır (N + 1 sorgu sorunu).

Öncelikle, uygun SQL günlüğü ve izlemesi yapmanız gerekir, böylece bu sorunu tespit edebilirsiniz.

İkincisi, bu tür bir konunun entegrasyon testleriyle yakalanması daha iyidir. Üretilen SQL ifadelerinin beklenen sayısını doğrulamak için bir otomatik JUnit assert kullanabilirsiniz . db-unit project , bu işlevi zaten sunuyor ve açık kaynağı.

N + 1 sorgusu sorununu tanımladığınızda, bir JOIN FETCH kullanmanız gerekir; böylelikle alt ilişkilendirmelerin N yerine bir sorguda getirilmesi gerekir. Birden fazla alt ilişkilendirme almanız gerekiyorsa, ilk sorguda bir koleksiyonu ve ikincisini ikincil bir SQL sorgusu getirmek daha iyidir.

12
Vlad Mihalcea

Verilen link n + 1 problemine çok basit bir örnek. Hazırda Bekletme durumuna uygularsanız, temelde aynı şeyden bahsediyor. Bir nesneyi sorguladığınızda varlık yüklenir, ancak herhangi bir ilişki (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 üzere başka bir sorgu. Döndürülen 100 nesne, bir ilk sorgu ve ardından her biri n + 1 için ilişkilendirme için 100 ek sorgu anlamına gelir.

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

10
Jeff Mills

Her biri 1 sonuç veren 100 sorgu göndermekten daha 100 sonuç döndüren 1 sorgu yayınlamak çok daha hızlıdır.

9
jj_

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

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

Maliyetler:

Dizinlerin ram'a uyduğunu varsayın.

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

Dizinlerin ram'a uymadığını varsayalım.

En kötü durumda ek maliyetler 1 + N plaka yükleme endeksi için erişir.

özet

Şişe boynu plakaya erişimdir (hdd'de saniyede 70 defa rastgele erişim) İstekli bir birleştirme seçimi, yük kapasitesi için plakaya 1 + N + (N * 4) kere de erişir. Yani endeksler ram'a uyuyorsa - sorun değil, yeterince hızlı çünkü sadece ram işlemleri var.

8
hans wurst

N + 1 seçim sorunu bir acıdır ve ünite testlerinde bu gibi vakaları tespit etmek mantıklıdır. Belirli bir test yöntemiyle yürütülen sorgu sayısını veya yalnızca rasgele bir kod bloğunu doğrulamak için küçük bir kütüphane geliştirdim - JDBC Sniffer

Test sınıfınıza özel bir JUnit kuralı ekleyin ve test yöntemleriniz üzerine beklenen sayıda sorgu içeren açıklama ekleyin:

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

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

Diğerlerinin daha zarif bir şekilde ifade ettiği gibi, ya OneToMany sütunlarının Kartezyen ürününe sahip olmanız ya da N + 1 Seçimleri yapmanızdır. Muhtemel devasa sonuç kümesi ya da sırasıyla veritabanıyla konuşkanlık.

Bunun belirtilmediğine şaşırdım, ancak bu sorunla nasıl başa çıktım ... Yarı geçici bir kimlik tablosu hazırladım . Bunu, IN () deyimi sınırlamanız olduğunda da yaparım .

Bu, tüm durumlar için işe yaramaz (muhtemelen çoğunluğu bile değil), ancak Kartezyen ürünün elinden çıkacak çok sayıda alt nesne varsa, özellikle de işe yarar (örneğin, OneToMany sayıyı sütunlar. Sonuçların bir kısmı sütunların çarpımı olacaktır) ve bunun bir toplu iş benzeri olması.

Öncelikle üst nesne kimliklerini toplu olarak bir kimliklerini masasına yerleştir. Bu batch_id bizim uygulamamızla oluşturduğumuz ve tuttuğumuz bir şey.

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

Şimdi her OneToMany sütunu için, ids tablosunda bir SELECT yaptınız _ds masasında INNER JOINing alt tabloyu WHERE batch_id= ile (veya tam tersi). Sadece sonuç sütunlarını birleştirmeyi kolaylaştıracağından id sütununa göre sipariş verdiğinizden emin olmak istiyorsunuz (aksi halde o kadar da kötü olmayabilecek sonuç kümesinin tamamı için bir HashMap/Table gerekir).

O zaman sadece periyodik olarak tablolarını temizlersiniz.

Bu, özellikle kullanıcı bir tür toplu işlem için 100 veya daha fazla farklı öğe seçtiyse özellikle iyi çalışır. 100 farklı ID'yi geçici tabloya koyun.

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

5
Adam Gent

Matt Solnit örneğini ele alalım, Araba ve Tekerlekler arasında bir ilişki LAZY olarak 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 "ÇEKME * car_id =: id" FOR EACH Araba için "Seçim * yapacağı anlamına gelir.

Bu, her N otomobil tarafından ilk ve daha fazla 1 seçim yapar, bu yüzden n + 1 problemi denir.

Bundan kaçınmak için, ilişkiyi istekli hale getirin; böylece hazırda bekletme, bir birimle veri yükler.

Ancak, ilgili Tekerleklere birçok kez erişemiyorsanız, dikkatini LAZY'da tutmanız veya getirme türünü Kriterlerle değiştirmek daha iyidir.

1
martins.tuga