Django ORM

Obwohl ORMs für Entwickler sehr nützlich sind, hat die Abstraktion des Zugriffs auf eine Datenbank ihren Preis. Entwickler, die tief in die Datenbank eintauchen möchten, werden feststellen, dass einige Dinge einfacher hätten gemacht werden können.

Dieser Artikel wurde von unseren Erfahrungen inspiriert, die Datenbanknutzung zu optimieren, um die Ladezeiten der Seiten zu verbessern. Um das bestmögliche Ergebnis zu erzielen, mussten wir viele Fehler machen und beschlossen, diese zu teilen. Dieser Artikel wird nützlich für Sie sein, wenn Sie planen, ein Projekt zu entwickeln oder zu unterstützen, das mit Django geschrieben wurde.

Verwenden Sie model.fk_id anstelle von model.fk.id

Auf den ersten Blick scheint diese Methode absolut nutzlos zu sein. Was können Sie über das Objekt nur anhand seiner ID herausfinden? Tatsächlich können alle Arten von Filter- oder Mapping- und anderen Lambda-Funktionen nicht ohne sie auskommen.

Zum Beispiel müssen Sie alle Ereignisse aus demselben Laden wie unser Ereignis finden. Das Erste, was einem in den Sinn kommt, ist eine einfache Anfrage zu verwenden:

Alles scheint einfach und problemlos, aber wenn wir unsere Anfragen im DB-Log überprüfen, werden wir dort 2 Anfragen sehen. In der ersten Anfrage erhalten Sie das Store-Objekt, benötigen jedoch keine Daten über den Store außer seiner ID. Um dies zu beheben, müssen Sie direkt auf den Fremdschlüssel zugreifen. In diesem Fall sieht der Code folgendermaßen aus:

Schließlich gibt es keine weiteren Anfragen nach Informationen über den Store und Ihre Abfrage wird nur eine Anfrage an die Datenbank stellen.

Abrufen verwandter Objekte mit den Methoden select_related und prefetch_related

Stellen Sie sich vor, Sie müssen alle Ereignisse aus der Datenbank abrufen und diese dann zusammen mit den Stores und der Liste der Marken für jedes Ereignis in das Template einfügen. Implementieren wir die Ansicht mit ListView:

Im Template zeigen Sie die Informationen über das Ereignis, den Store und die Marken an:

In diesem Fall erhalten Sie zuerst alle Ereignisse mit einer einzigen SQL-Abfrage und dann werden für jedes dieser Ereignisse Store und Marken separat abgefragt. Sie müssen Django dazu zwingen, all diese Daten mit einer geringeren Anzahl von Abfragen abzurufen.

Beginnen wir mit dem Abrufen der Stores. Damit das QuerySet die Daten zu bestimmten Fremdschlüsseln im Voraus erhält, gibt es die Methode select_related method. Aktualisieren Sie das QuerySet in Ihrer Ansicht, um diese Methode zu verwenden:

Die Methode select_related () gibt ein QuerySet zurück, das automatisch Daten aus verwandten Objekten in die Abfrage einbezieht, wenn die Abfrage ausgeführt wird. Der Zugriff auf verwandte Objekte über das Modell erfordert keine zusätzlichen Datenbankabfragen. Es ist praktisch für „one-to-many“ oder „one-to-one“ Beziehungen zu verwenden.

Die Methode select_related funktioniert nur mit Fremdschlüsseln im aktuellen Modell. Um die Anzahl der Anfragen beim Abrufen einer Menge verwandter Objekte (wie Marken in unserem Beispiel) zu reduzieren, müssen Sie die Methode prefetch_related verwenden.

Aktualisieren Sie erneut das QuerySet-Attribut der EventListView-Klasse:

Die Methode prefetch_related () gibt ein QuerySet zurück, das verwandte Objekte für jeden der angegebenen Suchparameter in einem Ansatz erhält.

Einschränkung der Felder in Auswahlen (defer, only)

Wenn Sie die SQL-Abfragen aus dem vorherigen Beispiel genauer betrachten, werden Sie feststellen, dass Sie mehr Felder erhalten, als Sie benötigen. Sie erhalten alle Felder des Stores und der Marken, einschließlich einer großen Beschreibung des Ereignisses. Sie können die Menge der übertragenen Daten erheblich reduzieren, indem Sie die Methode defer verwenden, die es Ihnen ermöglicht, den Empfang bestimmter Felder zu verzögern. Wenn der Code dennoch auf ein solches Feld zugreift, wird Django eine zusätzliche Anfrage stellen, um es zu erhalten. Fügen Sie einen Aufruf der Methode defer im QuerySet hinzu:

Jetzt wird das unnötige ‘description’-Feld nicht mehr abgefragt, was die Verarbeitungszeit der Anfrage reduziert.

Dennoch erhalten Sie viele Event-Felder, die Sie nicht verwenden. Es wäre einfacher, nur die Felder anzugeben, die Sie wirklich benötigen. Dafür gibt es die Methode ‘only’, vor der die Feldnamen übertragen werden und die verbleibenden Felder beiseite gelegt werden:

Die Methoden, defer() und only() erfüllen dieselbe Aufgabe, indem sie die Felder in den Abfragen einschränken. Der einzige Unterschied ist:

  • defer() verzögert das Abrufen der als Argumente übergebenen Felder,
  • only() verzögert das Abrufen aller Felder außer der übergebenen.

Verwenden Sie niemals len(queryset)

Wenn Sie die Anzahl der QuerySet-Objekte ermitteln müssen, verwenden Sie nicht die Methode len() . Die Methode count() ist dafür viel besser geeignet.

Zum Beispiel, wenn Sie die Gesamtzahl aller Events erhalten möchten, wäre der falsche Weg:

In diesem Fall wird zuerst die Abfrage zum Abrufen aller Daten aus der Tabelle durchgeführt, dann in ein Python-Objekt umgewandelt, und die Länge dieses Objekts wird mit der Methode len() ermittelt. Natürlich ist dies nicht die beste Option, und es wäre ausreichend, nur eine Zahl aus der Datenbank zu erhalten — die Anzahl der Events.

Verwenden Sie hierfür die Methode count():

Mit count() wird eine einfachere Abfrage in der Datenbank durchgeführt und es werden weniger Ressourcen für die Ausführung des Python-Codes benötigt.

Verwenden Sie niemals if queryset

Wenn Sie überprüfen müssen, ob das Ergebnis des QuerySet existiert, verwenden Sie QuerySet nicht als booleschen Wert oder queryset.count() > 0. Verwenden Sie stattdessen queryset.exists().

Die Methode exists() gibt True zurück, wenn das QuerySet irgendwelche Ergebnisse enthält, und False, wenn nicht. Sie versucht, die Abfrage auf die einfachste und schnellste Weise durchzuführen, führt aber nahezu dieselbe Abfrage wie eine normale QuerySet-Abfrage aus.

Exists() ist nützlich für Suchen, die sowohl die Objektmitgliedschaft in einem QuerySet als auch das Vorhandensein von Objekten in einem QuerySet betreffen, insbesondere im Kontext eines großen QuerySet.

Datenbankindizes

Stellen Sie sicher, dass die Felder, nach denen Sie suchen, indiziert sind. Verwenden Sie den Feldparameter db_index = True in Ihrem Modell.

Der Index wird im B-Baum gespeichert, sodass das Objekt in logarithmischer Zeit gefunden wird – O(log(n)). Wenn Sie eine Milliarde Elemente hätten, würde eine Objektsuche so lange dauern wie eine lineare Suche von 30 Elementen.

Wenn Sie db_index nicht verwenden, führt die Suche zu einem Tabellenscan. Stellen Sie sich ein Wörterbuch vor, in dem die Wörter völlig durcheinander sind und die einzige Möglichkeit, das Wort zu finden, darin besteht, alle Seiten nacheinander umzublättern. Sie können sich vorstellen, wie viel Zeit es in Anspruch nehmen würde, wenn Sie eine Milliarde Elemente ohne Indizes hätten und die oben genannte Abfrage ausführen würden.

Indizes sind nicht nur beim Filtern von Daten nützlich, sondern auch beim Sortieren. Außerdem erlauben viele DBMSs das Erstellen von Indizes auf mehreren Feldern, was nützlich ist, wenn Sie Daten nach einer Reihe von Feldern filtern. Wir empfehlen Ihnen, die Dokumentation für Ihr DBMS für weitere Details zu lesen.

Massenoperationen

a. Masseninsertion mit der Methode bulk_create

Angenommen, Ihre neue Django-Anwendung ersetzt die alte Anwendung und Sie müssen Daten über Benutzer in neue Modelle übertragen. Sie haben Daten aus der alten Anwendung in große JSON-Dateien exportiert.

Die Datei mit den Benutzern hat die folgende Struktur:

Lassen Sie uns eine Methode zum Importieren von Benutzern aus der JSON-Datei in die Datenbank erstellen:

Überprüfen Sie, wie viele SQL-Abfragen ausgeführt werden, wenn 200 Benutzer geladen werden, und sehen Sie, dass Sie 200 Abfragen abgeschlossen haben. Das bedeutet, dass für jeden Benutzer eine separate INSERT-SQL-Abfrage ausgeführt wird. Wenn Sie eine große Menge an Daten haben, kann dieser Ansatz sehr langsam sein. Verwenden wir die Methode bulk_create des User-Modellmanagers:

Nach dem Aufrufen der Methode werden Sie sehen, dass eine große Abfrage an die Datenbank für alle Benutzer ausgeführt wurde.

Wenn Sie wirklich eine große Menge an Daten einfügen müssen, müssen Sie diese möglicherweise in mehrere Abfragen aufteilen. Dafür gibt es den Parameter batch_size für die Methode bulk_create, der die maximale Anzahl von Objekten angibt, die in einer Anfrage eingefügt werden. Wenn Sie also 200 Objekte haben und bulk_size = 50 angeben, erhalten Sie 4 Anfragen

Die Methode bulk_size hat eine Reihe von Einschränkungen, die Sie in der Dokumentation nachlesen können.

b. Masseninsertion in M2M-Beziehung

In diesem Fall müssen Sie Ereignisse und Marken in die Datenbank einfügen, die sich in einer separaten JSON-Datei mit der folgenden Struktur befinden:

Die Methode dafür sieht wie folgt aus:

Durch Aufrufen der Methode erhalten wir eine enorme Menge an SQL-Abfragen!

Der Grund ist, dass das Hinzufügen jeder Marke zu einem Artikel durch eine separate Abfrage erfolgt. Es kann verbessert werden, indem eine Liste von Marken an die Methode the event.brands.add übergeben wird:

Diese Option sendet fast 2-mal weniger Anfragen, was ein gutes Ergebnis ist, wenn man bedenkt, dass Sie nur ein paar Zeilen Code geändert haben.

c. Massenaktualisierung

Nach der Datenübertragung könnten Sie auf die Idee kommen, dass alte Ereignisse (vor 2018) inaktiv gemacht werden sollten. Dafür wurde dem Event-Modell ein boolesches Feld ‘active’ hinzugefügt und Sie müssen dessen Wert setzen:

Durch das Ausführen dieses Codes erhalten Sie eine Anzahl von Anfragen, die der Anzahl der alten Ereignisse entspricht.

Außerdem gibt es für jedes Ereignis, das zur Bedingung passt, eine separate SQL-Abfrage, und alle Felder dieser Ereignisse werden überschrieben.

Dies kann dazu führen, dass Änderungen, die zwischen SELECT- und UPDATE-Abfragen vorgenommen wurden, überschrieben werden. Neben Leistungsproblemen erhalten Sie auch ein Race Condition.

Stattdessen können Sie die update-Methode verwenden, die für QuerySet-Objekte verfügbar ist:

Dieser Code erzeugt nur eine SQL-Abfrage. Fantastisch!

d. Massenlöschung

Als nächstes müssen inaktive Ereignisse gelöscht werden.

Der Code generiert eine 2N + 1-Abfrage an die Datenbank.

Zuerst wird die Verbindung zwischen dem Ereignis und der Marke in der Zwischentabelle gelöscht, und dann das Ereignis selbst. Sie können dies mit weniger Abfragen tun, indem Sie die delete-Methode der QuerySet-Klasse verwenden:

Dieser Code erledigt dasselbe in nur 3 Abfragen an die Datenbank.

Zuerst erhält eine einzelne Anfrage eine Liste der Identifikatoren aller inaktiven Ereignisse, dann löscht die zweite Anfrage alle Verbindungen zwischen Ereignissen und Marken auf einmal, und die letzte Anfrage löscht die Ereignisse.

Klasse!

Verwendung von Iterator

Angenommen, Sie müssen die Möglichkeit hinzufügen, Ereignisse in das CSV-Format zu exportieren. Lassen Sie uns eine einfache Methode dafür erstellen, wobei wir nur die Arbeit mit der Datenbank berücksichtigen:

Um diesen Befehl zu testen, wurden etwa 100.000 Ereignisse generiert. Beim Ausführen des Befehls durch den Speicherprofiler wurde festgestellt, dass der Befehl etwa 200 MB Speicher verwendet, da beim Ausführen der Abfrage das QuerySet alle Ereignisse auf einmal aus der Datenbank abruft und im Speicher zwischenspeichert, sodass nachfolgende Abfragen an dieses QuerySet keine zusätzlichen Abfragen ausführen würden. Sie können die Menge des verwendeten Speichers reduzieren, indem Sie die Methode iterator() der QuerySet-Klasse verwenden, die es ermöglicht, Ergebnisse einzeln mit dem serverseitigen Cursor abzurufen und gleichzeitig das Zwischenspeichern der Ergebnisse im QuerySet zu deaktivieren:

Beim Ausführen des aktualisierten Beispiels im Profiler verwendet das Team nur ~40 MB. Außerdem verwendet der Befehl bei jeder Datenmenge beim Verwenden des Iterators eine konstante Menge an Speicher.

Schlussfolgerungen

  1. Wenn Sie nur einen Fremdschlüsselwert benötigen, verwenden Sie den Fremdschlüsselwert, der bereits auf dem Objekt vorhanden ist, anstatt das gesamte zugehörige Objekt abzurufen und dessen Primärschlüssel zu verwenden.
  2. Verwenden Sie select_related für Fremdschlüssel im aktuellen Modell. Um M2M-Objekte und Objekte aus Modellen, die sich auf das aktuelle beziehen, zu erhalten, verwenden Sie prefetch_related.
  3. Verwenden Sie defer() und only(), um die Anzahl der Felder zu begrenzen, die Sie aus der Datenbank abrufen.
  4. Um die Gesamtanzahl der Objekte in der Datenbank zu erhalten, die dem QuerySet entsprechen, verwenden Sie die Methode count().
  5. Verwenden Sie die Methode exists(), wenn Sie nur feststellen möchten, ob mindestens ein Ergebnis existiert.
  6. Stellen Sie sicher, dass die Felder, nach denen Sie suchen, indiziert sind.
  7. Vergessen Sie nicht, Bulk-Methoden zu verwenden, wenn Sie mehrere Objekte gleichzeitig erstellen/aktualisieren/löschen.
  8. Der Iterator kann die RAM-Nutzung optimieren, wenn Sie mit großen QuerySets arbeiten.

Sehen Sie, wie wir einen B2B-Marktplatz mit Django umgestaltet und weiterentwickelt haben

Bitte geben Sie Ihre Geschäfts-E-Mail-Adresse ein
See how we enhanced an open-source solution for censorship resistance on the web