Inserimento in blocco con SQLAlchemy ORM


131

C'è un modo per fare in modo che SQLAlchemy esegua un inserimento di massa piuttosto che inserire ogni singolo oggetto. vale a dire,

fare:

INSERT INTO `foo` (`bar`) VALUES (1), (2), (3)

piuttosto che:

INSERT INTO `foo` (`bar`) VALUES (1)
INSERT INTO `foo` (`bar`) VALUES (2)
INSERT INTO `foo` (`bar`) VALUES (3)

Ho appena convertito del codice per utilizzare sqlalchemy piuttosto che sql grezzo e sebbene ora sia molto più bello lavorarci sembra essere più lento ora (fino a un fattore 10), mi chiedo se questo sia il motivo.

Forse potrei migliorare la situazione usando le sessioni in modo più efficiente. Al momento ne ho autoCommit=Falsee ne faccio una session.commit()dopo aver aggiunto alcune cose. Anche se questo sembra rendere obsoleti i dati se il DB viene modificato altrove, ad esempio anche se faccio una nuova query, ottengo ancora i vecchi risultati?

Grazie per l'aiuto!



1
Nick, capisco che questo sia un post molto vecchio. Sarebbe possibile aggiornare il titolo a qualcosa di corretto come "inserimento di più record con SQLAlchemy ORM". Le istruzioni di inserimento multi-record come quella che hai fornito sono abbastanza diverse dalle operazioni di caricamento di massa a livello di database. Gli inserimenti in blocco sono intesi per 1k + caricamenti di dati, di solito da set di dati di grandi dimensioni e eseguiti da gestori di applicazioni, non operazioni REST o codice a livello di applicazione ... Usiamo la nostra nomenclatura correttamente.
W4t3randWind

Per coloro che si imbattono in questa domanda mentre cercano informazioni sulle operazioni di massa in sqlalchemy Core (non ORM), vedere la mia risposta a un'altra domanda .
Nickolay

Risposte:


174

SQLAlchemy lo ha introdotto nella versione 1.0.0:

Operazioni in blocco: documenti SQLAlchemy

Con queste operazioni, ora puoi eseguire inserimenti o aggiornamenti in blocco!

Ad esempio, puoi fare:

s = Session()
objects = [
    User(name="u1"),
    User(name="u2"),
    User(name="u3")
]
s.bulk_save_objects(objects)
s.commit()

Qui verrà realizzato un inserto in blocco.


30
Hai anche bisogno di s.commit () per salvare effettivamente i record (mi ci è voluto un po 'per capirlo).
horcle_buzz

3
L'ho provato con sqlachemy 1.0.11 e fa ancora 3 istruzioni di inserimento. Ma è molto più veloce delle normali operazioni orm.
zidarsk8

3
sebbene non pertinente alla questione dei PO, vale la pena menzionare che questo interrompe alcune caratteristiche dell'ORM. docs.sqlalchemy.org/en/rel_1_0/orm/…
dangel

@dangel si grazie per aver postato questo. Sebbene il titolo di OP riguardi il "caricamento di massa", la sua domanda sulle istruzioni di inserimento di più record non ha nulla a che fare con la funzione di caricamento di massa di sqlalchemy.
W4t3randWind

Rispetto all'inserimento degli stessi dati da CSV con \copypsql (dallo stesso client allo stesso server), vedo un'enorme differenza di prestazioni sul lato server che si traduce in circa 10 volte più inserimenti / s. Apparentemente sta caricando in massa usando \copy(o COPYsul server) usando un impacchettamento nella comunicazione da client a server molto meglio dell'utilizzo di SQL tramite SQLAlchemy. Per saperne di più: grande massa inserto differenza di prestazioni PostgreSQL vs ... .
gertvdijk

42

I documenti di sqlalchemy contengono un resoconto sulle prestazioni di varie tecniche che possono essere utilizzate per gli inserimenti in blocco :

Gli ORM fondamentalmente non sono destinati agli inserimenti di massa ad alte prestazioni: questo è il motivo principale per cui SQLAlchemy offre il Core oltre all'ORM come componente di prima classe.

Per il caso d'uso di inserimenti in blocco veloci, il sistema di generazione ed esecuzione SQL su cui si basa ORM fa parte del Core. Utilizzando direttamente questo sistema, possiamo produrre un INSERT che è competitivo con l'utilizzo diretto dell'API del database grezzo.

In alternativa, SQLAlchemy ORM offre la suite di metodi Bulk Operations, che fornisce hook nelle sottosezioni dell'unità di processo di lavoro al fine di emettere costrutti INSERT e UPDATE a livello di core con un piccolo grado di automazione basata su ORM.

L'esempio seguente illustra i test basati sul tempo per diversi metodi di inserimento di righe, dal più automatizzato al meno. Con cPython 2.7, i runtime hanno osservato:

classics-MacBook-Pro:sqlalchemy classic$ python test.py
SQLAlchemy ORM: Total time for 100000 records 12.0471920967 secs
SQLAlchemy ORM pk given: Total time for 100000 records 7.06283402443 secs
SQLAlchemy ORM bulk_save_objects(): Total time for 100000 records 0.856323003769 secs
SQLAlchemy Core: Total time for 100000 records 0.485800027847 secs
sqlite3: Total time for 100000 records 0.487842082977 sec

script:

import time
import sqlite3

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String,  create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

Base = declarative_base()
DBSession = scoped_session(sessionmaker())
engine = None


class Customer(Base):
    __tablename__ = "customer"
    id = Column(Integer, primary_key=True)
    name = Column(String(255))


def init_sqlalchemy(dbname='sqlite:///sqlalchemy.db'):
    global engine
    engine = create_engine(dbname, echo=False)
    DBSession.remove()
    DBSession.configure(bind=engine, autoflush=False, expire_on_commit=False)
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)


def test_sqlalchemy_orm(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in xrange(n):
        customer = Customer()
        customer.name = 'NAME ' + str(i)
        DBSession.add(customer)
        if i % 1000 == 0:
            DBSession.flush()
    DBSession.commit()
    print(
        "SQLAlchemy ORM: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_orm_pk_given(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in xrange(n):
        customer = Customer(id=i+1, name="NAME " + str(i))
        DBSession.add(customer)
        if i % 1000 == 0:
            DBSession.flush()
    DBSession.commit()
    print(
        "SQLAlchemy ORM pk given: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_orm_bulk_insert(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    n1 = n
    while n1 > 0:
        n1 = n1 - 10000
        DBSession.bulk_insert_mappings(
            Customer,
            [
                dict(name="NAME " + str(i))
                for i in xrange(min(10000, n1))
            ]
        )
    DBSession.commit()
    print(
        "SQLAlchemy ORM bulk_save_objects(): Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_core(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    engine.execute(
        Customer.__table__.insert(),
        [{"name": 'NAME ' + str(i)} for i in xrange(n)]
    )
    print(
        "SQLAlchemy Core: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def init_sqlite3(dbname):
    conn = sqlite3.connect(dbname)
    c = conn.cursor()
    c.execute("DROP TABLE IF EXISTS customer")
    c.execute(
        "CREATE TABLE customer (id INTEGER NOT NULL, "
        "name VARCHAR(255), PRIMARY KEY(id))")
    conn.commit()
    return conn


def test_sqlite3(n=100000, dbname='sqlite3.db'):
    conn = init_sqlite3(dbname)
    c = conn.cursor()
    t0 = time.time()
    for i in xrange(n):
        row = ('NAME ' + str(i),)
        c.execute("INSERT INTO customer (name) VALUES (?)", row)
    conn.commit()
    print(
        "sqlite3: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " sec")

if __name__ == '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_orm_bulk_insert(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)

1
Grazie. Davvero utile e completo.
Steve B.

Ho visto un altro esempio utilizzando bindparams. La sintassi sembra succinta, va bene?
Jay,

35

Per quanto ne so, non c'è modo di convincere l'ORM a emettere inserti in blocco. Credo che il motivo sottostante sia che SQLAlchemy deve tenere traccia dell'identità di ogni oggetto (cioè, nuove chiavi primarie) e gli inserimenti in blocco interferiscono con questo. Ad esempio, supponendo che la footabella contenga una idcolonna ed è mappata a una Fooclasse:

x = Foo(bar=1)
print x.id
# None
session.add(x)
session.flush()
# BEGIN
# INSERT INTO foo (bar) VALUES(1)
# COMMIT
print x.id
# 1

Poiché SQLAlchemy ha rilevato il valore per x.idsenza emettere un'altra query, possiamo dedurre che ha ottenuto il valore direttamente dall'istruzione INSERT. Se non è necessario un accesso successivo agli oggetti creati tramite le stesse istanze, è possibile saltare il livello ORM per l'inserimento:

Foo.__table__.insert().execute([{'bar': 1}, {'bar': 2}, {'bar': 3}])
# INSERT INTO foo (bar) VALUES ((1,), (2,), (3,))

SQLAlchemy non può abbinare queste nuove righe con alcun oggetto esistente, quindi dovrai interrogarle di nuovo per qualsiasi operazione successiva.

Per quanto riguarda i dati obsoleti, è utile ricordare che la sessione non ha un modo integrato per sapere quando il database viene modificato al di fuori della sessione. Per accedere ai dati modificati esternamente tramite istanze esistenti, le istanze devono essere contrassegnate come scadute . Questo accade per impostazione predefinita session.commit(), ma può essere fatto manualmente chiamando session.expire_all()o session.expire(instance). Un esempio (SQL omesso):

x = Foo(bar=1)
session.add(x)
session.commit()
print x.bar
# 1
foo.update().execute(bar=42)
print x.bar
# 1
session.expire(x)
print x.bar
# 42

session.commit()scade x, quindi la prima istruzione print apre implicitamente una nuova transazione e interroga nuovamente gli xattributi. Se commentate la prima istruzione print, noterete che la seconda ora prende il valore corretto, perché la nuova query non viene emessa fino a dopo l'aggiornamento.

Ciò ha senso dal punto di vista dell'isolamento transazionale: dovresti rilevare solo le modifiche esterne tra le transazioni. Se questo ti sta causando problemi, suggerirei di chiarire o ripensare i confini delle transazioni della tua applicazione invece di raggiungerli immediatamente session.expire_all().


Grazie per la tua risposta, ci provo. WRT il problema in scadenza, quello che ho visto non era proprio lo stesso. Sto usando una sessione mirata in turbogear. L'esecuzione di una query getSession (). (Foo) .filter .... all () ha restituito cose diverse a seconda della richiesta, inoltre non ha restituito i record aggiornati che erano nel db finché non l'ho riavviato. Ho risolto questo problema eseguendo un autocommit = True e aggiungendo qualcosa che .remove () d la sessione dopo che la richiesta era stata completata (immagino che tu debba farlo comunque).
Nick Holden

Immagino che abbia restituito cose diverse a seconda della richiesta perché aveva una sessione con ambito per thread nel pool e le sessioni erano in stati diversi? Sembrava un po 'strano che sa non avrebbe ottenuto nuovi dati dopo una nuova richiesta. Mi aspetto di non capire cosa sta facendo autocommit = False
Nick Holden

Con autocommit=False, credo che dovresti chiamare il session.commit()completamento della richiesta (non ho familiarità con TurboGears, quindi ignoralo se viene gestito per te a livello di framework). Oltre ad assicurarti che le tue modifiche siano state apportate al database, questo farebbe scadere tutto nella sessione. La transazione successiva non inizierà fino al successivo utilizzo di quella sessione, quindi le richieste future sullo stesso thread non vedranno dati non aggiornati.
dhaffey

10
Stile alternativo:session.execute(Foo.__table__.insert(), values)
Joril

6
Tieni presente che le versioni più recenti di sqlalchemy hanno funzionalità di inserimento in blocco
Wayne Werner,

18

Di solito lo faccio usando add_all.

from app import session
from models import User

objects = [User(name="u1"), User(name="u2"), User(name="u3")]
session.add_all(objects)
session.commit()

2
Sei sicuro che funzioni? Non fa solo l'equivalente di .addinserirli nella sessione uno alla volta?
Alec

Sarebbe controintuitivo dato il nome del metodo, i documenti non entrano nei dettagli: Add the given collection of instances to this Session.hai motivo di credere che non faccia un inserimento in blocco?
reubano

3
Non penso che sia troppo controintuitivo - in effetti aggiunge tutte le cose a cui gli chiedi. Niente nell'aggiunta di tutte le cose alla sessione sembra implicare quali istruzioni SQL sottostanti vengono emesse. Guardando la fonte: github.com/zzzeek/sqlalchemy/blob/… sembra in effetti solo .addogni elemento individualmente.
Alec

Funziona bene, rispetto a bulk_save_objects(), flush()possiamo ottenere l'ID dell'oggetto, ma bulk_save_objects()non (evento con flush()chiamato).
coanor

14

Il supporto diretto è stato aggiunto a SQLAlchemy a partire dalla versione 0.8

Secondo i documenti , connection.execute(table.insert().values(data))dovrebbe fare il trucco. (Si noti che questo non è lo stesso connection.execute(table.insert(), data)che si traduce in molti inserimenti di singole righe tramite una chiamata a executemany). Su tutto tranne che su una connessione locale, la differenza di prestazioni può essere enorme.


10

SQLAlchemy lo ha introdotto nella versione 1.0.0:

Operazioni in blocco: documenti SQLAlchemy

Con queste operazioni, ora puoi eseguire inserimenti o aggiornamenti in blocco!

Ad esempio (se si desidera l'overhead più basso per semplici INSERT di tabella), è possibile utilizzare Session.bulk_insert_mappings():

loadme = [(1, 'a'),
          (2, 'b'),
          (3, 'c')]
dicts = [dict(bar=t[0], fly=t[1]) for t in loadme]

s = Session()
s.bulk_insert_mappings(Foo, dicts)
s.commit()

Oppure, se vuoi, salta le loadmetuple e scrivi i dizionari direttamente in dicts(ma trovo più facile lasciare tutta la prolissità dei dati e caricare un elenco di dizionari in un ciclo).


7

La risposta di Piere è corretta ma un problema è che bulk_save_objectsper impostazione predefinita non restituisce le chiavi primarie degli oggetti, se ciò ti preoccupa. Impostare return_defaultssu Trueper ottenere questo comportamento.

La documentazione è qui .

foos = [Foo(bar='a',), Foo(bar='b'), Foo(bar='c')]
session.bulk_save_objects(foos, return_defaults=True)
for foo in foos:
    assert foo.id is not None
session.commit()

2
Una cautela deve essere presa con la bandiera. Inserirà un oggetto alla volta in sequenza e il significativo miglioramento delle prestazioni potrebbe non essere presente [1]. Nel mio caso, le prestazioni sono peggiorate, cosa che sospettavo a causa dell'overhead. [1]: docs.sqlalchemy.org/en/13/orm/…
dhfromkorea

6

Tutte le strade portano a Roma , ma alcune attraversano montagne, richiedono traghetti ma se vuoi arrivarci velocemente basta prendere l'autostrada.


In questo caso l'autostrada usa la funzione execute_batch () di psycopg2 . La documentazione lo dice meglio:

L'attuale implementazione di executemany()è (usando un eufemismo estremamente caritatevole) non particolarmente performante. Queste funzioni possono essere utilizzate per accelerare l'esecuzione ripetuta di un'istruzione rispetto a un insieme di parametri. Riducendo il numero di roundtrip del server, le prestazioni possono essere di ordini di grandezza migliori rispetto all'utilizzo executemany().

Nella mia prova execute_batch()è di circa due volte più veloce come executemany(), e dà la possibilità di configurare il PAGE_SIZE per l'ulteriore messa a punto (se si vuole spremere l'ultimo 2-3% di prestazioni fuori del conducente).

La stessa funzionalità può essere facilmente abilitata se si utilizza SQLAlchemy impostandola use_batch_mode=Truecome parametro quando si crea un'istanza del motore concreate_engine()


Nota: psycopg2 execute_valuesè più veloce di psycopg2 execute_batchquando si eseguono inserimenti in blocco !
Fierr

5

Questo è un modo:

values = [1, 2, 3]
Foo.__table__.insert().execute([{'bar': x} for x in values])

Questo inserirà in questo modo:

INSERT INTO `foo` (`bar`) VALUES (1), (2), (3)

Riferimento: le domande frequenti su SQLAlchemy includono benchmark per vari metodi di commit.


3

La migliore risposta che ho trovato finora era nella documentazione di sqlalchemy:

http://docs.sqlalchemy.org/en/latest/faq/performance.html#im-inserting-400-000-rows-with-the-orm-and-it-s-really-slow

C'è un esempio completo di un benchmark di possibili soluzioni.

Come mostrato nella documentazione:

bulk_save_objects non è la soluzione migliore ma le prestazioni sono corrette.

La seconda migliore implementazione in termini di leggibilità penso sia stata con SQLAlchemy Core:

def test_sqlalchemy_core(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    engine.execute(
        Customer.__table__.insert(),
            [{"name": 'NAME ' + str(i)} for i in xrange(n)]
    )

Il contesto di questa funzione è fornito nell'articolo della documentazione.

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.