it-swarm.it

iteratore / generatore SqlAlchemy integrato a memoria efficiente?

Ho una tabella MySQL con record di circa 10 milioni che interfaccia con SqlAlchemy. Ho scoperto che le query su sottoinsiemi di grandi dimensioni di questa tabella consumeranno troppa memoria anche se pensavo di utilizzare un generatore integrato che ha recuperato in modo intelligente blocchi del set di dati di dimensioni ridotte:

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

Per evitarlo, trovo che devo costruire il mio iteratore che 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 per quanto riguarda SA generatori integrati?

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

71
Paul

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

Ma poi, il modo in cui funziona Query è che carica completamente il set di risultati dato per impostazione predefinita prima di restituirti i tuoi oggetti. La logica qui riguarda le query che sono più che semplici istruzioni SELECT. Ad esempio, quando si unisce ad altre tabelle che possono restituire più volte la stessa identità oggetto in un set di risultati (comune con il caricamento desideroso), è necessario che il set completo di righe sia in memoria in modo che i risultati corretti possano essere restituiti, altrimenti raccolte e tali potrebbe essere popolato solo parzialmente.

Quindi Query offre un'opzione per modificare questo comportamento tramite yield_per() . Questa chiamata farà sì che Query produca righe in lotti, dove gli dai la dimensione del lotto. Come afferma la documentazione, questo è appropriato solo se non stai facendo alcun tipo di caricamento avido delle raccolte, quindi è fondamentalmente se sai davvero cosa stai facendo. Inoltre, se il DBAPI sottostante pre-buffer le righe, ci sarà comunque quel sovraccarico di memoria, quindi l'approccio si ridimensiona solo leggermente rispetto al non usarlo.

Quasi mai uso yield_per(); invece, utilizzo una versione migliore dell'approccio LIMIT che suggerisci sopra usando le funzioni della finestra. LIMIT e OFFSET hanno un enorme problema che valori OFFSET molto grandi fanno sì che la query diventi sempre più lenta, poiché un OFFSET di N fa sì che la pagina passi attraverso N righe - è come fare la stessa query cinquanta volte anziché una, ogni volta leggendo un numero sempre maggiore di righe. Con un approccio con funzione finestra, preseleziono un set di valori "finestra" che si riferiscono a blocchi della tabella che voglio selezionare. Emetto quindi singole istruzioni SELECT che ciascuna estrae da una di quelle finestre alla volta.

L'approccio della 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. Ne vale sicuramente la pena usare IMHO almeno con Postgresql: se stai usando un database relazionale, potresti anche usare il meglio.

108
zzzeek

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

Penso che tu possa usare la slice call per limitare correttamente l'ambito di una query e puoi 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
13
Joel

Non sono un esperto di database, ma quando uso SQLAlchemy come semplice Python (cioè, non usando l'oggetto ORM Query) ho trovato una soluzione soddisfacente per interrogare un 300M- tabella delle righe senza 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, utilizzo il metodo SQLAlchemy fetchmany() per scorrere i risultati in un ciclo infinito while:

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 fare tutti i tipi di aggregazione dei dati senza alcun sovraccarico di memoria pericoloso.

NOTE il stream_results funziona con Postgres e l'adattatore pyscopg2, ma immagino che non funzionerà con nessun DBAPI, né con nessun driver del database ...

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

8
edouardtheron

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 things is None:
            break
        for thing in things:
            yield(thing)
        start += WINDOW_SIZE
6
Pietro Battiston

L'uso di LIMIT/OFFSET è negativo, perché è necessario trovare prima tutte le colonne {OFFSET}, quindi più grande è OFFSET - più lunga è la richiesta. L'utilizzo di query con finestre per me fornisce anche risultati negativi su una tabella di grandi dimensioni con una grande quantità di dati (aspetti i primi risultati troppo a lungo, che nel mio caso non va bene per la risposta Web bloccata).

Il miglior approccio fornito qui https://stackoverflow.com/a/27169302/4501 . Nel mio caso ho risolto il problema semplicemente usando l'indice nel campo datetime e recuperando la query successiva con datetime> = previous_datetime. Stupido, perché ho usato quell'indice in diversi casi prima, ma ho pensato che per recuperare tutte le query con finestre di dati sarebbe stato meglio. Nel mio caso ho sbagliato.

3
Victor Gavro

AFAIK, 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 della creazione di un elenco di tutte le entità prima dell'iterazione, ma è comunque necessario recuperare tutti i dati (grezzi) in memoria.

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

2
Pankrat