Qu'est-ce que le «problème de sélection N + 1» dans ORM (Object-Relational Mapping)?

StackOverflow https://stackoverflow.com/questions/97197

  •  01-07-2019
  •  | 
  •  

Question

Le "N + 1 problème de sélection" " est généralement considéré comme un problème dans les discussions ORM (Object-Relational Mapping), et je comprends que cela a quelque chose à voir avec le fait de devoir faire beaucoup de requêtes à la base de données pour quelque chose qui semble simple dans le monde des objets.

Quelqu'un at-il une explication plus détaillée du problème?

Était-ce utile?

La solution

Supposons que vous avez une collection d'objets Car (lignes de la base de données) et que chaque Car possède une collection d'objets Wheel (ainsi que des lignes ). En d'autres termes, Car - > La molette est une relation un à plusieurs.

Maintenant, supposons que vous deviez parcourir toutes les voitures et pour chacune d’elles, imprimer une liste des roues. La mise en œuvre naïve des O / R aurait les conséquences suivantes:

SELECT * FROM Cars;

Et ensuite pour chaque voiture :

SELECT * FROM Wheel WHERE CarId = ?

En d’autres termes, vous avez une sélection pour les voitures, puis N sélections supplémentaires, où N est le nombre total de voitures.

Vous pouvez également obtenir toutes les roues et effectuer les recherches en mémoire:

SELECT * FROM Wheel

Ceci réduit le nombre d'allers-retours à la base de données de N + 1 à 2. La plupart des outils ORM vous proposent plusieurs moyens d'éviter les sélections N + 1.

Référence: Persistence Java avec Hibernate , chapitre 13.

Autres conseils

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

Cela vous donne un ensemble de résultats où les lignes enfants de la table2 provoquent la duplication en renvoyant les résultats de la table1 pour chaque ligne enfant de la table2. Les mappeurs O / R doivent différencier les instances table1 en fonction d'un champ de clé unique, puis utiliser toutes les colonnes de table2 pour renseigner les instances enfant.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N + 1 correspond au lieu où la première requête remplit l'objet principal et à la seconde tous les objets enfants de chacun des objets primaires uniques renvoyés.

Considérez:

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

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

et des tables avec une structure similaire. Une seule requête pour l’adresse " 22 Valley St " peut retourner:

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 doit remplir une instance de Home avec ID = 1, Address = "22 Valley St". puis remplissez le tableau Inhabitants avec des instances de personnes pour Dave, John et Mike avec une seule requête.

Une requête N + 1 pour la même adresse utilisée ci-dessus aurait pour résultat:

Id Address
1  22 Valley St

avec une requête distincte telle que

SELECT * FROM Person WHERE HouseId = 1

et aboutissant à un ensemble de données séparé tel que

Name    HouseId
Dave    1
John    1
Mike    1

et le résultat final étant le même que ci-dessus avec la requête unique.

Les avantages de la sélection unique sont que vous obtenez toutes les données à l’avance, ce qui peut correspondre à vos désirs ultimes. La complexité de la requête réduit les avantages de N + 1 et vous pouvez utiliser un chargement différé dans lequel les ensembles de résultats enfants ne sont chargés qu'à la première demande.

Fournisseur ayant une relation un-à-plusieurs avec Product. Un fournisseur a (fournit) de nombreux produits.

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

Facteurs:

  • Mode paresseux pour le fournisseur défini sur & # 8220; true & # 8221; (par défaut)

  • Le mode de récupération utilisé pour l'interrogation du produit est sélectionné

  • Mode de récupération (par défaut): les informations sur le fournisseur sont accessibles

  • La mise en cache ne joue pas pour la première fois le

  • le fournisseur est consulté

Le mode de récupération est Sélectionner la récupération (par défaut)

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

Résultat:

  • 1 instruction select pour le produit
  • N instructions choisies pour le fournisseur

C’est un problème de sélection N + 1!

Je ne peux pas commenter directement d'autres réponses, car je n'ai pas assez de réputation. Cependant, il convient de noter que le problème ne se pose essentiellement que parce qu'historiquement, beaucoup de dbms ont été assez médiocres pour la gestion des jointures (MySQL est un exemple particulièrement remarquable). Donc, n + 1 a souvent été nettement plus rapide qu'une jointure. Et puis, il y a moyen d'améliorer n + 1 mais sans avoir besoin d'une jointure, ce à quoi se rapporte le problème initial.

Cependant, MySQL est maintenant bien meilleur qu’il ne l’était en matière de jointure. Quand j'ai appris MySQL, j'ai utilisé beaucoup de jointures. Ensuite, j'ai découvert à quel point ils sont lents et je suis passé à n + 1 dans le code. Mais récemment, je reviens aux jointures, car MySQL est maintenant beaucoup mieux à même de les gérer que lorsque j'ai commencé à l'utiliser.

De nos jours, une simple jointure sur un ensemble de tables correctement indexé pose rarement problème, en termes de performances. Et si cela donne un coup dur aux performances, l’utilisation des indicateurs d’index les résout souvent.

Ceci est discuté ici par l'une des équipes de développement de MySQL:

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

Le résumé est le suivant: si vous évitiez les jointures dans le passé en raison des performances décevantes de MySQL, essayez à nouveau avec les dernières versions. Vous serez probablement agréablement surpris.

Nous avons quitté l'ORM de Django à cause de ce problème. Fondamentalement, si vous essayez de faire

for p in person:
    print p.car.colour

L'ORM renverra avec plaisir toutes les personnes (généralement en tant qu'instances d'un objet Person), mais il devra alors interroger la table car pour chaque personne.

J'appelle " fanfolding " une approche simple et très efficace, ce qui évite l'idée absurde que les résultats d'une requête d'une base de données relationnelle doivent correspondre aux tables d'origine la requête est composée.

Étape 1: Sélection large

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

Ceci retournera quelque chose comme

  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

Étape 2: objectiver

Transformez les résultats en créateur d'objet générique avec un argument à scinder après le troisième élément. Cela signifie que "jones". l'objet ne sera pas créé plus d'une fois.

Étape 3: restituer

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

Voir cette page Web pour une implémentation de fanfolding pour python.

Supposons que vous ayez SOCIÉTÉ et EMPLOYÉ. SOCIÉTÉ compte de nombreux EMPLOYÉS (c’est-à-dire que EMPLOYEE a un champ COMPANY_ID).

Dans certaines configurations O / R, lorsque vous avez un objet Société mappé et que vous allez accéder à ses objets Employee, l'outil O / R effectue une sélection pour chaque employé. Dans ce cas, si peut sélectionner * parmi les employés où company_id = XX . Donc N (nombre d'employés) plus 1 (entreprise)

C’est ainsi que fonctionnaient les versions initiales d’EJB Entity Beans. Je crois que des choses comme Hibernate ont éliminé cela, mais je n'en suis pas trop sûr. La plupart des outils incluent généralement des informations sur leur stratégie de mappage.

Voici une bonne description du problème - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip -pitfall.php? name = pourquoi-lazy

Maintenant que vous comprenez le problème, vous pouvez généralement l'éviter en effectuant une extraction de jointure dans votre requête. Cela force essentiellement l'extraction de l'objet chargé paresseux afin que les données soient récupérées dans une requête au lieu de n + 1 requêtes. J'espère que cela vous aidera.

À mon avis, l'article rédigé dans Hibernate Pitfall: pourquoi les relations doivent être paresseuses est exactement le contraire de la véritable question N + 1.

Si vous avez besoin d'une explication correcte, veuillez vous reporter à Hibernate - Chapitre 19: Amélioration des performances - Stratégies de récupération

  

Sélectionnez la récupération (valeur par défaut) est   extrêmement vulnérable à N + 1 sélectionne   problèmes, donc nous pourrions vouloir activer   rejoindre la récupération

Consultez le post d'Ayende sur le sujet: Combattre le problème Sélectionnez N + 1 dans NHibernate

En principe, lorsque vous utilisez un ORM comme NHibernate ou EntityFramework, si vous avez une relation un vers plusieurs (maître-détail) et souhaitez répertorier tous les détails de chaque enregistrement principal, vous devez effectuer une requête N + 1. appels à la base de données, " N " étant le nombre d'enregistrements principaux: 1 requête pour obtenir tous les enregistrements principaux et N requêtes, une par enregistrement principal, pour obtenir tous les détails par enregistrement principal.

Plus d'appels de requête de base de données - > plus de temps de latence - > diminution des performances de l'application / de la base de données.

Cependant, les ORM ont des options pour éviter ce problème, principalement à l'aide de "jointures".

Le problème de requête N + 1 se produit lorsque vous oubliez de récupérer une association et que vous devez ensuite y accéder:

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

Qui génère les instructions SQL suivantes:

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'

Tout d'abord, Hibernate exécute la requête JPQL et une liste d'entités PostComment est extraite.

Ensuite, pour chaque PostComment , la propriété post associée est utilisée pour générer un message de journal contenant le titre Post .

Etant donné que l'association post n'est pas initialisée, Hibernate doit extraire l'entité Post avec une requête secondaire, et pour N entités PostComment , N autres requêtes vont être exécutées (d'où le problème de requête N + 1).

Tout d'abord, vous avez besoin de une journalisation et une surveillance SQL appropriées . afin que vous puissiez repérer ce problème.

Deuxièmement, il est préférable que ce type de problème soit détecté par des tests d’intégration. Vous pouvez utiliser un assertion automatique de JUnit à valider le nombre attendu d'instructions SQL générées . Le projet db-unit fournit déjà cette fonctionnalité et son code source ouvert.

Lorsque vous avez identifié le problème de requête N + 1, vous devez utilisez JOIN FETCH pour que les associations d'enfants soient extraites dans une requête au lieu de N . Si vous avez besoin d'extraire plusieurs associations enfants, il est préférable d'extraire une collection dans la requête initiale et la seconde avec une requête SQL secondaire.

Le lien fourni contient un exemple très simple du problème n + 1. Si vous l'appliquez à Hibernate, il s'agit essentiellement de la même chose. Lorsque vous interrogez un objet, l'entité est chargée mais toutes les associations (sauf configuration contraire) sont chargées paresseux. D'où une requête pour les objets racine et une autre requête pour charger les associations pour chacun d'eux. 100 objets renvoyés correspondent à une requête initiale, puis à 100 requêtes supplémentaires pour obtenir l'association pour chaque n + 1.

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

Un millionnaire a N voitures. Vous voulez obtenir toutes les (4) roues.

Une (1) requête charge toutes les voitures, mais pour chaque (N) voiture, une requête distincte est soumise pour le chargement des roues.

Coûts:

Supposez que les index entrent dans le bélier.

Analyse et planification de la requête 1 + N + recherche d'index AND 1 + N + (N * 4) accès aux plaques pour le chargement de la charge utile.

Supposons que les index ne rentrent pas dans la RAM.

Coûts supplémentaires dans le cas le plus défavorable 1 accès à la plaque N pour l’indice de chargement.

Résumé

Le goulot de la bouteille est un accès à la plaque (environ 70 fois par seconde, accès aléatoire sur disque dur) Une sélection enthousiaste de joint accéderait également aux temps de plaque 1 + N + (N * 4) pour la charge utile. Donc, si les index entrent dans la RAM - pas de problème, c’est assez rapide parce que seules les opérations de la RAM sont impliquées.

Il est beaucoup plus rapide d’émettre une requête qui renvoie 100 résultats que de 100 requêtes renvoyant un résultat.

N + 1 problème sélectionné est une douleur, et il est logique de détecter de tels cas dans les tests unitaires. J'ai développé une petite bibliothèque pour vérifier le nombre de requêtes exécutées par une méthode de test donnée ou juste un bloc de code arbitraire - Renifleur JDBC

Ajoutez simplement une règle spéciale JUnit à votre classe de test et placez une annotation avec le nombre attendu de requêtes sur vos méthodes de test:

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

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

Le problème, comme d’autres l'ont déjà dit avec plus d'élégance, est que vous avez un produit cartésien des colonnes OneToMany ou que vous effectuez des sélections N + 1. Résultats gigantesques possibles ou discussions avec la base de données, respectivement.

Je suis surpris que cela ne soit pas mentionné, mais voici comment j'ai résolu ce problème ... Je crée un tableau d'identifiants semi-temporaire . Je le fais également lorsque vous disposez de la limitation de la clause IN () .

Cela ne fonctionne pas dans tous les cas (probablement même pas la majorité), mais cela fonctionne particulièrement bien si vous avez beaucoup d'objets enfants tels que le produit cartésien deviendra incontrôlable (c'est-à-dire beaucoup de OneToMany colonnes le nombre de résultats sera une multiplication des colonnes) et plus d’un lot comme job.

D'abord, vous insérez vos identifiants d'objet parent sous forme de lot dans une table d'id. Ce batch_id est quelque chose que nous générons dans notre application et que nous conservons.

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

Maintenant, pour chaque colonne OneToMany , il vous suffit de faire un SELECT sur la table d'ids INNER JOIN dans la table enfant avec un WHERE batch_id = (ou vice versa). Vous voulez simplement vous assurer de classer par la colonne id car cela facilitera la fusion des colonnes de résultats (sinon, vous aurez besoin d'un HashMap / Table pour tout le jeu de résultats, qui ne sera peut-être pas si mauvais).

Ensuite, vous nettoyez régulièrement la table des identifiants.

Cela fonctionne aussi particulièrement bien si l’utilisateur sélectionne une centaine d’articles distincts pour un traitement en bloc. Placez les 100 identifiants distincts dans la table temporaire.

Le nombre de requêtes que vous effectuez est désormais égal au nombre de colonnes OneToMany.

Prenez l'exemple de Matt Solnit, imaginez que vous définissiez une association entre Car et Wheels comme étant LAZY et que vous ayez besoin de quelques champs Wheels. Cela signifie qu’après le premier choix, Hibernate fera "Select * from Wheels où car_id =: id". POUR CHAQUE voiture.

Cela fait la première sélection et plus 1 sélection par N voiture, c’est pourquoi on l’appelle n + 1 problème.

Pour éviter cela, créez une association avec l'extraction désirée afin qu'hibernate charge les données avec une jointure.

Mais attention, si vous n'avez souvent pas accès aux roues associées, il est préférable de la conserver ou de changer de type d'extraction à l'aide de critères.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top