Quelle est la différence entre select_related et prefetch_related dans Django ORM?

291

Dans Django doc,

select_related() "suit" les relations de clé étrangère, en sélectionnant des données supplémentaires sur les objets liés lors de l'exécution de sa requête.

prefetch_related() effectue une recherche distincte pour chaque relation et effectue la "jointure" en Python.

Qu'est-ce que cela signifie par "faire la jointure en python"? Quelqu'un peut-il illustrer avec un exemple?

Ma compréhension est que pour une relation de clé étrangère, utilisez select_related; et pour la relation M2M, utilisez prefetch_related. Est-ce correct?

NeoWang
la source
2
L'exécution de la jointure en python signifie que la jointure n'aura pas lieu dans la base de données. Avec un select_related, votre jointure se produit dans la base de données et vous ne subissez qu'une seule requête de base de données. Avec prefetch_related, vous exécuterez deux requêtes, puis les résultats seront «joints» par l'ORM afin que vous puissiez toujours taper object.related_set
Mark Galloway
3
En tant que note de bas de page, Timmy O'Mahony peut également expliquer leurs différences en utilisant les
accès
Cela peut vous aider à apprendrebatta.com/blog/working-with-select_related-in-django-89
anjaneyulubatta505

Réponses:

424

Votre compréhension est généralement correcte. Vous utilisez select_relatedlorsque l'objet que vous allez sélectionner est un seul objet, OneToOneFieldou alors a ForeignKey. Vous utilisez prefetch_relatedlorsque vous allez obtenir un "ensemble" de choses, donc ManyToManyFields comme vous l'avez dit ou inversez le ForeignKeys. Juste pour clarifier ce que je veux dire par "reverse ForeignKeys", voici un exemple:

class ModelA(models.Model):
    pass

class ModelB(models.Model):
    a = ForeignKey(ModelA)

ModelB.objects.select_related('a').all() # Forward ForeignKey relationship
ModelA.objects.prefetch_related('modelb_set').all() # Reverse ForeignKey relationship

La différence est que cela select_relatedfait une jointure SQL et récupère donc les résultats dans le cadre de la table à partir du serveur SQL. prefetch_relatedd'autre part exécute une autre requête et réduit donc les colonnes redondantes dans l'objet d'origine ( ModelAdans l'exemple ci-dessus). Vous pouvez utiliser prefetch_relatedpour tout ce que vous pouvez utiliser select_related.

Les compromis sont que prefetch_relateddoit créer et envoyer une liste d'ID à sélectionner à nouveau sur le serveur, cela peut prendre un certain temps. Je ne sais pas s'il y a une bonne façon de le faire dans une transaction, mais je crois comprendre que Django envoie toujours juste une liste et dit SELECT ... WHERE pk IN (..., ..., ...) fondamentalement. Dans ce cas, si les données extraites sont rares (disons les objets de l'État américain liés aux adresses des personnes), cela peut être très bon, mais si elles sont plus proches de une à une, cela peut gaspiller beaucoup de communications. En cas de doute, essayez les deux et voyez celui qui fonctionne le mieux.

Tout ce qui est discuté ci-dessus concerne essentiellement les communications avec la base de données. Côté Python cependantprefetch_related l'avantage supplémentaire est qu'un seul objet est utilisé pour représenter chaque objet de la base de données. Avec select_relatedles objets en double seront créés en Python pour chaque objet "parent". Étant donné que les objets en Python ont un peu de surcharge de mémoire décente, cela peut également être une considération.

CrazyCasta
la source
3
quoi de plus rapide cependant?
elad silver
24
select_relatedest une requête tandis que prefetch_relateddeux, la première est donc plus rapide. Mais select_relatedne vous aidera pas pour ManyToManyField- s
bhinesley
31
@eladsilver Désolé pour la réponse lente. Cela dépend en fait. select_relatedutilise un JOIN dans le SQL tandis que prefetch_relatedla requête est exécutée sur le premier modèle, collecte tous les ID dont il a besoin pour la prélecture, puis exécute une requête avec une clause IN dans le WHERE avec tous les ID dont il a besoin. Si vous avez dit 3-5 modèles utilisant la même clé étrangère, ce select_relatedsera certainement mieux. Si vous avez des centaines ou des milliers de modèles utilisant la même clé étrangère, cela prefetch_relatedpourrait être mieux. Entre les deux, vous devrez tester et voir ce qui se passe.
CrazyCasta
1
Je contesterais votre commentaire sur la prélecture liée "n'a généralement pas beaucoup de sens". Cela est vrai pour les champs FK marqués comme uniques, mais partout où plusieurs lignes ont la même valeur FK (auteur, utilisateur, catégorie, ville, etc.) la prélecture réduit la bande passante entre Django et la base de données mais pas la duplication des lignes. Il utilise également généralement moins de mémoire sur la base de données. L'un ou l'autre de ces éléments est souvent plus important que la surcharge d'une seule requête supplémentaire. Étant donné que c'est la meilleure réponse à une question raisonnablement populaire, je pense que cela devrait être noté dans la réponse.
Gordon Wrigley
1
@GordonWrigley Oui, cela fait un moment que je n'ai pas écrit ça, alors je suis retourné et j'ai clarifié un peu. Je ne suis pas sûr d'être d'accord avec le bit "utilise moins de mémoire sur la base de données", mais oui à tout. Et il peut certainement utiliser moins de mémoire du côté Python.
CrazyCasta
26

Les deux méthodes atteignent le même objectif, pour éviter les requêtes db inutiles. Mais ils utilisent différentes approches pour l'efficacité.

La seule raison d'utiliser l'une de ces méthodes est lorsqu'une seule grande requête est préférable à de nombreuses petites requêtes. Django utilise la grande requête pour créer des modèles en mémoire de manière préventive plutôt que d'effectuer des requêtes à la demande sur la base de données.

select_relatedeffectue une jointure à chaque recherche, mais étend la sélection pour inclure les colonnes de toutes les tables jointes. Cependant, cette approche comporte une mise en garde.

Les jointures ont le potentiel de multiplier le nombre de lignes dans une requête. Lorsque vous effectuez une jointure sur une clé étrangère ou un champ un-à-un, le nombre de lignes n'augmente pas. Toutefois, les jointures plusieurs à plusieurs n'ont pas cette garantie. Donc, Django restreintselect_related aux relations qui ne se traduiront pas de manière inattendue par une adhésion massive.

Le "join in python" pour prefetch_relatedest un peu plus alarmant qu'il ne devrait l'être. Il crée une requête distincte pour chaque table à joindre. Il filtre chacune de ces tables avec une clause WHERE IN, comme:

SELECT "credential"."id",
       "credential"."uuid",
       "credential"."identity_id"
FROM   "credential"
WHERE  "credential"."identity_id" IN
    (84706, 48746, 871441, 84713, 76492, 84621, 51472);

Plutôt que d'effectuer une jointure unique avec potentiellement trop de lignes, chaque table est divisée en une requête distincte.

cdosborn
la source
1

Comme le dit la documentation de Django:

prefetch_related ()

Renvoie un QuerySet qui récupérera automatiquement, en un seul lot, les objets associés pour chacune des recherches spécifiées.

Cela a un objectif similaire à select_related, dans la mesure où les deux sont conçus pour arrêter le déluge de requêtes de base de données provoqué par l'accès aux objets associés, mais la stratégie est assez différente.

select_related fonctionne en créant une jointure SQL et en incluant les champs de l'objet associé dans l'instruction SELECT. Pour cette raison, select_related obtient les objets associés dans la même requête de base de données. Cependant, pour éviter le jeu de résultats beaucoup plus large qui résulterait de la jonction à travers une relation «plusieurs», select_related est limité aux relations à valeur unique - clé étrangère et un à un.

prefetch_related, d'autre part, effectue une recherche distincte pour chaque relation et effectue la «jointure» en Python. Cela lui permet de pré-extraire des objets plusieurs-à-plusieurs et plusieurs-à-un, ce qui ne peut pas être fait à l'aide de select_related, en plus de la clé étrangère et des relations un-à-un qui sont prises en charge par select_related. Il prend également en charge la prélecture de GenericRelation et GenericForeignKey, mais il doit être limité à un ensemble homogène de résultats. Par exemple, la prélecture des objets référencés par un GenericForeignKey n'est prise en charge que si la requête est limitée à un ContentType.

Plus d'informations à ce sujet: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related

Amin.B
la source
1

Passé en revue les réponses déjà publiées. Je pensais que ce serait mieux si j'ajoute une réponse avec un exemple réel.

Disons que vous avez 3 modèles Django qui sont liés.

class M1(models.Model):
    name = models.CharField(max_length=10)

class M2(models.Model):
    name = models.CharField(max_length=10)
    select_relation = models.ForeignKey(M1, on_delete=models.CASCADE)
    prefetch_relation = models.ManyToManyField(to='M3')

class M3(models.Model):
    name = models.CharField(max_length=10)

Ici, vous pouvez interroger le M2modèle et ses M1objets relatifs en utilisant le select_relationchamp et les M3objets en utilisantprefetch_relation champ.

Cependant , comme nous l' avons mentionné M1la relation de » de M2est ForeignKey, elle retourne juste seulement 1 record pour tout M2objet. La même chose s'applique pourOneToOneField également.

Mais M3la relation de M2est un ManyToManyFieldqui pourrait renvoyer un certain nombre deM1 objets.

Prenons un cas où vous avez 2 M2objets m21, m22qui ont les mêmes 5M3 objets associés avec des ID 1,2,3,4,5. Lorsque vous récupérez des M3objets associés pour chacun de ces M2objets, si vous utilisez select related, voici comment cela va fonctionner.

Pas:

  1. Trouver m21 objet.
  2. Recherchez tous les M3objets liés à l' m21objet dont les ID sont1,2,3,4,5 .
  3. Répétez la même chose pour l' m22objet et tous les autres M2objets.

Comme nous avons les mêmes 1,2,3,4,5identifiants pour les deux m21,m22 objets, si nous utilisons l'option select_related, il va interroger la base de données deux fois pour les mêmes ID qui ont déjà été récupérés.

Au lieu de cela, si vous utilisez prefetch_related, lorsque vous essayez d'obtenir des M2objets, il prendra note de tous les ID que vos objets ont renvoyés (Remarque: seuls les ID) lors de l'interrogation de la M2table et, comme dernière étape, Django va effectuer une requête vers la M3table avec l'ensemble de tous les ID que vos M2objets ont renvoyés. et rejoignez-les pourM2 objets utilisant Python au lieu de la base de données.

De cette façon, vous interrogez tous les M3objets une seule fois, ce qui améliore les performances.

Jarvis
la source