iteratore / generatore SqlAlchemy integrato con efficienza di memoria?


90

Ho una tabella MySQL di ~ 10M di record con cui interfacciarmi usando SqlAlchemy. Ho scoperto che le query su grandi sottoinsiemi di questa tabella consumano troppa memoria anche se pensavo di utilizzare un generatore integrato che recuperava in modo intelligente frammenti di piccole dimensioni del set di dati:

for thing in session.query(Things):
    analyze(thing)

Per evitare ciò, trovo che devo costruire il mio iteratore che si morde a pezzi:

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)

È normale o c'è qualcosa che mi manca riguardo ai generatori integrati SA?

La risposta a questa domanda sembra indicare che il consumo di memoria non è previsto.


Ho qualcosa di molto simile, tranne per il fatto che produce "cosa". Funziona meglio di tutte le altre soluzioni
iElectric

2
Non è Thing.id> lastThingID? E cosa sono le "righe"?
sinergico

Risposte:


118

La maggior parte delle implementazioni DBAPI bufferizza completamente le righe man mano che vengono recuperate, quindi di solito, prima che l'ORM SQLAlchemy ottenga anche solo un risultato, l'intero set di risultati è in memoria.

Ma poi, il modo in cui Queryfunziona è che carica completamente il set di risultati specificato per impostazione predefinita prima di restituirti i tuoi oggetti. La logica qui riguarda le query che sono più che semplici istruzioni SELECT. Ad esempio, nei join ad altre tabelle che possono restituire la stessa identità dell'oggetto più volte in un set di risultati (comune con il caricamento ansioso), il set completo di righe deve essere in memoria in modo che i risultati corretti possano essere restituiti altrimenti raccolte e simili potrebbe essere popolato solo parzialmente.

Quindi Queryoffre un'opzione per modificare questo comportamento yield_per(). Questa chiamata farà sì che il Queryproduca righe in batch, dove gli dai la dimensione del batch. Come affermano i documenti, questo è appropriato solo se non stai facendo alcun tipo di caricamento desideroso delle raccolte, quindi è fondamentalmente se sai davvero cosa stai facendo. Inoltre, se il DBAPI sottostante esegue il pre-buffer delle righe, ci sarà ancora quel sovraccarico di memoria, quindi l'approccio si ridimensiona solo leggermente meglio rispetto al mancato utilizzo.

Non lo uso quasi mai yield_per(); invece, utilizzo una versione migliore dell'approccio LIMIT che suggerisci sopra usando le funzioni della finestra. LIMIT e OFFSET hanno un grosso problema che valori di OFFSET molto grandi fanno sì che la query diventi sempre più lenta, poiché un OFFSET di N fa sì che si sfoglia N righe - è come fare la stessa query cinquanta volte invece di una, ogni volta che legge un numero sempre maggiore di righe. Con un approccio basato sulla funzione finestra, prelevo un insieme di valori "finestra" che si riferiscono a parti della tabella che desidero selezionare. Quindi emetto singole istruzioni SELECT che ciascuna estrae da una di quelle finestre alla volta.

L'approccio alla funzione finestra è sul wiki e lo uso con grande successo.

Nota anche: non tutti i database supportano le funzioni della finestra; hai bisogno di Postgresql, Oracle o SQL Server. IMHO usando almeno Postgresql ne vale sicuramente la pena: se stai usando un database relazionale, potresti anche usare il meglio.


Citi Query instanciates tutto per confrontare le identità. Potrebbe essere evitato ordinando sulla chiave primaria e confrontando solo i risultati consecutivi?
Tobu

il problema è che se fornisci un'istanza con identità X, l'applicazione la acquisisce e quindi prende decisioni in base a questa entità e forse la modifica. In seguito, forse (in realtà di solito) anche nella riga successiva, la stessa identità ritorna nel risultato, forse per aggiungere più contenuti alle sue collezioni. La domanda ha quindi ricevuto l'oggetto in uno stato incompleto. l'ordinamento non aiuta qui perché il problema più grande è il funzionamento del caricamento desideroso: sia il caricamento "unito" che quello "sottoquery" hanno problemi diversi.
zzzeek

Ho capito che "la riga successiva aggiorna le raccolte", nel qual caso è sufficiente guardare avanti di una riga del database per sapere quando le raccolte sono complete. L'implementazione del caricamento desideroso dovrebbe cooperare con l'ordinamento, in modo che gli aggiornamenti della raccolta vengano sempre eseguiti su righe adiacenti.
Tobu

l'opzione yield_per () è sempre disponibile quando sei sicuro che la query che stai emettendo è compatibile con la fornitura di set di risultati parziali. Ho passato una maratona di diversi giorni cercando di abilitare questo comportamento in tutti i casi, c'erano sempre oscuri, cioè fino a quando il tuo programma non ne usa uno, bordi che hanno fallito. In particolare, non si può presumere di fare affidamento sull'ordinazione. Come sempre, sono benvenuto ai contributi effettivi del codice.
zzzeek

1
Dal momento che sto usando postgres, sembra che sia possibile utilizzare la transazione di sola lettura Repeatable Read ed eseguire tutte le query con finestra in quella transazione.
schatten

24

Non sono un esperto di database, ma quando utilizzo SQLAlchemy come un semplice livello di astrazione Python (ovvero, non utilizzando l'oggetto ORM Query) ho trovato una soluzione soddisfacente per interrogare una tabella di 300 M di righe senza far esplodere l'utilizzo della memoria ...

Ecco un esempio fittizio:

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)

Quindi, uso il fetchmany()metodo SQLAlchemy per iterare i risultati in un whileciclo infinito :

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

Questo metodo mi ha permesso di eseguire tutti i tipi di aggregazione dei dati senza alcun pericoloso sovraccarico di memoria.

NOTE la stream_resultslavora con Postgres e l' pyscopg2adattatore, ma credo che non funziona con qualsiasi DBAPI, nè con qualsiasi driver di database ...

C'è un caso d'uso interessante in questo post del blog che ha ispirato il mio metodo sopra.


1
Se si sta lavorando su postgres o mysql (con pymysql), questa dovrebbe essere la risposta accettata IMHO.
Yuki Inoue

1
Mi ha salvato la vita, vedevo le mie query funzionare sempre più lentamente. Ho strumentato quanto sopra su pyodbc (dal server sql a postgres) e funziona come un sogno.
Ed Baker

Questo è stato per me l'approccio migliore. Dato che sto usando ORM, avevo bisogno di compilare l'SQL nel mio dialetto (Postgres) e quindi eseguirlo direttamente dalla connessione (non dalla sessione) come mostrato sopra. La compilazione "come fare per" ho trovato in questa altra domanda stackoverflow.com/questions/4617291 . Migliorare la velocità è stato grande. Anche il passaggio da JOINS a SUBQUERIES ha comportato un notevole aumento delle prestazioni. Consiglia inoltre di utilizzare sqlalchemy_mixins, l'utilizzo di smart_query ha aiutato molto a creare la query più efficiente. github.com/absent1706/sqlalchemy-mixins
Gustavo Gonçalves

14

Ho esaminato un efficiente traversal / paging con SQLAlchemy e vorrei aggiornare questa risposta.

Penso che tu possa usare la chiamata slice per limitare adeguatamente l'ambito di una query e potresti riutilizzarla in modo efficiente.

Esempio:

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

Sembra molto semplice e veloce. Non sono sicuro che .all()sia necessario. Noto che la velocità è migliorata molto dopo la prima chiamata.
hamx0r

@ hamx0r mi rendo conto che questo è un vecchio commento quindi lo lascio per i posteri. Senza .all()la variabile cose c'è una query che non supporta len ()
David

9

Nello spirito della risposta di Joel, utilizzo quanto segue:

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

things = query.slice (start, stop) .all () restituirà [] alla fine e il ciclo while non si interromperà mai
Martin Reguly

4

Usare LIMIT / OFFSET è sbagliato, perché devi trovare tutte le colonne {OFFSET} prima, quindi maggiore è OFFSET, più lunga è la richiesta. L'utilizzo di query con finestre per me dà anche risultati negativi su tabelle di grandi dimensioni con una grande quantità di dati (si aspettano i primi risultati troppo a lungo, che non va bene nel mio caso per la risposta web a blocchi).

Il miglior approccio fornito qui https://stackoverflow.com/a/27169302/450103 . Nel mio caso ho risolto il problema semplicemente utilizzando l'indice sul campo datetime e recuperando la query successiva con datetime> = previous_datetime. Stupido, perché ho usato quell'indice in diversi casi prima, ma pensavo che per recuperare tutti i dati la query con finestre sarebbe stata migliore. Nel mio caso mi sbagliavo.


3

Per quanto ne so, la prima variante ottiene ancora tutte le tuple dalla tabella (con una query SQL) ma crea la presentazione ORM per ogni entità durante l'iterazione. Quindi è più efficiente rispetto alla creazione di un elenco di tutte le entità prima dell'iterazione, ma devi comunque recuperare tutti i dati (grezzi) in memoria.

Quindi, usare LIMIT su tavoli enormi mi sembra una buona idea.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.