Optimisation de l’accès à la base de données¶
La couche de base de données de Django fournit plusieurs manières d’aider les développeurs à tirer le meilleur de leurs bases de données. Ce document rassemble des liens vers la documentation adéquate et ajoute quelques astuces, réparties dans plusieurs chapitres respectant la chronologie des étapes à suivre lorsqu’on souhaite optimiser l’utilisation de la base de données.
Priorité au profilage¶
Comme principe de programmation générale, cela va sans dire. Recherchez quelles sont les requêtes effectuées et ce qu’elles coûtent. Utilisez QuerySet.explain() pour comprendre comment les requêtes QuerySet sont exécutées par la base de données. Il est aussi possible d’utiliser un projet externe comme django-debug-toolbar ou un outil qui surveille directement la base de données.
Rappelez-vous que l’on peut optimiser pour la rapidité, pour la mémoire ou pour les deux, en fonction des besoins. Parfois, l’optimisation d’un côté va péjorer la situation de l’autre, mais dans d’autres cas, les deux vont en profiter ensemble. De plus, le travail effectué par le processus de la base de données n’a pas toujours le même coût (pour vous) que le même travail effectué dans le processus Python. C’est à vous de décider des priorités, de pondérer avantages et inconvénients et de profiler tout cela selon les besoins car tout dépend de votre application et du serveur.
Avec tout ce qui suit, n’oubliez pas de profiler après chaque modification pour vous assurer que celle-ci est bénéfique et que le bénéfice est suffisant pour compenser la perte de lisibilité du code. Pour toutes les suggestions ci-dessous, il faut tenir compte du risque que le principe général ne s’applique pas dans votre cas de figure, ou que l’effet soit même inversé.
Techniques standards d’optimisation de base de données¶
Ceci comprend :
Indexes. This is a number one priority, after you have determined from profiling what indexes should be added. Use
Meta.indexesorField.db_indexto add these from Django. Consider adding indexes to fields that you frequently query usingfilter(),exclude(),order_by(), etc. as indexes may help to speed up lookups. Note that determining the best indexes is a complex database-dependent topic that will depend on your particular application. The overhead of maintaining an index may outweigh any gains in query speed.
Utilisation appropriée des types de champs.
Nous supposerons que vous avez effectué les opérations mentionnées ci-dessus. La suite de ce document se concentre sur la façon d’utiliser Django de telle manière à ne pas effectuer de travail inutile. Ce document n’aborde cependant pas d’autres techniques d’optimisation qui s’appliquent à des opérations lourdes, comme la mise en cache à portée générale.
Compréhension des objets QuerySet¶
La compréhension des QuerySet est capitale pour obtenir de bonnes performances avec du code simple. En particulier :
Compréhension de l’évaluation du QuerySet¶
Pour éviter des problèmes de performance, il est important de comprendre :
que les QuerySet sont différés.
quand ils sont évalués.
Compréhension des attributs mis en cache¶
Tout comme la mise en cache d’un QuerySet global, il y a mise en cache du résultat des attributs dans les objets de l’ORM. En général, les attributs qui ne sont pas exécutables seront mis en cache. Par exemple, à partir des modèles d’exemple du blog:
>>> entry = Entry.objects.get(id=1)
>>> entry.blog # Blog object is retrieved at this point
>>> entry.blog # cached version, no DB access
Mais généralement, les attributs exécutables provoquent des accès à la base de données à chaque appel :
>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all() # query performed
>>> entry.authors.all() # query performed again
Soyez prudent dans la lecture du code des gabarits, le système des gabarits ne permet pas d’utiliser des parenthèses, mais les objets exécutables sont appelés automatiquement, masquant la distinction ci-dessus.
Soyez prudent avec vos propres propriétés personnalisées, il vous revient d’implémenter leur mise en cache au besoin, par exemple en utilisant le décorateur cached_property.
Utilisation de la balise de gabarit with¶
Pour faire usage du comportement de cache de QuerySet, il peut être nécessaire d’utiliser la balise de gabarit with.
Utilisation de iterator()¶
When you have a lot of objects, the caching behavior of the QuerySet can
cause a large amount of memory to be used. In this case,
iterator() may help.
Utilisation de explain()¶
QuerySet.explain() apporte des informations détaillées sur la façon dont la base de données exécute les requêtes, y compris les index et jointures utilisées. Ces détails peuvent aider à trouver les requêtes qui pourraient être réécrites plus efficacement ou à identifier des index qui pourraient être ajoutés pour améliorer les performances.
Travail de base de données en base de données plutôt qu’en Python¶
Par exemple :
Au niveau le plus élémentaire, utilisez filter et exclude pour filtrer au niveau de la base de données.
Utilisez les
expressions Fpour filtrer en rapport avec d’autres champs à l’intérieur du même modèle.Utilisez les annotations pour effectuer le travail d’agrégation dans la base de données.
Si ces outils ne suffisent pas à générer le code SQL nécessaire :
Utilisation de RawSQL¶
Une méthode moins portable mais plus puissante est d’utiliser l’expression RawSQL qui permet d’ajouter explicitement du code SQL dans une requête. Si cela ne suffit toujours pas :
Utilisation de SQL brut¶
Écrivez votre propre code SQL pour sélectionner des données ou remplir les modèles. Utilisez django.db.connection.queries pour examiner ce que Django écrit pour vous et considérez cela comme un point de départ.
Sélection d’objets individuels en utilisant une colonne unique et indexée¶
Il y a deux raisons d’utiliser une colonne avec unique ou db_index lorsqu’on utilise get() pour récupérer des objets individuels. Premièrement, la requête sera plus rapide en raison de l’index de base de données. De plus, la requête pourrait s’effectuer beaucoup plus lentement si plusieurs objets correspondent à la recherche ; avec une contrainte d’unicité sur la colonne, vous avez la garantie que cela ne se produira jamais.
Ainsi, en se basant sur les modèles d’exemple du blog:
>>> entry = Entry.objects.get(id=10)
sera plus rapide que :
>>> entry = Entry.objects.get(headline="News Item Title")
parce qu”id est indexé dans la base de données et que son unicité est garantie.
La ligne suivante est potentiellement très lente :
>>> entry = Entry.objects.get(headline__startswith="News")
Tout d’abord, headline n’est pas indexé, ce qui fait que la base de données sous-jacente prendra plus de temps pour trouver la ligne.
Deuxièmement, cette recherche ne garantit pas qu’un seul objet sera renvoyé. Si la requête correspond à plus d’un objet, elle va tous les sélectionner et les transférer depuis la base de données. Cette pénalité peut être conséquente si des centaines ou des milliers d’enregistrements sont renvoyés. Ce d’autant plus si la base de données est située sur un autre serveur, là où la latence et le surcoût de la liaison réseau jouent aussi un rôle.
Tout récupérer d’un coup quand on en connaît le besoin¶
Il est en général moins efficace de solliciter la base de données plusieurs fois pour différentes parties d’un ensemble de données que de tout récupérer par une seule requête quand on sait à l’avance que l’on aura besoin du tout. C’est particulièrement important lorsqu’une requête est exécutée dans une boucle et risque au final d’effectuer plusieurs requêtes en base de données alors qu’une seule n’est vraiment nécessaire. Ainsi :
Ne pas récupérer des éléments inutilement¶
Utilisation de QuerySet.values() et values_list()¶
When you only want a dict or list of values, and don’t need ORM model
objects, make appropriate usage of
values().
These can be useful for replacing model objects in template code - as long as
the dicts you supply have the same attributes as those used in the template,
you are fine.
Utilisation de QuerySet.defer() et only()¶
Use defer() and
only() if there are database columns
you know that you won’t need (or won’t need in most cases) to avoid loading
them. Note that if you do use them, the ORM will have to go and get them in
a separate query, making this a pessimization if you use it inappropriately.
N’utilisez pas trop agressivement les champs différés sans profilage car la base de données doit lire sur le disque la plupart des données non textuelles et non VARCHAR d’un enregistrement des résultats, même lorsqu’elle finit par n’exploiter que quelques colonnes. Les méthodes defer() et only() sont particulièrement adéquates lorsqu’on peut éviter de charger de grandes quantités de données textuelles ou quand la valeur de certains champs est coûteuse en terme de conversion en Python. Comme toujours, profilez d’abord, optimisez ensuite.
Utilisation de QuerySet.contains(obj)¶
…si vous voulez uniquement savoir si obj est présent dans le résultat de requête, au lieu de if obj in queryset.
Utilisation de QuerySet.count()¶
…si vous n’avez besoin que du nombre de lignes, et évitez len(queryset).
Utilisation de QuerySet.exists()¶
…si vous voulez uniquement savoir qu’il y a au moins un résultat, et évitez if queryset.
Mais :
Ne pas abuser de contains(), count() et exists()¶
Si vous allez avoir besoin de données réelles du QuerySet, évaluez-le immédiatement.
Par exemple, si on dispose d’un modèle Group ayant une relation plusieurs-à-plusieurs vers User, le code suivant est optimal :
members = group.members.all()
if display_group_members:
if members:
if current_user in members:
print("You and", len(members) - 1, "other users are members of this group.")
else:
print("There are", len(members), "members in this group.")
for member in members:
print(member.username)
else:
print("There are no members in this group.")
Il est optimal car :
Comme les objets QuerySet sont différés, aucune requête de base de données n’est exécutée si
display_group_membersvautFalse.Le stockage de
group.members.all()dans la variablememberspermet de réutiliser son résultat mis en cache.La ligne
if members:provoque l’appel àQuerySet.__bool__(), ce qui provoque aussi l’exécution de la requêtegroup.members.all()dans la base de données. S’il n’y a pas de résultats, elle renvoieFalse, sinonTrue.La ligne
if current_user in members:vérifie si l’utilisateur est dans le cache de résultats, donc aucune requête de base de données supplémentaire n’est effectuée.L’emploi de
len(members)appelleQuerySet.__len__(), réutilisant le résultat mis en cache, donc toujours sans requête supplémentaire vers la base de données.La boucle
for memberutilise les données du cache.
Au total, le code effectue une ou zéro requête de base de données. La seule optimisation explicite effectuée est l’emploi de la variable members. Si on avait utilisé QuerySet.exists() pour le if, QuerySet.contains() pour le in ou QuerySet.count() pour le nombre total, cela aurait généré à chaque fois des requêtes supplémentaires.
Utilisation de QuerySet.update() et delete()¶
Plutôt que de récupérer un ensemble d’objets, de définir certaines valeurs et de les enregistrer individuellement, utilisez une instruction SQL UPDATE groupée au moyen de QuerySet.update(). De la même façon, effectuez des suppressions groupées chaque fois que c’est possible.
Notez cependant que des méthodes de mise à jour groupées ne peuvent pas appeler les méthodes save() et delete() des instances individuelles, ce qui signifie que tout comportement spécialisé que vous auriez ajouté avec ces méthodes ne sera pas exécuté, y compris tout ce qui découle de l’utilisation des signaux attachés aux objets de base de données.
Utilisation directe de valeurs de clé étrangère¶
Si vous n’avez besoin que de la valeur d’une clé étrangère, utilisez la valeur qui est déjà stockée dans l’objet que vous manipulez, plutôt que de récupérer tout l’objet lié pour simplement disposer de sa clé primaire. Par exemple, faites :
entry.blog_id
au lieu de :
entry.blog.id
Ne pas trier les résultats sans raison¶
Ordering is not free; each field to order by is an operation the database must
perform. If a model has a default ordering (Meta.ordering) and you don’t need it, remove
it on a QuerySet by calling
order_by() with no parameters.
L’ajout d’un index à votre base de données peut aider à améliorer les performances de tri.
Utiliser les méthodes d’opérations groupées¶
Faites usage des méthodes groupées (bulk) pour réduire le nombre d’instructions SQL.
Création groupée¶
When creating objects, where possible, use the
bulk_create() method to reduce the
number of SQL queries. For example:
Entry.objects.bulk_create(
[
Entry(headline="This is a test"),
Entry(headline="This is only a test"),
]
)
…est préférable à :
Entry.objects.create(headline="This is a test")
Entry.objects.create(headline="This is only a test")
Notez qu’il existe un certain nombre de restrictions à cette méthode, il s’agit donc de s’assurer qu’elle correspond à votre cas d’utilisation.
Mise à jour groupée¶
When updating objects, where possible, use the
bulk_update() method to reduce the
number of SQL queries. Given a list or queryset of objects:
entries = Entry.objects.bulk_create(
[
Entry(headline="This is a test"),
Entry(headline="This is only a test"),
]
)
L’exemple suivant :
entries[0].headline = "This is not a test"
entries[1].headline = "This is no longer a test"
Entry.objects.bulk_update(entries, ["headline"])
…est préférable à :
entries[0].headline = "This is not a test"
entries[0].save()
entries[1].headline = "This is no longer a test"
entries[1].save()
Notez qu’il existe un certain nombre de restrictions à cette méthode, il s’agit donc de s’assurer qu’elle correspond à votre cas d’utilisation.
Insertion groupée¶
Lors de l’insertion d’objets dans des champs ManyToManyField, utilisez add() avec plusieurs objets pour réduire le nombre de requêtes SQL. Par exemple
my_band.members.add(me, my_friend)
…est préférable à :
my_band.members.add(me)
my_band.members.add(my_friend)
…où Band et Artist sont des modèles liés par une relation plusieurs-à-plusieurs.
When inserting different pairs of objects into
ManyToManyField or when the custom
through table is defined, use
bulk_create() method to reduce the
number of SQL queries. For example:
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.bulk_create(
[
PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
],
ignore_conflicts=True,
)
…est préférable à :
my_pizza.toppings.add(pepperoni)
your_pizza.toppings.add(pepperoni, mushroom)
…où Pizza et Topping ont une relation plusieurs-à-plusieurs. Notez qu’il existe un certain nombre de restrictions à cette méthode, il s’agit donc de s’assurer qu’elle correspond à votre cas d’utilisation.
Suppression groupée¶
Lors de la suppression d’objets de champs ManyToManyField, utilisez remove() avec plusieurs objets pour réduire le nombre de requêtes SQL. Par exemple
my_band.members.remove(me, my_friend)
…est préférable à :
my_band.members.remove(me)
my_band.members.remove(my_friend)
…où Band et Artist sont des modèles liés par une relation plusieurs-à-plusieurs.
Lors de la suppression de plusieurs paires d’objets de champs ManyToManyField, utilisez delete() sur une expression Q avec plusieurs instances de modèle through pour réduire le nombre de requêtes SQL. Par exemple
from django.db.models import Q
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
Q(pizza=my_pizza, topping=pepperoni)
| Q(pizza=your_pizza, topping=pepperoni)
| Q(pizza=your_pizza, topping=mushroom)
).delete()
…est préférable à :
my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)
…où Pizza et Topping sont liés par une relation plusieurs-à-plusieurs.