In letzter Zeit habe ich viel Zeit damit verbracht, die Website zu optimieren, und jetzt möchte ich darüber sprechen.
Dieser Artikel erläutert die Verwendung der Methoden
select_related
und
prefetch_related
in QuerySet und ihre Unterschiede. Ich werde auch versuchen zu erklären, warum Django als langsam gilt und warum es immer noch nicht ist. Natürlich ist Django viel langsamer als Flask, aber in den meisten Projekten liegt das Problem nicht in Django selbst, sondern in der fehlenden Datenbankabfrageoptimierung.
Optimieren wir daher die Forumseite der EVILEG-Website . Und dabei hilft uns die Django-Silk-Batterie, die dazu dient, die Anzahl der Anfragen an die Datenbank sowie deren Dauer zu messen.
Installieren und konfigurieren Sie Django Silk
Installieren Sie Django Silk
pip install django-silk
Fügen Sie es zu INSTALLED_APPS hinzu
INSTALLED_APPS = [ ... 'silk' ]
und fügen Sie MIDDLWARE hinzu
MIDDLEWARE = [ ... 'silk.middleware.SilkyMiddleware', ... ]
Sie müssen auch die URLs von django-silk hinzufügen, damit Sie Anforderungsstatistiken anzeigen können.
from django.urls import path, include urlpatterns = [ path('silk/', include('silk.urls', namespace='silk')) ]
Und der letzte Schritt besteht darin, die Django-Silk-Migration anzuwenden.
python manage.py migrate
Hinweise zu Django Silk
Verwenden Sie Django Silk nicht auf einem Produktionsserver. Zumindest mit den in diesem Artikel gezeigten Einstellungen. Wenn Sie bereits einen guten Traffic auf der Website haben, beispielsweise 1400 Personen pro Tag, wird Django Silk mit diesen Einstellungen einfach alle Ihre Ressourcen auffressen. Experimentieren Sie daher nur auf dem Entwicklungsserver.
Optimierung
Lassen Sie uns zunächst sehen, wie schlecht es um Datenbankabfragen auf der Hauptseite des Forums steht. Sehen wir uns zur Verdeutlichung an, wie diese Seite aussieht.
Dazu müssen wir nur die Seite herunterladen, an der wir interessiert sind, und die Abfragestatistiken in Django Silk anzeigen.
Vorerst werden alle Anfragen im Debug-Modus angezeigt.
Die Situation ist deprimierend, denn das Laden der Hauptseite des Forums hat:
- 325 Abfragen an die Datenbank
- verbrachte 155 ms
- 568 ms ganze Seite
Dies ist eine sehr zeitaufwändige Aufgabe, zumal für jede Anfrage eine Verbindung zur Datenbank aufgebaut wird und dann auch alle notwendigen Daten in Objekte geladen werden müssen.
Der Ressourcenverbrauch ist enorm. Ich denke, das ist einer der Gründe, warum viele Leute denken, Django sei langsam, aber in Wirklichkeit haben sie einfach nicht verstanden, wie man Datenbankabfragen abstimmt und optimiert.
Optimierung
Mal sehen, wie das ursprüngliche QuerySet für die Hauptseite des Forums aussieht.
def get_queryset(self): return ForumTopic.objects.all()
1 493 / 5 000
Übersetzungsergebnisse
Wie Sie sehen können, nichts Kompliziertes. Dies sind die Anforderungen, die normalerweise ganz am Anfang der Verwendung des Django-ORM geschrieben werden. Und erst dann beginnen Fragen, wie man die Leistung von Django optimieren kann.
Dies liegt daran, dass der anfängliche Abfragesatz, der zum Rendern dieser Seite benötigt wird, nur ForumTopic -Objekte aus der Datenbank übernimmt, aber keine anderen Objekte, die den ForeignKey -Feldern des ForumTopic -Datenmodells hinzugefügt werden. Daher ist Django gezwungen, alle extrem großen Objekte automatisch zu laden, wenn sie benötigt werden. Aber der Programmierer weiß, was für jede einzelne Seite erforderlich ist, und kann Django mit einer Anfrage alle riesigen Objekte mitteilen, die im Voraus abgeholt werden müssen. Lassen Sie uns dies mit select_related tun.
select_related
Mit dieser Methode können Sie zusätzliche Objekte aus anderen Tabellen in einer Abfrage sammeln. Auf diese Weise können Sie viele Abfragen zu einer zusammenfassen und die Auswahl beschleunigen sowie den Aufwand für die Verbindung zur Datenbank reduzieren, da die Anzahl der Verbindungen bereits stark reduziert ist.
Lassen Sie uns versuchen, einige Daten in einer Abfrage mit selected_related auszuwählen. Ich weiß, dass für mein ForumTopic -Modell die folgenden Felder als zugehörig ausgewählt werden können:
- Artikel - Forumsartikel
- Antwort - Antwort, ein Beitrag, der in einem Forenthread als beantwortet markiert wurde
- Abschnitt - der Abschnitt, in dem die Frage gestellt wurde
- Benutzer - der Benutzer, der diese Frage gestellt hat
Die ursprüngliche Datenbankabfrage kann wie folgt modifiziert werden:
def get_queryset(self, **kwargs): return ForumTopic.objects.all().select_related('article', 'answer', 'section', 'user')
Dann schauen Sie sich das Ergebnis in Django Silk an.
Die Situation bei der Zahl der Anfragen ist besser geworden
- 256 Anfragen an die Datenbank
- Zeitaufwand 131 ms
- 444ms ganze Seite
Die folgende Abbildung zeigt eine Zeile mit einer neuen Abfrage, die über 4 Join-Vorgänge verfügt.
Wie Sie sehen können, betrug die Dauer dieser Anfrage 19,225 ms .
Schon ein gutes Ergebnis. Aber ich weiß mit Sicherheit, dass dies nicht die Grenze ist. Tatsache ist, dass die Struktur der Hauptseite des Forums ziemlich komplex ist und die Anzahl der Beiträge in jedem Thema, den letzten Beitrag, einen Link zur Antwort auf die Lösung sowie eine Anfrage an das Benutzerprofil angibt . für Antworten. Und jetzt ist die Methode prefetch_related an der Reihe.
prefetch_related
prefetch_related unterscheidet sich darin, dass Sie nicht nur Objekte laden können, die in den ForeignKey -Feldern des Modells verwendet werden, sondern auch solche Objekte, deren Modelle das ForeignKey -Feld im Modell haben ist an den Hauptdatenbankanforderungsdaten beteiligt. Das heißt, Sie können Nachrichten mit einer separaten Anfrage in ein Thema laden. In dieser Situation möchte ich die folgenden Felder laden.
- Kommentare sind Beiträge im Thema, ForumPost-Modell
- comments__user - Fremdschlüssel des Benutzers, der die Nachricht hinterlassen hat
- answer___parent – Der Fremdschlüssel von ForumTopic ist eine Antwort, die mit Themenerlaubnis markiert ist. Theoretisch wäre es möglich, dieses Objekt über select_related auszuwählen, aber die Abfragestruktur wurde sehr komplex, was eine effiziente Verwendung von select_related nicht zuließ. Ja, ja, die Verwendung dieser Methode sollte vernünftig sein. Die Leistung verbessert sich natürlich, aber manchmal ist es besser, einige Daten in einer separaten Abfrage zu sammeln.
Dann sieht die Datenbankabfrage so aus:
def get_queryset(self, **kwargs): return ForumTopic.objects.all().select_related('article', 'answer', 'section', 'user').prefetch_related( 'comments', 'comments__user', 'answer___parent' )
Und in Django Silk erhalte ich folgendes Ergebnis
Als Ergebnis haben wir folgendes:
- 6 Abfragen an die Datenbank
- verbraucht für 26 ms
- 148 ms ganze Seite
Es ist einfach ein großartiges Ergebnis, das erreicht werden kann. Gleichzeitig hat der Nutzer bereits das Gefühl, dass die Seite sehr schnell lädt.
Aber das ist noch nicht alles, beachten Sie, dass die Anfrage, die 4 Join-Operationen hat, immer noch im Bereich von 17–20 ms liegt. Können wir etwas dagegen tun? Natürlich können wir das, und dazu müssen wir die Methode only verwenden.
nur
Die only -Methode ermöglicht es uns, nur die Spalten auszuwählen, die wir zum Anzeigen der Seite benötigen. In diesem Fall müssen jedoch alle erforderlichen Spalten berücksichtigt werden, da sonst jede fehlende Spalte von Django in einer separaten Abfrage erfasst wird.
Also habe ich die folgende Datenbankabfrage geschrieben
def get_queryset(self, **kwargs): return ForumTopic.objects.all().select_related('article', 'answer', 'section', 'user').prefetch_related( 'comments', 'comments__user', 'answer___parent' ).only( 'user__first_name', 'user__last_name', 'section__title', 'section__title_ru', 'article__title', 'article__title_ru' )
Und kam zu folgendem Ergebnis
- 6 Abfragen an die Datenbank
- verbraucht für 20 ms
- 136 ms ganze Seite
Natürlich gebe ich die bestmöglichen Ergebnisse, da es immer einige Hinweise in den Messungen gibt, aber wenn Sie den Screenshot analysieren, können Sie sehen, dass die Anfragedauer von 17-19ms auf 11- 13ms . Abgesehen davon, dass nur die erforderlichen Volumina und der Verbrauch abgetastet werden, wenn beispielsweise sehr große Arrays von Textdaten aus der Datenbank entnommen werden, werden sie beim Rendern von Seiten nicht verwendet.
Lassen Sie uns nun ein wenig mit den Abfragen select_related und prefetch_related spielen.
Zusätzliche Optimierung
Wenn Sie bis zu diesem Punkt gelesen haben, haben Sie, glaube ich, gesehen, dass die Verwendung von select_related eine großartige Möglichkeit ist, Datenbankabfragen zu optimieren. Aber es gibt ein ABER . Bei der Verwendung der Paginator -Klasse, die auf meiner Seite verwendet wird, können einige Probleme auftreten. Fakt ist aber, dass für Paginator die Zählabfrage ausgeführt werden muss, um die korrekte Seitenzahl zu berechnen. Und wenn die Anfrage sehr komplex ist, dann kann die Dauer der Anfrage zum Zählen ziemlich lang sein und der Ausführung einer normalen Anfrage entsprechen. Daher kann das Schreiben einer schnellen und effizienten Hauptabfrage eine wichtige Bedingung sein, und alle anderen Objekte werden besser mit prefetch_related geladen. Das heißt, Sie haben möglicherweise eine Situation, in der es besser ist, ein paar zusätzliche Abfragen durchzuführen, indem Sie die Join -Operationen mit der Hauptabfrage überladen.
Und ich habe eine solche Anfrage in ORM für diese Seite geschrieben
def get_queryset(self, **kwargs): return ForumTopic.objects.all().select_related('answer').prefetch_related( Prefetch('article', queryset=Article.objects.all().only('title', 'title_ru')), Prefetch('section', queryset=ForumSection.objects.all().only('slug', 'title', 'title_ru')), Prefetch('user', queryset=User.objects.all().only('username', 'first_name', 'last_name')), Prefetch('comments', queryset=ForumPost.objects.all().select_related('user').only( 'user__username', 'user__first_name', 'user__last_name', '_parent_id' )), Prefetch('answer___parent', queryset=ForumTopic.objects.all().only('id')) ).only( 'title', 'user_id', 'section_id', 'article_id', 'answer___parent_id', 'pub_date', 'lastmod', 'attachment' )
Dabei kam ich zu folgendem Leistungsergebnis
- 8 Abfragen an die Datenbank
- verbrachte 14 ms
- 141 ms ganze Seite
Natürlich können wir sagen, dass es in diesem Fall keinen sehr großen Gewinn gibt. Darüber hinaus ist die Gesamt-Download-Geschwindigkeit sogar etwas gesunken (5 ms), und es gab 2 weitere Abfragen an die Datenbank, aber gleichzeitig habe ich eine 42-prozentige Steigerung der Abfrageleistung erhalten, und das schon etwas Wertvolles. Wenn Ihre Site also sehr lange Abfragen hat, die für die Paginierung verwendet werden und eine große Anzahl von Joins haben, könnte es sich lohnen, die Verwendung von select_related in prefetch_related umzuschreiben. Es kann tatsächlich dazu beitragen, Ihre Django-Site viel schneller zu machen.
Fazit
- Verwenden Sie select_related , um verwandte Felder aus anderen Tabellen gleichzeitig mit der Hauptabfrage auszuwählen
- Verwenden Sie prefetch_related , um zusätzlich in einer Abfrage alle anderen Modellobjekte zu laden, die einen ForeignKey in Ihrem Hauptabfragesatz haben.
- Verwenden Sie only , um die Anzahl der abzurufenden Spalten zu begrenzen. Dies beschleunigt auch Abfragen und reduziert den Speicherverbrauch.
- Wenn Sie Paginator verwenden, stellen Sie sicher, dass die Hauptabfrage keine sehr umfangreiche count -Abfrage generiert, da es sonst möglich ist, dass einige select_related -Abfragen als prefetch_related geladen werden
Спасибо. Хорошая статья.
Я нашёл 2 опечатки. Выделил жирным.
prefetch_related
prefetch_related отличается тем, что позволяет подгрузить не только объекты, которые используются в ForeignKey полях модели, но и те объекты, модели...
Должно вроде быть "но и те ".
Дополнительная оптимизация
...То есть у вас может быть ситуация, когда лучше выполнить ещё пару дополнительных запросов, через перегружать join операциями основной запрос.
Тут видимо имелось ввиду чем .
Спасибо, поправил
Стоило бы упомянуть про Prefetch объекты со специально сформированными querysetами. Про кеширование. Помимо only есть defer. В некоторых случаях в drf можно автоматически делать select/prefetch_related. И запросы можно смотреть в django_debug_toolbar или в shell_plus --print-sql
Вы про это?
Для drf можно сделать отдельную статью, я вообще не рассматривал в данной статье drf
В качестве альтернативы
Огромное спасибо вам за статью! Для меня стали открытием select_related и prefetch_related