speichereffizienter eingebauter SqlAlchemy-Iterator / Generator?

88

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.

Paul
quelle
Ich habe etwas sehr ähnliches, außer dass es "Ding" ergibt. Funktioniert besser als alle anderen Lösungen
iElectric
2
Ist es nicht Thing.id> lastThingID? Und was ist "Zeilen"?
Synergie

Antworten:

118

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 Querybesteht 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 Queryeine Möglichkeit, dieses Verhalten durch zu ändern yield_per(). Dieser Aufruf bewirkt, dass die QueryZeilen 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.

zzzeek
quelle
Sie erwähnen, dass Query alles instanziiert, um Identitäten zu vergleichen. Könnte dies vermieden werden, indem nach dem Primärschlüssel sortiert und nur aufeinanderfolgende Ergebnisse verglichen werden?
Tobu
Das Problem besteht darin, dass die Anwendung eine Instanz mit der Identität X abruft, diese dann erfasst und dann Entscheidungen auf der Grundlage dieser Entität trifft und sie möglicherweise sogar mutiert. Später, vielleicht (eigentlich normalerweise) sogar in der nächsten Zeile, kommt dieselbe Identität im Ergebnis zurück, vielleicht um mehr Inhalte zu seinen Sammlungen hinzuzufügen. Die Anwendung hat das Objekt daher in einem unvollständigen Zustand erhalten. Das Sortieren hilft hier nicht weiter, da das größte Problem das eifrige Laden ist - sowohl das "verbundene" als auch das "Unterabfragen" -Laden haben unterschiedliche Probleme.
Zzzeek
Ich habe die Sache "Nächste Zeile aktualisiert die Sammlungen" verstanden. In diesem Fall müssen Sie nur eine Datenbankzeile nach vorne schauen, um zu wissen, wann die Sammlungen vollständig sind. Die Implementierung des eifrigen Ladens müsste mit der Sortierung zusammenarbeiten, damit Sammlungsaktualisierungen immer in benachbarten Zeilen durchgeführt werden.
Tobu
Die Option yield_per () ist immer verfügbar, wenn Sie sicher sind, dass die von Ihnen ausgegebene Abfrage mit der Bereitstellung von Teilergebnismengen kompatibel ist. Ich habe eine mehrtägige Marathonsitzung damit verbracht, dieses Verhalten in allen Fällen zu aktivieren. Es gab immer dunkle Stellen, dh bis Ihr Programm eine davon verwendet, Kanten, die fehlgeschlagen sind. Insbesondere kann nicht davon ausgegangen werden, dass man sich auf die Bestellung verlässt. Wie immer bin ich zu aktuellen Code-Beiträgen willkommen.
Zzzeek
1
Da ich Postgres verwende, scheint es möglich zu sein, die schreibgeschützte Transaktion Repeatable Read zu verwenden und alle Fensterabfragen in dieser Transaktion auszuführen.
schatten
22

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:

from sqlalchemy import create_engine, select

conn = create_engine("DB URL...").connect()
q = select([huge_table])

proxy = conn.execution_options(stream_results=True).execute(q)

Dann verwende ich die SQLAlchemy- fetchmany()Methode, um die Ergebnisse in einer Endlosschleife zu whiledurchlaufen:

while 'batch not empty':  # equivalent of 'while True', but clearer
    batch = proxy.fetchmany(100000)  # 100,000 rows at a time

    if not batch:
        break

    for row in batch:
        # Do your stuff here...

proxy.close()

Mit dieser Methode konnte ich alle Arten von Datenaggregationen ohne gefährlichen Speicheraufwand durchführen.

NOTE Das stream_resultsfunktioniert mit Postgres und dem pyscopg2Adapter, 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.

Edouardtheron
quelle
1
Wenn man an Postgres oder MySQL (mit pymysql) arbeitet, sollte dies meiner Meinung nach die akzeptierte Antwort sein.
Yuki Inoue
1
Ich habe mein Leben gerettet und meine Anfragen immer langsamer ausgeführt. Ich habe das oben genannte auf pyodbc (vom SQL Server bis zum Postgres) instrumentiert und es läuft wie ein Traum.
Ed Baker
Dies war für mich der beste Ansatz. Da ich ORM verwende, musste ich das SQL in meinen Dialekt (Postgres) kompilieren und dann direkt von der Verbindung (nicht von der Sitzung) ausführen, wie oben gezeigt. Die Kompilierung "How to" fand ich in dieser anderen Frage stackoverflow.com/questions/4617291 . Die Geschwindigkeitsverbesserung war groß. Der Wechsel von JOINS zu SUBQUERIES war ebenfalls eine große Leistungssteigerung. Empfehlen Sie auch die Verwendung von sqlalchemy_mixins. Die Verwendung von smart_query hat viel dazu beigetragen, die effizienteste Abfrage zu erstellen. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves
13

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:

window_size = 10  # or whatever limit you like
window_idx = 0
while True:
    start,stop = window_size*window_idx, window_size*(window_idx+1)
    things = query.slice(start, stop).all()
    if things is None:
        break
    for thing in things:
        analyze(thing)
    if len(things) < window_size:
        break
    window_idx += 1
Joel
quelle
Dies scheint sehr einfach und schnell zu sein. Ich bin mir nicht sicher, ob das .all()notwendig ist. Ich stelle fest, dass sich die Geschwindigkeit nach dem ersten Anruf stark verbessert hat.
Hamx0r
@ hamx0r Mir ist klar, dass dies ein alter Kommentar ist, also überlasse ich ihn einfach der Nachwelt. Ohne .all()die Dinge Variable ist eine Abfrage, die len () nicht unterstützt
David
9

Im Geiste von Joels Antwort verwende ich Folgendes:

WINDOW_SIZE = 1000
def qgen(query):
    start = 0
    while True:
        stop = start + WINDOW_SIZE
        things = query.slice(start, stop).all()
        if len(things) == 0:
            break
        for thing in things:
            yield thing
        start += WINDOW_SIZE
Pietro Battiston
quelle
things = query.slice (start, stop) .all () gibt am Ende [] zurück und while-Schleife wird niemals unterbrochen
Martin Reguly
4

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.

Victor Gavro
quelle
3

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.

Pankrat
quelle