Evgenii Legotckoi
Evgenii Legotckoi22. Oktober 2019 01:39

Django - Tutorial 049. Optimierung der Django-Leistung mit einem echten Projekt

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.

Der erste Schritt zur Optimierung der Homepage des EVILEG-Forums

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.

Leistungsverbesserung mit Select_related

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

Using select_related and prefetch_related

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
Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Stabiles Hosting des sozialen Netzwerks EVILEG. Wir empfehlen VDS-Hosting für Django-Projekte.

Magst du es? In sozialen Netzwerken teilen!

Руслан Волшебник
  • 24. Oktober 2019 11:37

Спасибо. Хорошая статья.

Я нашёл 2 опечатки. Выделил жирным.

prefetch_related
prefetch_related отличается тем, что позволяет подгрузить не только объекты, которые используются в ForeignKey полях модели, но и те объекты, модели...
Должно вроде быть "но и те ".

Дополнительная оптимизация
...То есть у вас может быть ситуация, когда лучше выполнить ещё пару дополнительных запросов, через перегружать join операциями основной запрос.
Тут видимо имелось ввиду чем .

Спасибо, поправил

p
  • 25. November 2019 09:11

Стоило бы упомянуть про Prefetch объекты со специально сформированными querysetами. Про кеширование. Помимо only есть defer. В некоторых случаях в drf можно автоматически делать select/prefetch_related. И запросы можно смотреть в django_debug_toolbar или в shell_plus --print-sql

Evgenii Legotckoi
  • 25. November 2019 09:21

Стоило бы упомянуть про Prefetch объекты со специально сформированными querysetами

Вы про это?

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'
    )

В некоторых случаях в drf можно автоматически делать select/prefetch_related

Для drf можно сделать отдельную статью, я вообще не рассматривал в данной статье drf

И запросы можно смотреть в django_debug_toolbar или в shell_plus --print-sql

В качестве альтернативы

D
  • 2. März 2021 02:48

Огромное спасибо вам за статью! Для меня стали открытием select_related и prefetch_related

Kommentare

Nur autorisierte Benutzer können Kommentare posten.
Bitte Anmelden oder Registrieren
Letzte Kommentare
ИМ
Игорь Максимов5. Oktober 2024 07:51
Django – Lektion 064. So schreiben Sie eine Python-Markdown-Erweiterung Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55. Juli 2024 11:02
QML - Lektion 016. SQLite-Datenbank und das Arbeiten damit in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssr8. Februar 2024 18:43
Qt Linux - Lektion 001. Autorun Qt-Anwendung unter Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lektion 007. Arbeiten mit ICMP-Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25. Dezember 2023 10:30
Boost - statisches Verknüpfen im CMake-Projekt unter Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
Jetzt im Forum diskutieren
J
JacobFib17. Oktober 2024 03:27
добавить qlineseries в функции Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты https://topdecorpro.ru…
JW
Jhon Wick1. Oktober 2024 15:52
Indian Food Restaurant In Columbus OH| Layla’s Kitchen Indian Restaurant If you're looking for a truly authentic https://www.laylaskitchenrestaurantohio.com/ , Layla’s Kitchen Indian Restaurant is your go-to destination. Located at 6152 Cleveland Ave, Colu…
КГ
Кирилл Гусарев27. September 2024 09:09
Не запускается программа на Qt: точка входа в процедуру не найдена в библиотеке DLL Написал программу на C++ Qt в Qt Creator, сбилдил Release с помощью MinGW 64-bit, бинарнику напихал dll-ки с помощью windeployqt.exe. При попытке запуска моей сбилженной программы выдаёт три оши…
F
Fynjy22. Juli 2024 04:15
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …

Folgen Sie uns in sozialen Netzwerken