Ich habe eine MySQL-Tabelle mit ~ 10 Millionen Datensätzen, mit der ich über SqlAlchemy zusammenarbeite. Ich habe festgestellt, dass Abfragen in großen Teilmengen dieser Tabelle zu viel Speicher verbrauchen, obwohl ich dachte, ich verwende einen eingebauten Generator, der intelligent mundgerechte Teile des Datensatzes abruft:
for thing in session.query(Things):
analyze(thing)
Um dies zu vermeiden, muss ich meinen eigenen Iterator erstellen, der in Stücken abbeißt:
lastThingID = None
while True:
things = query.filter(Thing.id < lastThingID).limit(querySize).all()
if not rows or len(rows) == 0:
break
for thing in things:
lastThingID = row.id
analyze(thing)
Ist das normal oder fehlt mir etwas an eingebauten SA-Generatoren?
Die Antwort auf diese Frage scheint darauf hinzudeuten, dass der Speicherverbrauch nicht zu erwarten ist.
python
mysql
sqlalchemy
Paul
quelle
quelle
Antworten:
Die meisten DBAPI-Implementierungen puffern Zeilen beim Abrufen vollständig. In der Regel befindet sich die gesamte Ergebnismenge im Speicher, bevor das SQLAlchemy-ORM überhaupt ein Ergebnis erhält.
Die Funktionsweise
Query
besteht jedoch darin, dass die angegebene Ergebnismenge standardmäßig vollständig geladen wird, bevor Ihre Objekte an Sie zurückgegeben werden. Die Begründung bezieht sich hier auf Abfragen, die mehr als einfache SELECT-Anweisungen sind. Beispielsweise muss bei Verknüpfungen mit anderen Tabellen, die möglicherweise dieselbe Objektidentität mehrmals in einer Ergebnismenge zurückgeben (häufig beim eifrigen Laden), der gesamte Satz von Zeilen im Speicher gespeichert werden, damit die korrekten Ergebnisse zurückgegeben werden können, andernfalls Sammlungen und dergleichen möglicherweise nur teilweise besiedelt.Bietet also
Query
eine Möglichkeit, dieses Verhalten durch zu ändernyield_per()
. Dieser Aufruf bewirkt, dass dieQuery
Zeilen in Stapeln ausgegeben werden, in denen Sie die Stapelgröße angeben. Wie in den Dokumenten angegeben, ist dies nur dann angebracht, wenn Sie keine eifrigen Sammlungen laden. Wenn Sie also wirklich wissen, was Sie tun. Wenn das zugrunde liegende DBAPI Zeilen vorpuffert, bleibt der Speicheraufwand bestehen, sodass der Ansatz nur geringfügig besser skaliert als nicht verwendet wird.Ich benutze es kaum
yield_per()
; Stattdessen verwende ich eine bessere Version des oben vorgeschlagenen LIMIT-Ansatzes mit Fensterfunktionen. LIMIT und OFFSET haben das große Problem, dass sehr große OFFSET-Werte dazu führen, dass die Abfrage immer langsamer wird, da ein OFFSET von N dazu führt, dass sie durch N Zeilen blättert - es ist, als würde sie jedes Mal fünfzig Mal eine Abfrage ausführen, anstatt eine immer größere Anzahl von Zeilen. Bei einem Fensterfunktionsansatz rufe ich eine Reihe von "Fenster" -Werten vorab ab, die sich auf Teile der Tabelle beziehen, die ich auswählen möchte. Ich gebe dann einzelne SELECT-Anweisungen aus, die jeweils aus einem dieser Fenster gleichzeitig abgerufen werden.Der Fensterfunktionsansatz befindet sich im Wiki und ich benutze ihn mit großem Erfolg.
Beachten Sie auch: Nicht alle Datenbanken unterstützen Fensterfunktionen. Sie benötigen Postgresql, Oracle oder SQL Server. IMHO mit mindestens Postgresql lohnt sich auf jeden Fall - wenn Sie eine relationale Datenbank verwenden, können Sie auch die beste verwenden.
quelle
Ich bin kein Datenbankexperte, aber wenn ich SQLAlchemy als einfache Python-Abstraktionsschicht verwende (dh nicht das ORM-Abfrageobjekt verwende), habe ich eine zufriedenstellende Lösung gefunden, um eine 300-Zeilen-Tabelle abzufragen, ohne die Speichernutzung zu explodieren ...
Hier ist ein Dummy-Beispiel:
Dann verwende ich die SQLAlchemy-
fetchmany()
Methode, um die Ergebnisse in einer Endlosschleife zuwhile
durchlaufen:Mit dieser Methode konnte ich alle Arten von Datenaggregationen ohne gefährlichen Speicheraufwand durchführen.
NOTE
Dasstream_results
funktioniert mit Postgres und dempyscopg2
Adapter, aber ich denke, es funktioniert weder mit DBAPI noch mit einem Datenbanktreiber ...In diesem Blog-Beitrag gibt es einen interessanten Anwendungsfall , der meine obige Methode inspiriert hat.
quelle
pymysql
) arbeitet, sollte dies meiner Meinung nach die akzeptierte Antwort sein.Ich habe mich mit effizientem Durchlaufen / Paging mit SQLAlchemy befasst und möchte diese Antwort aktualisieren.
Ich denke, Sie können den Slice-Aufruf verwenden, um den Umfang einer Abfrage richtig einzuschränken, und Sie können ihn effizient wiederverwenden.
Beispiel:
quelle
.all()
notwendig ist. Ich stelle fest, dass sich die Geschwindigkeit nach dem ersten Anruf stark verbessert hat..all()
die Dinge Variable ist eine Abfrage, die len () nicht unterstütztIm Geiste von Joels Antwort verwende ich Folgendes:
quelle
Die Verwendung von LIMIT / OFFSET ist schlecht, da Sie zuvor alle {OFFSET} -Spalten finden müssen. Je größer OFFSET ist, desto länger wird die Anforderung. Die Verwendung einer Fensterabfrage für mich führt auch bei einer großen Tabelle mit einer großen Datenmenge zu schlechten Ergebnissen (Sie warten zu lange auf die ersten Ergebnisse, was in meinem Fall für eine blockierte Webantwort nicht gut ist).
Bester Ansatz hier angegeben https://stackoverflow.com/a/27169302/450103 . In meinem Fall habe ich das Problem behoben, indem ich einfach den Index für das Datum / Uhrzeit-Feld verwendet und die nächste Abfrage mit Datum / Uhrzeit> = vorherige_Datenzeit abgerufen habe. Dumm, weil ich diesen Index schon in verschiedenen Fällen verwendet habe, aber dachte, dass das Abrufen aller Datenfenster-Abfragen besser wäre. In meinem Fall habe ich mich geirrt.
quelle
AFAIK, die erste Variante ruft immer noch alle Tupel aus der Tabelle ab (mit einer SQL-Abfrage), erstellt jedoch beim Iterieren die ORM-Präsentation für jede Entität. Es ist also effizienter als das Erstellen einer Liste aller Entitäten vor dem Iterieren, aber Sie müssen immer noch alle (Roh-) Daten in den Speicher abrufen.
Daher klingt es für mich nach einer guten Idee, LIMIT für große Tische zu verwenden.
quelle