Le «problème de sélection N + 1» est généralement indiqué comme un problème dans les discussions sur le mappage relationnel objet (ORM), et je comprends qu'il a quelque chose à voir avec le fait d'avoir à faire beaucoup de requêtes dans la base de données pour quelque chose qui semble simple dans l'objet monde.
Quelqu'un at-il une explication plus détaillée du problème?
orm
select-n-plus-1
Lars A. Brekken
la source
la source
Réponses:
Supposons que vous ayez une collection d'
Car
objets (lignes de base de données) et que chacunCar
ait une collection d'Wheel
objets (également des lignes). En d'autres termes,Car
→Wheel
est une relation 1 à plusieurs.Maintenant, disons que vous devez parcourir toutes les voitures et, pour chacune, imprimer une liste des roues. La mise en œuvre naïve d'O / R ferait ce qui suit:
Et puis pour chacun
Car
: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.
Alternativement, on pourrait obtenir toutes les roues et effectuer les recherches en mémoire:
Cela réduit le nombre d'aller-retour dans la base de données de N + 1 à 2. La plupart des outils ORM vous offrent plusieurs façons d'empêcher les sélections N + 1.
Référence: Java Persistence with Hibernate , chapitre 13.
la source
SELECT * from Wheel;
), au lieu de N + 1. Avec un grand N, l'atteinte des performances peut être très importante.Cela vous permet d'obtenir un jeu de résultats dans lequel les lignes enfants du tableau 2 provoquent la duplication en renvoyant les résultats du tableau 1 pour chaque ligne enfant du tableau 2. Les mappeurs O / R doivent différencier les instances de table1 en fonction d'un champ de clé unique, puis utiliser toutes les colonnes de table2 pour remplir les instances enfant.
Le N + 1 est l'endroit où la première requête remplit l'objet principal et la deuxième requête remplit tous les objets enfants pour chacun des objets principaux uniques retournés.
Considérer:
et des tables avec une structure similaire. Une seule requête pour l'adresse "22 Valley St" peut renvoyer:
L'O / RM doit remplir une instance de Home avec ID = 1, Address = "22 Valley St", puis remplir le tableau Inhabitants avec des instances People pour Dave, John et Mike avec une seule requête.
Une requête N + 1 pour la même adresse utilisée ci-dessus entraînerait:
avec une requête distincte comme
et résultant en un ensemble de données distinct comme
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 être ce que vous désirez en fin de compte. Les avantages de N + 1 sont la complexité des requêtes est réduite et vous pouvez utiliser le chargement différé où les jeux de résultats enfants ne sont chargés qu'à la première demande.
la source
Fournisseur avec une relation un-à-plusieurs avec le produit. Un fournisseur a (fournit) de nombreux produits.
Les facteurs:
Mode paresseux pour le fournisseur défini sur «vrai» (par défaut)
Le mode d'extraction utilisé pour interroger le produit est Sélectionner
Mode d'extraction (par défaut): accès aux informations du fournisseur
La mise en cache ne joue pas de rôle pour la première fois
Accès au fournisseur
Le mode d'extraction est Sélectionner l'extraction (par défaut)
Résultat:
C'est un problème de sélection N + 1!
la source
Je ne peux pas commenter directement les autres réponses, car je n'ai pas assez de réputation. Mais il convient de noter que le problème ne se pose essentiellement que parce que, historiquement, beaucoup de dbms ont été assez médiocres en ce qui concerne la gestion des jointures (MySQL en étant un exemple particulièrement remarquable). Ainsi, n + 1 a souvent été beaucoup plus rapide qu'une jointure. Et puis il y a des moyens d'améliorer n + 1 mais toujours sans avoir besoin d'une jointure, ce à quoi le problème d'origine se rapporte.
Cependant, MySQL est maintenant bien meilleur qu'il ne l'était quand il s'agit de jointures. Quand j'ai appris MySQL pour la première fois, j'ai beaucoup utilisé les jointures. Ensuite, j'ai découvert à quel point ils sont lents et je suis passé à n + 1 dans le code. Mais, récemment, je suis revenu sur les jointures, car MySQL est maintenant beaucoup plus efficace pour les gérer qu'il ne l'était lorsque j'ai commencé à l'utiliser.
De nos jours, une simple jointure sur un ensemble de tables correctement indexées est rarement un problème, en termes de performances. Et si cela donne un impact sur les performances, l'utilisation d'indices d'index les résout souvent.
Ceci est discuté ici par l'une des équipes de développement MySQL:
http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html
Donc, le résumé est le suivant: si vous avez évité les jointures dans le passé en raison des performances abominables de MySQL avec elles, essayez à nouveau sur les dernières versions. Vous serez probablement agréablement surpris.
la source
JOIN
algorithmes couramment utilisés dans les SGBDR est appelé boucles imbriquées. Il s'agit fondamentalement d'une sélection N + 1 sous le capot. La seule différence est que la base de données a fait un choix intelligent pour l'utiliser en fonction des statistiques et des index, plutôt que du code client la forçant catégoriquement sur ce chemin.Nous nous sommes éloignés de l'ORM à Django à cause de ce problème. Fondamentalement, si vous essayez de faire
L'ORM retournera volontiers toutes les personnes (généralement en tant qu'instances d'un objet Person), mais il devra alors interroger la table de voiture pour chaque personne.
Une approche simple et très efficace est ce que j'appelle le " fanfolding ", ce qui évite l'idée absurde que les résultats de la requête d'une base de données relationnelle doivent correspondre aux tables d'origine à partir desquelles la requête est composée.
Étape 1: Sélection large
Cela retournera quelque chose comme
Étape 2: Objectiver
Suck les résultats dans un créateur d'objet générique avec un argument à diviser après le troisième élément. Cela signifie que l'objet "jones" ne sera pas créé plus d'une fois.
Étape 3: rendu
Voir cette page Web pour une implémentation du fanfolding pour python.
la source
select_related
, ce qui est censé résoudre ce problème - en fait, ses documents commencent par un exemple similaire à votrep.car.colour
exemple.select_related()
etprefetch_related()
à Django maintenant.select_related()
et ami ne semble pas faire d'extrapolations évidemment utiles d'une jointure commeLEFT OUTER JOIN
. Le problème n'est pas un problème d'interface, mais un problème lié à l'étrange idée que les objets et les données relationnelles sont mappables ... à mon avis.Quel est le problème de requête N + 1
Le problème de requête N + 1 se produit lorsque l'infrastructure d'accès aux données a exécuté N instructions SQL supplémentaires pour extraire les mêmes données qui auraient pu être récupérées lors de l'exécution de la requête SQL principale.
Plus la valeur de N est élevée, plus les requêtes seront exécutées, plus l'impact sur les performances sera important. Et, contrairement au journal des requêtes lentes qui peut vous aider à trouver des requêtes à exécution lente, le problème N + 1 ne sera pas localisé car chaque requête supplémentaire individuelle s'exécute suffisamment rapidement pour ne pas déclencher le journal des requêtes lentes.
Le problème est l'exécution d'un grand nombre de requêtes supplémentaires qui, dans l'ensemble, prennent suffisamment de temps pour ralentir le temps de réponse.
Considérons que nous avons les tables de base de données post et post_comments suivantes qui forment une relation de table un-à-plusieurs :
Nous allons créer les 4
post
lignes suivantes:Et, nous allons également créer 4
post_comment
enregistrements enfants:Problème de requête N + 1 avec SQL simple
Si vous sélectionnez l'
post_comments
utilisation de cette requête SQL:Et, plus tard, vous décidez de récupérer les associés
post
title
pour chacunpost_comment
:Vous allez déclencher le problème de requête N + 1 car, au lieu d'une requête SQL, vous avez exécuté 5 (1 + 4):
La résolution du problème de requête N + 1 est très facile. Tout ce que vous devez faire est d'extraire toutes les données dont vous avez besoin dans la requête SQL d'origine, comme ceci:
Cette fois, une seule requête SQL est exécutée pour récupérer toutes les données que nous souhaitons utiliser.
Problème de requête N + 1 avec JPA et Hibernate
Lorsque vous utilisez JPA et Hibernate, il existe plusieurs façons de déclencher le problème de requête N + 1, il est donc très important de savoir comment éviter ces situations.
Pour les exemples suivants, considérez que nous mappons les tables
post
etpost_comments
aux entités suivantes:Les mappages JPA ressemblent à ceci:
FetchType.EAGER
L'utilisation
FetchType.EAGER
implicite ou explicite de vos associations JPA est une mauvaise idée car vous allez récupérer bien plus de données dont vous avez besoin. De plus, laFetchType.EAGER
stratégie est également sujette à des problèmes de requête N + 1.Malheureusement, les associations
@ManyToOne
et@OneToOne
utilisentFetchType.EAGER
par défaut, donc si vos mappages ressemblent à ceci:Vous utilisez la
FetchType.EAGER
stratégie et, chaque fois que vous oubliez de l'utiliserJOIN FETCH
lors du chargement de certainesPostComment
entités avec une requête API JPQL ou Criteria:Vous allez déclencher le problème de requête N + 1:
Notez les instructions SELECT supplémentaires qui sont exécutées car le
post
association doit être puisée avant de retourner leList
desPostComment
entités.Contrairement au plan de récupération par défaut que vous utilisez lorsque vous appelez le
find
méthode deEnrityManager
, une requête API JPQL ou Criteria définit un plan explicite que Hibernate ne peut pas modifier en injectant automatiquement un JOIN FETCH. Donc, vous devez le faire manuellement.Si vous n'aviez pas besoin du
post
association, vous n'avez pas de chance lors de l'utilisationFetchType.EAGER
car il n'y a aucun moyen d'éviter de la chercher. C'est pourquoi il vaut mieux utiliserFetchType.LAZY
par défaut.Mais si vous souhaitez utiliser l'
post
association, vous pouvez utiliserJOIN FETCH
pour éviter le problème de requête N + 1:Cette fois, Hibernate exécutera une seule instruction SQL:
FetchType.LAZY
Même si vous passez à l'utilisation
FetchType.LAZY
explicite pour toutes les associations, vous pouvez toujours rencontrer le problème N + 1.Cette fois, l'
post
association est cartographiée comme suit:Maintenant, lorsque vous récupérez les
PostComment
entités:Hibernate exécutera une seule instruction SQL:
Mais, si après, vous allez référencer l'association paresseuse
post
:Vous obtiendrez le problème de requête N + 1:
Parce que le
post
association est extraite paresseusement, une instruction SQL secondaire sera exécutée lors de l'accès à l'association paresseuse afin de générer le message de journal.Encore une fois, le correctif consiste à ajouter une
JOIN FETCH
clause à la requête JPQL:Et, tout comme dans le
FetchType.EAGER
exemple, cette requête JPQL générera une seule instruction SQL.Comment détecter automatiquement le problème de requête N + 1
Si vous souhaitez détecter automatiquement le problème de requête N + 1 dans votre couche d'accès aux données, cet article explique comment procéder à l'aide de la
db-util
projet open-source.Tout d'abord, vous devez ajouter la dépendance Maven suivante:
Ensuite, il vous suffit d'utiliser l'
SQLStatementCountValidator
utilitaire pour affirmer les instructions SQL sous-jacentes générées:Si vous utilisez
FetchType.EAGER
et exécutez le scénario de test ci-dessus, vous obtiendrez l'échec du scénario de test suivant:la source
SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5
. Mais ce que vous obtenez est 2 voitures à 5 roues (première voiture avec les 4 roues et deuxième voiture avec seulement 1 roue), car LIMIT limitera l'ensemble des résultats, pas seulement la clause racine.Supposons que vous ayez une ENTREPRISE et un EMPLOYÉ. L'ENTREPRISE a de nombreux EMPLOYÉS (c'est-à-dire que l'EMPLOYÉ a un champ COMPANY_ID).
Dans certaines configurations O / R, lorsque vous avez un objet Company mappé et accédez à ses objets Employee, l'outil O / R effectue une sélection pour chaque employé, alors que si vous faisiez simplement des choses en SQL simple, vous le pouviez
select * from employees where company_id = XX
. Ainsi 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 supprimé cela, mais je ne suis pas trop sûr. La plupart des outils incluent généralement des informations sur leur stratégie de cartographie.
la source
Voici une bonne description du problème
Maintenant que vous comprenez le problème, il peut généralement être évité en effectuant une extraction de jointure dans votre requête. Cela force essentiellement la récupération de l'objet chargé paresseux afin que les données soient récupérées dans une seule requête au lieu de n + 1 requêtes. J'espère que cela t'aides.
la source
Consultez l'article d'Ayende sur le sujet: Combattre le problème Select N + 1 dans NHibernate .
Fondamentalement, lorsque vous utilisez un ORM comme NHibernate ou EntityFramework, si vous avez une relation un-à-plusieurs (maître-détail) et que vous souhaitez répertorier tous les détails pour chaque enregistrement maître, vous devez effectuer des appels de requête N + 1 à la base de données, "N" étant le nombre d'enregistrements maîtres: 1 requête pour obtenir tous les enregistrements maîtres, et N requêtes, une par enregistrement maître, pour obtenir tous les détails par enregistrement maître.
Plus d'appels de requête de base de données → plus de temps de latence → baisse des performances de l'application / base de données.
Cependant, les ORM ont des options pour éviter ce problème, principalement en utilisant des JOIN.
la source
Il est beaucoup plus rapide d'émettre 1 requête qui renvoie 100 résultats que d'émettre 100 requêtes qui renvoient chacune 1 résultat.
la source
À mon avis, l'article écrit dans Hibernate Pitfall: Pourquoi les relations devraient être paresseuses est exactement à l'opposé du vrai problème N + 1.
Si vous avez besoin d'une explication correcte, reportez-vous à Hibernate - Chapitre 19: Amélioration des performances - Stratégies d'extraction
la source
Le lien fourni a 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 recherchez un objet, l'entité est chargée mais toutes les associations (sauf configuration contraire) seront chargées paresseusement. D'où une requête pour les objets racine et une autre requête pour charger les associations pour chacun d'eux. 100 objets retournés signifient une requête initiale puis 100 requêtes supplémentaires pour obtenir l'association pour chacun, n + 1.
http://pramatr.com/2009/02/05/sql-n-1-selects-explained/
la source
Un millionnaire possède 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.
Frais:
Supposons que les index s'insèrent dans la RAM.
Analyse et planification 1 + N des requêtes + recherche d'index ET accès aux plaques 1 + N + (N * 4) pour le chargement de la charge utile.
Supposons que les index ne rentrent pas dans le ram.
Coûts supplémentaires dans le pire des cas 1 + N accès à la plaque pour l'indice de chargement.
Sommaire
Le col de la bouteille est un accès à la plaque (environ 70 fois par seconde, un accès aléatoire sur le disque dur). Une sélection de jointure désireuse accède également à la plaque 1 + N + (N * 4) fois pour la charge utile. Donc, si les index s'insèrent dans ram - pas de problème, c'est assez rapide car seules les opérations de ram sont impliquées.
la source
Le problème de sélection N + 1 est une douleur, et il est logique de détecter de tels cas dans des 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 - JDBC Sniffer
Ajoutez simplement une règle JUnit spéciale à votre classe de test et placez une annotation avec le nombre attendu de requêtes sur vos méthodes de test:
la source
Le problème, comme d'autres l'ont dit plus élégamment, est que vous avez un produit cartésien des colonnes OneToMany ou que vous effectuez des sélections N + 1. Soit un ensemble de résultats gigantesque possible ou bavard 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 une table d'ID semi-temporaire . Je le fais également lorsque vous avez la
IN ()
limitation de clause .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 un multiplication des colonnes) et son plus d'un travail de type batch.Vous insérez d'abord vos ID d'objet parent en tant que lot dans une table d'ID. Ce batch_id est quelque chose que nous générons dans notre application et que nous conservons.
Maintenant, pour chaque
OneToMany
colonne, vous venez de faire unSELECT
sur la table des identifiants de la tableINNER JOIN
enfant avec unWHERE batch_id=
(ou vice versa). Vous voulez simplement vous assurer que vous triez par la colonne id car cela facilitera la fusion des colonnes de résultats (sinon vous aurez besoin d'un HashMap / Table pour l'ensemble des résultats, ce qui n'est peut-être pas si mauvais).Ensuite, vous nettoyez périodiquement la table ids.
Cela fonctionne également particulièrement bien si l'utilisateur sélectionne environ 100 éléments distincts pour une sorte de traitement en masse. Mettez les 100 identifiants distincts dans la table temporaire.
Maintenant, le nombre de requêtes que vous effectuez est le nombre de colonnes OneToMany.
la source
Prenons l'exemple de Matt Solnit, imaginez que vous définissez une association entre la voiture et les roues comme LAZY et que vous ayez besoin de certains champs Wheels. Cela signifie qu'après la première sélection, l'hibernation va faire "Select * from Wheels où car_id =: id" POUR CHAQUE voiture.
Cela fait la première sélection et plus 1 sélection par chaque voiture N, c'est pourquoi cela s'appelle un problème n + 1.
Pour éviter cela, rendez l'association extraite comme désireuse, afin que l'hibernation charge les données avec une jointure.
Mais attention, si plusieurs fois vous n'accédez pas aux roues associées, il est préférable de le garder paresseux ou de changer le type de récupération avec les critères.
la source