Domanda

Il "problema delle selezioni N+1" viene generalmente indicato come un problema nelle discussioni sulla mappatura relazionale degli oggetti (ORM) e capisco che ha qualcosa a che fare con la necessità di effettuare molte query sul database per qualcosa che sembra semplice nell'oggetto mondo.

Qualcuno ha una spiegazione più dettagliata del problema?

È stato utile?

Soluzione

Diciamo che hai una collezione di Car oggetti (righe del database) e ciascuno Car ha una collezione di Wheel oggetti (anche righe).In altre parole, Car -> Wheel è una relazione 1-a-molti.

Ora, diciamo che devi scorrere tutte le auto e, per ciascuna, stampare un elenco delle ruote.L'implementazione ingenua dell'O/R farebbe quanto segue:

SELECT * FROM Cars;

Poi per ciascuno Car:

SELECT * FROM Wheel WHERE CarId = ?

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

In alternativa, è possibile ottenere tutte le ruote ed eseguire le ricerche in memoria:

SELECT * FROM Wheel

Ciò riduce il numero di viaggi di andata e ritorno al database da N+1 a 2.La maggior parte degli strumenti ORM offre diversi modi per impedire le selezioni N+1.

Riferimento: Persistenza Java con ibernazione, capitolo 13.

Altri suggerimenti

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

Ciò ti dà un set di risultati in cui le righe secondarie in table2 causano la duplicazione restituendo i risultati table1 per ogni riga secondaria in table2.I mappatori O/R devono differenziare le istanze di table1 in base a un campo chiave univoco, quindi utilizzare tutte le colonne di 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 secondari 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 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

L'O/RM dovrebbe riempire un'istanza di Home con ID=1, Indirizzo="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 utilizzato sopra comporterebbe:

Id Address
1  22 Valley St

con una query separata come

SELECT * FROM Person WHERE HouseId = 1

e risultando 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 ottieni tutti i dati in anticipo, il che potrebbe essere ciò che alla fine desideri.Il vantaggio di N+1 è che la complessità delle query è ridotta e puoi utilizzare il caricamento lento in cui i set di risultati secondari vengono caricati solo alla prima richiesta.

Fornitore con una relazione uno-a-molti con il Prodotto.Un Fornitore ha (fornisce) 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 eseguire query sul prodotto è Seleziona

  • Modalità di recupero (predefinita):È possibile accedere alle informazioni sul fornitore

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

  • Si accede al fornitore

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 dichiarazione di selezione per Prodotto
  • N selezionare dichiarazioni per Fornitore

Questo è il problema di selezione N+1!

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 dbm sono stati piuttosto scadenti 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 n+1 ma comunque senza bisogno di un join, che è ciò a cui si riferisce il problema originale.

Tuttavia, MySQL ora è molto migliore di prima per quanto riguarda i join.Quando ho imparato MySQL per la prima volta, ho utilizzato molto i join.Poi ho scoperto quanto sono lenti e sono passato invece a n+1 nel codice.Ma recentemente sono tornato ai join, perché MySQL ora è molto più bravo a gestirli rispetto a quando ho iniziato a usarlo.

Al giorno d'oggi, un semplice join su un insieme di tabelle indicizzate correttamente rappresenta raramente un problema, in termini di prestazioni.E se dà un calo delle prestazioni, l'uso dei suggerimenti sull'indice spesso li risolve.

Questo è discusso qui da uno dei team di sviluppo 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 pessime prestazioni di MySQL, riprova con le versioni più recenti.Probabilmente rimarrai piacevolmente sorpreso.

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

for p in person:
    print p.car.colour

L'ORM restituirà volentieri tutte le persone (tipicamente come istanze di un oggetto Persona), ma poi dovrà interrogare la tabella dell'auto per ogni Persona.

Un approccio semplice e molto efficace a questo è quello che io chiamo "piegatura a ventaglio", che evita l'idea insensata che i risultati della query da un database relazionale debbano corrispondere alle tabelle originali da cui è composta la query.

Passo 1:Ampia scelta

  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

Passo 2:Oggettivare

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

Passaggio 3:Render

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

Vedere questa pagina web per un'implementazione di piegatura a ventaglio per pitone.

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

In alcune configurazioni O/R, quando si ha un oggetto Azienda mappato e si accede ai suoi oggetti Dipendente, lo strumento O/R eseguirà una selezione per ogni dipendente, mentre se si stesse semplicemente facendo le cose in SQL diretto, si potrebbero select * from employees where company_id = XX.Quindi N (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 solitamente include informazioni sulla strategia di mappatura adottata.

Ecco una buona descrizione del problema: https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

Ora che hai compreso il problema, in genere è possibile evitarlo eseguendo un recupero del join nella query.Ciò forza sostanzialmente il recupero dell'oggetto caricato in modo lento in modo che i dati vengano recuperati in una query anziché in n+1 query.Spero che questo ti aiuti.

Secondo me l'articolo scritto in Insidia dell'ibernazione:Perché le relazioni dovrebbero essere pigre è esattamente l'opposto del vero problema N+1.

Se hai bisogno di una spiegazione corretta, fai riferimento Ibernazione - Capitolo 19:Miglioramento delle prestazioni: strategie di recupero

Seleziona il recupero (il valore predefinito) è estremamente vulnerabile a N+1 seleziona i problemi, quindi potremmo voler abilitare il recupero

Controlla il post di Ayende sull'argomento: Combattere il problema della selezione 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 principale, è necessario effettuare N + 1 chiamate di query al database, dove "N" è il numero di record anagrafici:1 query per ottenere tutti i record anagrafici e N query, una per record anagrafico, per ottenere tutti i dettagli per record anagrafico.

Più chiamate di query al database --> più tempo di latenza --> prestazioni dell'applicazione/database ridotte.

Tuttavia, gli ORM hanno opzioni per evitare questo problema, principalmente utilizzando i "join".

Il problema delle query N+1 si verifica quando ci si dimentica di recuperare un'associazione e quindi è necessario accedervi:

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

Che genera le seguenti istruzioni SQL:

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'

Innanzitutto, Hibernate esegue la query JPQL e un elenco di PostComment le entità vengono recuperate.

Quindi, per ciascuno PostComment, gli associati post viene utilizzata per generare un messaggio di registro contenente il file Post titolo.

Perché il post l'associazione non è inizializzata, Hibernate deve recuperare il file Post entità con una query secondaria e per n PostComment entità, verranno eseguite altre N query (da qui il problema delle query N+1).

Innanzitutto, hai bisogno registrazione e monitoraggio SQL adeguati in modo che tu possa individuare questo problema.

In secondo luogo, questo tipo di problema è meglio essere individuato dai test di integrazione.Puoi usare un asserzione JUnit automatica per convalidare il conteggio previsto delle istruzioni SQL generate.IL progetto db-unit fornisce già questa funzionalità ed è open source.

Una volta identificato il problema relativo alla query N+1, è necessario utilizzare JOIN FETCH in modo che le associazioni secondarie vengano recuperate in una query, anziché in N.Se è necessario recuperare più associazioni secondarie, è preferibile recuperare una raccolta nella query iniziale e la seconda con una query SQL secondaria.

Il collegamento fornito presenta un esempio molto semplice del problema n + 1.Se lo applichi a Hibernate, sostanzialmente si parla della stessa cosa.Quando esegui una query per un oggetto, l'entità viene caricata ma qualsiasi associazione (a meno che non sia configurata diversamente) verrà caricata in modo lento.Quindi una query per gli oggetti radice e un'altra query per caricare le associazioni per ciascuno di questi.100 oggetti restituiti indicano una query iniziale e quindi 100 query aggiuntive per ottenere l'associazione per ciascuno, n + 1.

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

Un milionario possiede N automobili.Vuoi ottenere tutte (4) le ruote.

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

Costi:

Supponiamo che gli indici si adattino alla ram.

1 + N analisi e pianificazione delle query + ricerca dell'indice E 1 + N + (N * 4) accesso alla piastra per il caricamento del carico utile.

Supponiamo che gli indici non si adattino a ram.

Costi aggiuntivi nel caso peggiore 1 + N accessi piastra per indice di carico.

Riepilogo

Il collo della bottiglia è l'accesso alla piastra (ca.70 volte al secondo accesso casuale su HDD) Una selezione di join ansiosa accederebbe anche alla piastra 1 + N + (n * 4) per il payload.Quindi, se gli indici si adattano alla ram, nessun problema, è abbastanza veloce perché sono coinvolte solo le operazioni ram.

È molto più veloce emettere 1 query che restituisce 100 risultati piuttosto che emettere 100 query che restituiscono ciascuna 1 risultato.

Il problema della selezione N+1 è una seccatura ed è opportuno 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 semplicemente da un blocco di codice arbitrario - Sniffer JDBC

Aggiungi semplicemente una regola JUnit speciale alla tua classe di test e inserisci un'annotazione con il numero previsto di query sui tuoi metodi di test:

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

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

Il problema, come altri hanno affermato in modo più elegante, è che hai un prodotto cartesiano delle colonne OneToMany o stai eseguendo selezioni N+1.O possibile risultato gigantesco o chiacchierato con il database, rispettivamente.

Sono sorpreso che questo non sia menzionato, ma è così che ho risolto questo problema... Creo una tabella ID semi-temporanea. Lo faccio anche quando hai il IN () clausola limitativa.

Questo non funziona per tutti i casi (probabilmente nemmeno per la maggioranza) ma funziona particolarmente bene se si hanno molti oggetti figli tali che il prodotto cartesiano sfuggirà di mano (cioè molti OneToMany colonne, il numero di risultati sarà una moltiplicazione delle colonne) ed è più un lavoro batch.

Per prima cosa inserisci gli ID degli oggetti principali come batch in una tabella ID.Questo batch_id è qualcosa che generiamo nella nostra app e che manteniamo.

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

Ora per ciascuno OneToMany colonna fai semplicemente a SELECT sulla tabella ID INNER JOINing la tabella figlio con a WHERE batch_id= (o vice versa).Vuoi solo assicurarti di ordinare in base alla 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 poi così male).

Quindi pulisci periodicamente la tabella degli ID.

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

Ora il numero di query che stai eseguendo dipende dal numero di colonne OneToMany.

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

Questo rende la prima selezione e le successive 1 selezione per ciascuna N auto, ecco perché si chiama problema n+1.

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 accedi alle Ruote associate, è meglio mantenerlo PIGRO o cambiare tipo di recupero con Criteria.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top