Qual è il "N + 1 seleziona il problema" in ORM (Object-Relational Mapping)?


1598

Il "N + 1 seleziona il problema" è generalmente indicato come un problema nelle discussioni sulla mappatura oggetto-relazionale (ORM) e capisco che ha qualcosa a che fare con il dover fare molte query di database per qualcosa che sembra semplice nell'oggetto mondo.

Qualcuno ha una spiegazione più dettagliata del problema?


2
Questo è un ottimo collegamento con una bella spiegazione sulla comprensione del problema n + 1 . Comprende anche le soluzioni per contrastare questo problema: architects.dzone.com/articles/how-identify-and-resilve-n1
aces.

Ci sono alcuni post utili che parlano di questo problema e della possibile soluzione. Problemi comuni dell'applicazione e come risolverli: il problema Seleziona N + 1 , Il proiettile (argento) per il problema N + 1 , Caricamento
lento

Per tutti coloro che sono alla ricerca di una soluzione a questo problema, ho trovato un post che lo descriveva. stackoverflow.com/questions/32453989/...
damndemon

2
Considerando le risposte, questo non dovrebbe essere definito come un problema 1 + N? Dato che questa sembra essere una terminologia, non sto chiedendo specificamente OP.
user1418717

Risposte:


1018

Supponiamo che tu abbia una raccolta di Caroggetti (righe del database) e ognuno Carha una raccolta di Wheeloggetti (anche righe). In altre parole, CarWheelè una relazione da 1 a molti.

Ora, supponiamo che sia necessario scorrere tutte le auto e, per ognuna, stampare un elenco delle ruote. L'implementazione O / R ingenua farebbe quanto segue:

SELECT * FROM Cars;

E poi per ciascuno Car:

SELECT * FROM Wheel WHERE CarId = ?

In altre parole, hai una selezione per le auto, quindi N selezioni aggiuntive, dove N è il numero totale di auto.

In alternativa, si potrebbero ottenere tutte le ruote ed eseguire le ricerche in memoria:

SELECT * FROM Wheel

Ciò riduce il numero di round trip nel database da N + 1 a 2. La maggior parte degli strumenti ORM offre diversi modi per impedire la selezione di N + 1.

Riferimento: Java Persistence with Hibernate , capitolo 13.


140
Per chiarire "Questo è male" - potresti ottenere tutte le ruote con 1 select ( SELECT * from Wheel;), invece di N + 1. Con una N grande, il successo delle prestazioni può essere molto significativo.
Tucuxi,

212
@tucuxi Sono sorpreso che tu abbia ricevuto così tanti voti per aver sbagliato. Un database è molto buono sugli indici, fare la query per un CarID specifico restituirebbe molto velocemente. Ma se hai tutte le ruote sono una volta, dovresti cercare CarID nella tua applicazione, che non è indicizzata, questo è più lento. A meno che tu non abbia grossi problemi di latenza nel raggiungere il tuo database andando n + 1 è effettivamente più veloce - e sì, l'ho confrontato con una grande varietà di codice del mondo reale.
Ariel,

74
@ariel Il modo 'corretto' è quello di ottenere tutte le ruote, ordinate da CarId (1 selezione), e se sono necessari più dettagli rispetto a CarId, fare una seconda query per tutte le auto (2 domande in totale). La stampa delle cose è ora ottimale e non sono necessari indici o memoria secondaria (è possibile scorrere i risultati, non è necessario scaricarli tutti). Hai confrontato la cosa sbagliata. Se sei ancora sicuro dei tuoi benchmark, ti ​​dispiacerebbe pubblicare un commento più lungo (o una risposta completa) che spieghi l'esperimento e i risultati?
Tucuxi,

92
"Hibernate (non ho familiarità con gli altri framework ORM) offre diversi modi per gestirlo." e così sono?
Tima,

58
@Ariel Prova a eseguire i tuoi benchmark con database e application server su macchine separate. Nella mia esperienza, i viaggi di andata e ritorno nel database costano di più in termini di costi rispetto alla query stessa. Quindi sì, le domande sono molto veloci, ma sono i viaggi di andata e ritorno che hanno causato scompiglio. Ho convertito "WHERE Id = const " in "WHERE Id IN ( const , const , ...)" e ottenuto ordini di grandezza aumenta da esso.
Hans,

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

Ciò fornisce un set di risultati in cui le righe figlio nella tabella2 causano la duplicazione restituendo i risultati della tabella1 per ogni riga figlio nella tabella2. I mappatori O / R dovrebbero differenziare le istanze table1 in base a un campo chiave univoco, quindi utilizzare tutte le colonne table2 per popolare le istanze figlio.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N + 1 è il punto in cui la prima query popola l'oggetto primario e la seconda query popola tutti gli oggetti figlio per ciascuno degli oggetti primari univoci restituiti.

Prendere in considerazione:

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

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

e tabelle con una struttura simile. Una singola query per l'indirizzo "22 Valley St" può restituire:

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

O / RM dovrebbe riempire un'istanza di Home con ID = 1, Address = "22 Valley St" e quindi popolare l'array Inhabitants con istanze People per Dave, John e Mike con una sola query.

Una query N + 1 per lo stesso indirizzo usato sopra comporterebbe:

Id Address
1  22 Valley St

con una query separata come

SELECT * FROM Person WHERE HouseId = 1

e risultante in un set di dati separato come

Name    HouseId
Dave    1
John    1
Mike    1

e il risultato finale è lo stesso di sopra con la singola query.

Il vantaggio della selezione singola è che si ottengono tutti i dati in anticipo, il che potrebbe essere ciò che in definitiva si desidera. I vantaggi di N + 1 sono la complessità della query ridotta e puoi utilizzare il caricamento lento in cui i set di risultati figlio vengono caricati solo alla prima richiesta.


4
L'altro vantaggio di n + 1 è che è più veloce perché il database può restituire i risultati direttamente da un indice. Fare il join e quindi l'ordinamento richiede una tabella temporanea, che è più lenta. L'unico motivo per evitare n + 1 è se hai molta latenza a parlare con il tuo database.
Ariel,

17
Partecipare e ordinare può essere abbastanza veloce (perché ti unirai ai campi indicizzati e possibilmente ordinati). Quanto è grande il tuo 'n + 1'? Credi seriamente che il problema n + 1 si applichi solo alle connessioni al database ad alta latenza?
Tucuxi,

9
@ariel - Il tuo consiglio che N + 1 è il "più veloce" è sbagliato, anche se i tuoi benchmark potrebbero essere corretti. Come è possibile? Vedi en.wikipedia.org/wiki/Anecdotal_evidence e anche il mio commento nell'altra risposta a questa domanda.
Whitneyland,

7
@Ariel - Penso di aver capito bene :). Sto solo cercando di sottolineare che il tuo risultato si applica solo a una serie di condizioni. Potrei facilmente costruire un contro esempio che mostrasse il contrario. Ha senso?
Whitneyland,

13
Per ribadire, il problema di SELECT N + 1 è, in sostanza, il problema: ho 600 record da recuperare. È più veloce ottenerne tutte 600 in una query o 1 alla volta in 600 query. A meno che tu non sia su MyISAM e / o tu abbia uno schema scarsamente normalizzato / scarsamente indicizzato (nel qual caso l'ORM non è il problema), un db correttamente sintonizzato restituirà le 600 righe in 2 ms, mentre restituisce le singole righe in circa 1 ms ciascuno. Quindi spesso vediamo N + 1 impiegare centinaia di millisecondi in cui un join richiede solo una coppia
Cani

64

Fornitore con una relazione uno-a-molti con il Prodotto. Un fornitore ha (forniture) molti prodotti.

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

fattori:

  • Modalità Lazy per il fornitore impostata su "true" (impostazione predefinita)

  • La modalità di recupero utilizzata per l'interrogazione sul Prodotto è Seleziona

  • Modalità di recupero (impostazione predefinita): si accede alle informazioni del fornitore

  • La memorizzazione nella cache non svolge per la prima volta un ruolo

  • Il fornitore è accessibile

La modalità di recupero è Seleziona recupero (impostazione predefinita)

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

Risultato:

  • 1 selezionare dichiarazione per Prodotto
  • N selezionare dichiarazioni per il fornitore

Questo è un problema di selezione N + 1!


3
Dovrebbe essere 1 seleziona per il fornitore, quindi N seleziona per il prodotto?
bencampbell_14

@bencampbell_ Sì, inizialmente mi sentivo lo stesso. Ma poi con il suo esempio, è un prodotto per molti fornitori.
Mohd Faizan Khan,

38

Non posso commentare direttamente altre risposte, perché non ho abbastanza reputazione. Ma vale la pena notare che il problema si pone essenzialmente solo perché, storicamente, molti dbms sono stati piuttosto scarsi quando si tratta di gestire i join (MySQL è un esempio particolarmente degno di nota). Quindi n + 1 è stato, spesso, notevolmente più veloce di un join. E poi ci sono modi per migliorare su n + 1 ma ancora senza bisogno di un join, che è ciò a cui si riferisce il problema originale.

Tuttavia, MySQL ora è molto meglio di una volta quando si tratta di unirsi. Quando ho imparato MySQL per la prima volta, ho usato molto i join. Poi ho scoperto quanto sono lenti e invece sono passato a n + 1 nel codice. Ma, recentemente, sono tornato ai join, perché MySQL ora è molto più bravo a gestirli di quanto non lo fosse quando ho iniziato a usarlo.

Al giorno d'oggi, un semplice join su un set di tabelle correttamente indicizzato è raramente un problema, in termini di prestazioni. E se dà un colpo alle prestazioni, l'uso di suggerimenti sull'indice spesso li risolve.

Questo è discusso qui da uno dei team di sviluppo di MySQL:

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

Quindi il riassunto è: se in passato hai evitato i join a causa delle prestazioni orribili di MySQL con loro, riprova con le ultime versioni. Probabilmente rimarrai piacevolmente sorpreso.


7
Chiamare le prime versioni di MySQL un DBMS relazionale è piuttosto complicato ... Se le persone che si sono imbattute in questi problemi avessero utilizzato un vero database, non avrebbero riscontrato questo tipo di problemi. ;-)
Craig

2
È interessante notare che molti di questi tipi di problemi sono stati risolti in MySQL con l'introduzione e la successiva ottimizzazione del motore INNODB, ma ti imbatterai comunque in persone che cercano di promuovere MYISAM perché pensano che sia più veloce.
Craig,

5
Cordiali saluti, uno dei 3 JOINalgoritmi comuni utilizzati in RDBMS 'è chiamato loop nidificati. Fondamentalmente è una selezione N + 1 sotto il cofano. L'unica differenza è che il DB ha fatto una scelta intelligente per usarlo in base a statistiche e indici, piuttosto che al codice client costringendolo a percorrere quel percorso categoricamente.
Brandon

2
@Brandon Sì! Proprio come i suggerimenti JOIN e INDEX, forzare un determinato percorso di esecuzione in tutti i casi raramente supererà il database. Il database è quasi sempre molto, molto bravo a scegliere l'approccio ottimale per ottenere i dati. Forse nei primi tempi di dbs dovevi "formulare" la tua domanda in un modo peculiare per convincere il db, ma dopo decenni di ingegneria di livello mondiale, ora puoi ottenere le migliori prestazioni ponendo al tuo database una domanda relazionale e lasciandola scoprire come recuperare e assemblare quei dati per te.
Cani,

3
Non solo il database utilizza indici e statistiche, ma tutte le operazioni sono anche operazioni di I / O locali, molte delle quali operano spesso su cache altamente efficiente anziché su disco. I programmatori di database dedicano molta attenzione all'ottimizzazione di questo tipo di cose.
Craig,

27

Ci siamo allontanati dall'ORM di Django a causa di questo problema. Fondamentalmente, se ci provi

for p in person:
    print p.car.colour

L'ORM restituirà felicemente tutte le persone (in genere come istanze di un oggetto Person), ma dovrà quindi interrogare la tabella dell'auto per ogni persona.

Un approccio semplice e molto efficace a questo è qualcosa che io chiamo " fanfolding ", che evita l'idea senza senso che i risultati della query da un database relazionale dovrebbero essere ricondotti alle tabelle originali da cui è composta la query.

Passaggio 1: ampia selezione

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

Questo restituirà qualcosa di simile

  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

Passaggio 2: oggettivare

Succhiare i risultati in un creatore di oggetti generico con un argomento da dividere dopo il terzo elemento. Ciò significa che l'oggetto "jones" non verrà creato più di una volta.

Passaggio 3: rendering

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

Vedi questa pagina Web per un'implementazione di fanfolding per Python.


10
sono così felice di essermi imbattuto nel tuo post, perché pensavo di impazzire. quando ho scoperto il problema N + 1, il mio pensiero immediato è stato- beh, perché non crei semplicemente una vista che contenga tutte le informazioni di cui hai bisogno e tiri fuori da quella vista? hai convalidato la mia posizione. grazie Signore.
uno sviluppatore il

14
Ci siamo allontanati dall'ORM di Django a causa di questo problema. Eh? Django ha select_related, che ha lo scopo di risolverlo - in effetti, i suoi documenti iniziano con un esempio simile al tuo p.car.colouresempio.
Adrian17,

8
Questa è una vecchia risposta, che abbiamo select_related()e prefetch_related()adesso a Django.
Mariusz Jamro,

1
Freddo. Ma un select_related()amico non sembra fare nessuna delle estrapolazioni ovviamente utili di un join come LEFT OUTER JOIN. Il problema non è un problema di interfaccia, ma un problema che ha a che fare con la strana idea che oggetti e dati relazionali siano mappabili ... a mio avviso.
rorycl,

26

Poiché questa è una domanda molto comune, ho scritto questo articolo , su cui si basa questa risposta.

Qual è il problema di query N + 1

Il problema di query N + 1 si verifica quando il framework di accesso ai dati ha eseguito N istruzioni SQL aggiuntive per recuperare gli stessi dati che avrebbero potuto essere recuperati durante l'esecuzione della query SQL primaria.

Maggiore è il valore di N, più query verranno eseguite, maggiore sarà l'impatto sulle prestazioni. E, a differenza del registro delle query lente che può aiutarti a trovare le query con esecuzione lenta, il problema N + 1 non sarà individuabile perché ogni singola query aggiuntiva viene eseguita abbastanza velocemente da non attivare il registro delle query lente.

Il problema è l'esecuzione di un gran numero di query aggiuntive che, nel complesso, richiedono tempo sufficiente per rallentare i tempi di risposta.

Consideriamo che abbiamo le seguenti tabelle del database post e post_comments che formano una relazione da una a molte tabelle :

Le tabelle <code> post </code> e <code> post_comments </code>

Creeremo le seguenti 4 postrighe:

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)

E creeremo anche 4 post_commentrecord figlio:

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)

Problema di query N + 1 con SQL semplice

Se si seleziona post_commentsutilizzando questa query SQL:

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

E, in seguito, decidi di recuperare gli associati post titleper ciascuno 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
    );
}

Inizierai il problema della query N + 1 perché, anziché una query SQL, hai eseguito 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'

Risolvere il problema relativo alla query N + 1 è molto semplice. Tutto quello che devi fare è estrarre tutti i dati necessari nella query SQL originale, in questo modo:

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

Questa volta, viene eseguita una sola query SQL per recuperare tutti i dati che siamo ulteriormente interessati a utilizzare.

Problema di query N + 1 con JPA e Hibernate

Quando si utilizzano JPA e Hibernate, è possibile innescare il problema della query N + 1 in diversi modi, quindi è molto importante sapere come evitare queste situazioni.

Per i prossimi esempi, considera che stiamo mappando le tabelle poste post_commentssulle seguenti entità:

Entità <code> Post </code> e <code> PostComment </code>

I mapping JPA si presentano così:

@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

L'utilizzo FetchType.EAGERimplicito o esplicito per le associazioni JPA è una cattiva idea perché recupererai molti più dati di cui hai bisogno. Di più, ilFetchType.EAGERInoltre strategia è anche soggetta a problemi di query N + 1.

Sfortunatamente, le associazioni @ManyToOnee @OneToOneusano FetchType.EAGERdi default, quindi se le tue mappature assomigliano a questo:

@ManyToOne
private Post post;

Stai usando la FetchType.EAGERstrategia e, ogni volta che dimentichi di usarla JOIN FETCHquando carichi alcune PostCommententità con una query API JPQL o Criteria:

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

Stai per innescare il problema della query 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

Notare le istruzioni SELECT aggiuntive che vengono eseguite perché l' postassociazione deve essere recuperata prima di restituire il ListdiPostComment entità.

A differenza del piano di recupero predefinito, che si sta utilizzando quando si chiama il findmetodo di EnrityManager, una query API JPQL o Criteria definisce un piano esplicito che Hibernate non può modificare iniettando un JOIN FETCH automaticamente. Quindi, è necessario farlo manualmente.

Se non hai bisogno postdell'associazione, sei sfortunato quando lo usi FetchType.EAGERperché non c'è modo di evitare di recuperarlo. Ecco perché è meglio usareFetchType.LAZY di default.

Ma, se si desidera utilizzare l' postassociazione, è possibile utilizzare JOIN FETCHper evitare il problema della query 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()
    );
}

Questa volta, Hibernate eseguirà una singola istruzione 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'

Per maggiori dettagli sul motivo per cui dovresti evitare la FetchType.EAGERstrategia di recupero, consulta anche questo articolo .

FetchType.LAZY

Anche se passi all'uso FetchType.LAZYesplicito per tutte le associazioni, puoi comunque imbatterti nel problema N + 1.

Questa volta, l' postassociazione è mappata in questo modo:

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

Ora, quando recuperi le PostCommententità:

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

Hibernate eseguirà una singola istruzione SQL:

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

Ma, se in seguito, farai riferimento postall'associazione caricata in modo pigro :

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

Verrà visualizzato il problema della query 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'

Perché la post associazione viene recuperata pigramente, quando si accede all'associazione pigra verrà eseguita un'istruzione SQL secondaria per creare il messaggio di registro.

Ancora una volta, la correzione consiste nell'aggiunta di una JOIN FETCHclausola alla query 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()
    );
}

E, proprio come FetchType.EAGERnell'esempio, questa query JPQL genererà una singola istruzione SQL.

Anche se si sta utilizzando FetchType.LAZYe non si fa riferimento all'associazione figlio di una @OneToOnerelazione JPA bidirezionale , è comunque possibile attivare il problema della query N + 1.

Per maggiori dettagli su come superare il problema di query N + 1 generato dalle @OneToOneassociazioni, consulta questo articolo .

Come rilevare automaticamente il problema della query N + 1

Se si desidera rilevare automaticamente il problema di query N + 1 nel livello di accesso ai dati, questo articolo spiega come farlo utilizzandodb-util progetto open-source.

Innanzitutto, è necessario aggiungere la seguente dipendenza Maven:

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

Successivamente, devi solo usare l' SQLStatementCountValidatorutilità per affermare le istruzioni SQL sottostanti che vengono generate:

SQLStatementCountValidator.reset();

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

SQLStatementCountValidator.assertSelectCount(1);

Nel caso in cui si stia utilizzando FetchType.EAGERed eseguendo il test case sopra riportato, si otterrà il seguente errore del test case:

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!

Per maggiori dettagli sul progetto db-utilopen source, consulta questo articolo .


Ma ora hai un problema con l'impaginazione. Se hai 10 auto, ogni auto con 4 ruote e vuoi impaginare auto con 5 auto per pagina. Quindi in pratica hai SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5. Ma ciò che ottieni sono 2 auto con 5 ruote (prima auto con tutte e 4 le ruote e seconda auto con solo 1 ruota), perché LIMIT limiterà l'intero set di risultati, non solo la clausola di root.
CappY

2

Grazie per l'articolo Lo leggerò. Con lo scorrimento veloce - ho visto che la soluzione è Window Function, ma sono abbastanza nuovi in ​​MariaDB - quindi il problema persiste nelle versioni precedenti. :)
CappY

@VladMihalcea, ho sottolineato dal tuo articolo o dal post ogni volta che fai riferimento al caso ManyToOne mentre spieghi il problema N + 1. Ma in realtà le persone sono per lo più interessate al caso OneToMany relativo al problema N + 1. Potresti per favore riferire e spiegare il caso OneToMany?
JJ Beam

18

Supponiamo di avere AZIENDA e DIPENDENTE. L'AZIENDA ha molti DIPENDENTI (ovvero i DIPENDENTI hanno un campo COMPANY_ID).

In alcune configurazioni O / R, quando si dispone di un oggetto Azienda mappato e si accede ai suoi oggetti Dipendente, lo strumento O / R eseguirà una selezione per ogni dipendente, laddove se si eseguissero semplicemente operazioni in SQL diretto, è possibile select * from employees where company_id = XX. Quindi N (# di dipendenti) più 1 (azienda)

Ecco come funzionavano le versioni iniziali di EJB Entity Beans. Credo che cose come Hibernate abbiano eliminato questo, ma non ne sono troppo sicuro. La maggior parte degli strumenti di solito include informazioni sulla loro strategia di mappatura.


18

Ecco una buona descrizione del problema

Ora che hai compreso il problema, in genere può essere evitato eseguendo un recupero di join nella query. Questo in pratica forza il recupero dell'oggetto caricato in modo pigro, quindi i dati vengono recuperati in una query anziché in n + 1 query. Spero che sia di aiuto.


17

Controlla il post di Ayende sull'argomento: Combattere il problema Seleziona N + 1 in NHibernate .

Fondamentalmente, quando si utilizza un ORM come NHibernate o EntityFramework, se si dispone di una relazione uno-a-molti (master-dettaglio) e si desidera elencare tutti i dettagli per ciascun record master, è necessario effettuare chiamate di query N + 1 al database, "N" è il numero di record master: 1 query per ottenere tutti i record master e N query, una per record master, per ottenere tutti i dettagli per record principale.

Più chiamate alle query del database → più tempo di latenza → riduzione delle prestazioni dell'applicazione / del database.

Tuttavia, gli ORM hanno opzioni per evitare questo problema, principalmente usando JOIN.


3
i join non sono una buona soluzione (spesso), poiché possono comportare un prodotto cartesiano, il che significa che il numero di righe dei risultati è il numero di risultati della tabella radice moltiplicati per il numero di risultati in ciascuna tabella figlio. particolarmente male su più livelli di ararchia. Selezionando 20 "blog" con 100 "post" su ciascuno e 10 "commenti" su ciascun post, verranno visualizzate 20000 righe di risultati. NHibernate ha soluzioni alternative, ad esempio la "dimensione batch" (selezionare i figli con la clausola in in id padre) o "sottoseleziona".
Erik Hart,

14

È molto più veloce emettere 1 query che restituisce 100 risultati che emettere 100 query, ciascuna delle quali restituisce 1 risultato.


13

A mio avviso, l'articolo scritto in Hibernate Pitfall: Why Relationships dovrebbe essere pigro è esattamente l'opposto del vero problema N + 1.

Se hai bisogno di una spiegazione corretta, fai riferimento a Hibernate - Capitolo 19: Miglioramento delle prestazioni - Recupero delle strategie

Seleziona recupero (impostazione predefinita) è estremamente vulnerabile a N + 1 seleziona i problemi, quindi potremmo voler abilitare il recupero dei join


2
ho letto la pagina di ibernazione. Non dice che cosa il problema N + 1 seleziona in realtà è . Ma dice che puoi usare i join per risolverlo.
Ian Boyd,

3
la dimensione del batch è necessaria per selezionare il recupero, per selezionare oggetti figlio per più genitori in un'istruzione select. Sottoselezionare potrebbe essere un'altra alternativa. I join possono peggiorare se hai più livelli gerarchici e viene creato un prodotto cartesiano.
Erik Hart,

10

Il link fornito ha un esempio molto semplice del problema n + 1. Se lo applichi a Hibernate, sostanzialmente parla della stessa cosa. Quando si esegue una query per un oggetto, l'entità viene caricata ma tutte le associazioni (se non diversamente configurato) verranno caricate in modo lazy. Da qui una query per gli oggetti root e un'altra query per caricare le associazioni per ognuno di questi. 100 oggetti restituiti indicano una query iniziale e quindi 100 query aggiuntive per ottenere l'associazione per ciascuna, n + 1.

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


9

Un milionario ha N macchine. Vuoi ottenere tutte (4) le ruote.

Una (1) query carica tutte le auto, ma per ogni (N) auto viene inviata una query separata per le ruote di caricamento.

Costi:

Supponiamo che gli indici si adattino alla ram.

1 + N query di analisi e piallatura + ricerca indice E 1 + N + (N * 4) accesso alla piastra per il caricamento del carico utile.

Supponiamo che gli indici non rientrino nella RAM.

Costi aggiuntivi nel peggiore dei casi 1 + accessi alla piastra N per l'indice di caricamento.

Sommario

Il collo della bottiglia è accesso alla piastra (circa 70 volte al secondo accesso casuale su hdd) Una selezione di join desiderosi accederà anche alla piastra 1 + N + (N * 4) volte per il carico utile. Quindi, se gli indici si adattano alla ram - nessun problema, è abbastanza veloce perché coinvolgono solo le operazioni della ram.


9

Il problema con la selezione di N + 1 è una seccatura ed è logico rilevare tali casi nei test unitari. Ho sviluppato una piccola libreria per verificare il numero di query eseguite da un determinato metodo di test o solo un blocco arbitrario di codice - JDBC Sniffer

Basta aggiungere una speciale regola JUnit alla classe di test e posizionare l'annotazione con il numero previsto di query sui metodi di test:

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

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

5

Il problema, come altri hanno affermato in modo più elegante, è che hai un prodotto cartesiano delle colonne OneToMany o stai facendo selezioni N + 1. O possibile set di risultati giganteschi o chiacchierone con il database, rispettivamente.

Sono sorpreso che questo non sia menzionato ma è così che ho risolto questo problema ... Faccio una tabella degli ID semi-temporanea . Lo faccio anche quando hai la IN ()limitazione della clausola .

Questo non funziona per tutti i casi (probabilmente nemmeno la maggioranza) ma funziona particolarmente bene se hai molti oggetti figlio in modo tale che il prodotto cartesiano sfuggirà di mano (cioè molte OneToManycolonne il numero di risultati sarà un moltiplicazione delle colonne) ed è più simile a un lavoro batch.

Innanzitutto inserisci gli ID oggetto padre come batch in una tabella ID. Questo batch_id è qualcosa che generiamo nella nostra app e su cui tratteniamo.

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

Ora per ogni OneToManycolonna fai semplicemente un a SELECTsulla tabella ID nella tabella INNER JOINfiglio con un WHERE batch_id=(o viceversa). Devi solo assicurarti di ordinare per colonna id in quanto renderà più semplice l'unione delle colonne dei risultati (altrimenti avrai bisogno di una HashMap / Table per l'intero set di risultati che potrebbe non essere così male).

Quindi pulisci periodicamente la tabella degli ID.

Questo funziona anche particolarmente bene se l'utente seleziona dire circa 100 articoli distinti per una sorta di elaborazione in blocco. Inserisci i 100 ID distinti nella tabella temporanea.

Ora il numero di query che stai facendo è il numero di colonne OneToMany.


1

Prendi l'esempio di Matt Solnit, immagina di definire un'associazione tra Car e Wheels come LAZY e che hai bisogno di alcuni campi Wheels. Ciò significa che dopo la prima selezione, l'ibernazione eseguirà "Seleziona * da Ruote dove car_id =: id" PER OGNI automobile.

Questo fa la prima selezione e più 1 seleziona per ogni auto N, ecco perché si chiama n + 1 problema.

Per evitare ciò, rendere l'associazione recuperata come desiderosa, in modo che l'ibernazione carichi i dati con un join.

Ma attenzione, se molte volte non si accede a Wheels associati, è meglio tenerlo POCO o cambiare il tipo di recupero con Criteri.


1
Ancora una volta, i join non sono una buona soluzione, soprattutto quando è possibile caricare più di 2 livelli gerarchici. Seleziona invece "sottoseleziona" o "dimensione batch"; l'ultimo caricherà i figli per ID genitore nella clausola "in", come "seleziona ... da ruote in cui car_id (1,3,4,6,7,8,11,13)".
Erik Hart,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.