psycopg2: inserisci più righe con una query


141

Devo inserire più righe con una query (il numero di righe non è costante), quindi devo eseguire una query come questa:

INSERT INTO t (a, b) VALUES (1, 2), (3, 4), (5, 6);

L'unico modo che conosco è

args = [(1,2), (3,4), (5,6)]
args_str = ','.join(cursor.mogrify("%s", (x, )) for x in args)
cursor.execute("INSERT INTO t (a, b) VALUES "+args_str)

ma voglio un modo più semplice.

Risposte:


219

Ho creato un programma che inserisce più linee su un server che si trovava in un'altra città.

Ho scoperto che l'uso di questo metodo era circa 10 volte più veloce di executemany. Nel mio caso tupè una tupla contenente circa 2000 righe. Ci sono voluti circa 10 secondi quando si utilizza questo metodo:

args_str = ','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x) for x in tup)
cur.execute("INSERT INTO table VALUES " + args_str) 

e 2 minuti quando si utilizza questo metodo:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)

15
Ancora molto rilevante quasi due anni dopo. Un'esperienza oggi suggerisce che il numero di righe che si desidera spingere aumenta, meglio è usare la executestrategia. Ho visto una velocità di circa 100 volte grazie a questo!
Rob Watts,

4
Forse executemanyesegue un commit dopo ogni inserimento. Se invece avvolgessi il tutto in una transazione, forse questo accelererebbe le cose?
Richard,

4
Ho appena confermato questo miglioramento da solo. Da quello che ho letto psycopg2 executemanynon fa nulla di ottimale, fa solo loop e fa molte executeaffermazioni. Utilizzando questo metodo, un inserimento di 700 righe in un server remoto è passato da 60 secondi a <2 secondi.
Nelson,

5
Forse sono paranoico, ma concatenando la query con un +sembra che si possa aprire l'iniezione sql, mi sento come se la execute_values()soluzione @Clodoaldo Neto fosse più sicura.
Will Munn,

26
nel caso in cui qualcuno riscontri il seguente errore: [TypeError: elemento di sequenza 0: istanza str prevista, byte trovati] eseguire questo comando invece [args_str = ','. join (cur.mogrify ("(% s,% s)", x ) .decode ("utf-8") per x in tup)]
mrt

147

Nuovo execute_valuesmetodo in Psycopg 2.7:

data = [(1,'x'), (2,'y')]
insert_query = 'insert into t (a, b) values %s'
psycopg2.extras.execute_values (
    cursor, insert_query, data, template=None, page_size=100
)

Il modo pitonico di farlo in Psycopg 2.6:

data = [(1,'x'), (2,'y')]
records_list_template = ','.join(['%s'] * len(data))
insert_query = 'insert into t (a, b) values {}'.format(records_list_template)
cursor.execute(insert_query, data)

Spiegazione: Se i dati da inserire sono dati come un elenco di tuple come in

data = [(1,'x'), (2,'y')]

allora è già nel formato esatto richiesto come

  1. la valuessintassi della insertclausola prevede un elenco di record come in

    insert into t (a, b) values (1, 'x'),(2, 'y')

  2. Psycopgadatta un Python tuplea un Postgresql record.

L'unico lavoro necessario è fornire un modello di elenco di record che deve essere compilato da psycopg

# We use the data list to be sure of the template length
records_list_template = ','.join(['%s'] * len(data))

e inseriscilo nella insertquery

insert_query = 'insert into t (a, b) values {}'.format(records_list_template)

Stampa delle insert_queryuscite

insert into t (a, b) values %s,%s

Ora alla solita Psycopgsostituzione degli argomenti

cursor.execute(insert_query, data)

O semplicemente testando ciò che verrà inviato al server

print (cursor.mogrify(insert_query, data).decode('utf8'))

Produzione:

insert into t (a, b) values (1, 'x'),(2, 'y')

1
Come si confronta la prestazione di questo metodo con cur.copy_from?
Michael Goldshteyn,

1
Ecco un esempio con un punto di riferimento . copy_from viene ridimensionato a circa 6,5 ​​volte più veloce sulla mia macchina con record da 10 milioni.
Joseph Sheedy,

Sembra carino - penso che tu abbia un randagio, alla fine della tua definizione iniziale di insert_query (a meno che tu non stia cercando di renderlo una tupla?) E manchi come dopo% per% s anche nella definizione iniziale di insert_query.
deadcode del

2
usando execute_valuessono stato in grado di far funzionare il mio sistema a record 1k al minuto fino a 128k record al minuto
Conrad.Dean

66

Aggiornamento con psycopg2 2.7:

Il classico executemany()è circa 60 volte più lento dell'implementazione di @ ant32 (chiamato "piegato") come spiegato in questa discussione: https://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

Questa implementazione è stata aggiunta a psycopg2 nella versione 2.7 e si chiama execute_values():

from psycopg2.extras import execute_values
execute_values(cur,
    "INSERT INTO test (id, v1, v2) VALUES %s",
    [(1, 2, 3), (4, 5, 6), (7, 8, 9)])

Risposta precedente:

Per inserire più righe, l'uso della VALUESsintassi execute()multirow con è circa 10 volte più veloce rispetto all'uso di psycopg2 executemany(). In effetti, executemany()gestisce solo molti individuiINSERT dichiarazioni .

Il codice di @ ant32 funziona perfettamente in Python 2. Ma in Python 3, cursor.mogrify()restituisce byte, cursor.execute()accetta byte o stringhe e si ','.join()aspettastr un'istanza.

Quindi in Python 3 potrebbe essere necessario modificare il codice di @ ant32, aggiungendo .decode('utf-8'):

args_str = ','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x).decode('utf-8') for x in tup)
cur.execute("INSERT INTO table VALUES " + args_str)

O usando solo i byte (con b''o b""):

args_bytes = b','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x) for x in tup)
cur.execute(b"INSERT INTO table VALUES " + args_bytes) 

26

cursor.copy_from è di gran lunga la soluzione più veloce che ho trovato per inserti di massa. Ecco una sintesi che ho creato contenente una classe denominata IteratorFile che consente a un iteratore che produce stringhe di essere letto come un file. Possiamo convertire ogni record di input in una stringa usando un'espressione del generatore. Quindi la soluzione sarebbe

args = [(1,2), (3,4), (5,6)]
f = IteratorFile(("{}\t{}".format(x[0], x[1]) for x in args))
cursor.copy_from(f, 'table_name', columns=('a', 'b'))

Per questa banale dimensione di argomenti non farà molta differenza di velocità, ma vedo grandi accelerazioni quando si tratta di migliaia di righe. Sarà anche più efficiente in termini di memoria rispetto alla creazione di una stringa di query gigante. Un iteratore conserverebbe in memoria un solo record di input alla volta, dove a un certo punto rimarrai senza memoria nel tuo processo Python o in Postgres creando la stringa di query.


3
Ecco un benchmark che confronta copy_from / IteratorFile con una soluzione di query builder. copy_from viene ridimensionato a circa 6,5 ​​volte più veloce sulla mia macchina con record da 10 milioni.
Joseph Sheedy,

3
devi dick in giro con stringhe e timestamp di escape ecc?
CpILL,

Sì, dovrai assicurarti di avere un record TSV ben formato.
Joseph Sheedy,

24

Un frammento della pagina tutorial di Psycopg2 su Postgresql.org (vedi in basso) :

Un ultimo elemento che vorrei mostrarti è come inserire più righe usando un dizionario. Se avessi il seguente:

namedict = ({"first_name":"Joshua", "last_name":"Drake"},
            {"first_name":"Steven", "last_name":"Foo"},
            {"first_name":"David", "last_name":"Bar"})

È possibile inserire facilmente tutte e tre le righe nel dizionario utilizzando:

cur = conn.cursor()
cur.executemany("""INSERT INTO bar(first_name,last_name) VALUES (%(first_name)s, %(last_name)s)""", namedict)

Non salva molto codice, ma sembra decisamente migliore.


35
Questo eseguirà molte INSERTdichiarazioni individuali . Utile, ma non uguale a un singolo VALUEinserto multiplo.
Craig Ringer,

7

Tutte queste tecniche sono chiamate "Inserti estesi" nella terminologia di Postgres e, dal 24 novembre 2016, sono ancora molto più veloci dell'esecutivo di psychopg2 () e di tutti gli altri metodi elencati in questo thread (che ho provato prima di arrivare a questo risposta).

Ecco un po 'di codice che non usa cur.mogrify ed è bello e semplicemente per avere la testa in giro:

valueSQL = [ '%s', '%s', '%s', ... ] # as many as you have columns.
sqlrows = []
rowsPerInsert = 3 # more means faster, but with diminishing returns..
for row in getSomeData:
        # row == [1, 'a', 'yolo', ... ]
        sqlrows += row
        if ( len(sqlrows)/len(valueSQL) ) % rowsPerInsert == 0:
                # sqlrows == [ 1, 'a', 'yolo', 2, 'b', 'swag', 3, 'c', 'selfie' ]
                insertSQL = 'INSERT INTO "twitter" VALUES ' + ','.join(['(' + ','.join(valueSQL) + ')']*rowsPerInsert)
                cur.execute(insertSQL, sqlrows)
                con.commit()
                sqlrows = []
insertSQL = 'INSERT INTO "twitter" VALUES ' + ','.join(['(' + ','.join(valueSQL) + ')']*len(sqlrows))
cur.execute(insertSQL, sqlrows)
con.commit()

Ma va notato che se puoi usare copy_from (), dovresti usare copy_from;)


Cresciuto dai morti, ma cosa succede nella situazione delle ultime file? Presumo che tu abbia effettivamente eseguito di nuovo quella clausola finale sulle ultime righe rimanenti, nel caso tu abbia un numero pari di righe?
mcpeterson

Esatto, mi dispiace di aver dimenticato di farlo quando ho scritto l'esempio - è piuttosto stupido da parte mia. Non farlo non avrebbe dato alla gente un errore, il che mi fa preoccupare quante persone hanno copiato / incollato la soluzione e si sono occupati dei loro affari ..... Comunque, mcpeterson molto grato - grazie!
JJ,

2

Sto usando la risposta di ant32 sopra per diversi anni. Tuttavia ho scoperto che è un errore in Python 3 perché mogrifyrestituisce una stringa di byte.

La conversione esplicita in stringhe di byte è una soluzione semplice per rendere compatibile il codice python 3.

args_str = b','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x) for x in tup) 
cur.execute(b"INSERT INTO table VALUES " + args_str)

1

Un altro approccio gradevole ed efficiente è passare le righe per l'inserimento come 1 argomento, che è un array di oggetti json.

Ad esempio, stai passando argomento:

[ {id: 18, score: 1}, { id: 19, score: 5} ]

È un array, che può contenere qualsiasi quantità di oggetti all'interno. Quindi il tuo SQL assomiglia a:

INSERT INTO links (parent_id, child_id, score) 
SELECT 123, (r->>'id')::int, (r->>'score')::int 
FROM unnest($1::json[]) as r 

Avviso: il tuo postgress deve essere abbastanza nuovo da supportare json


1

La soluzione cursor.copyfrom fornita da @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) sopra ( https://stackoverflow.com/a/30721460/11100064 ) è davvero velocissima.

Tuttavia, l'esempio che fornisce non è genericamente utilizzabile per un record con un numero qualsiasi di campi e mi ci è voluto del tempo per capire come usarlo correttamente.

L'IteratorFile deve essere istanziato con campi separati da tabulazioni come questo ( rè un elenco di dicts in cui ogni dict è un record):

    f = IteratorFile("{0}\t{1}\t{2}\t{3}\t{4}".format(r["id"],
        r["type"],
        r["item"],
        r["month"],
        r["revenue"]) for r in records)

Per generalizzare un numero arbitrario di campi, creeremo prima una stringa di riga con il numero corretto di schede e segnaposto di campo: "{}\t{}\t{}....\t{}"quindi utilizzeremo .format()per compilare i valori del campo *list(r.values())) for r in records::

        line = "\t".join(["{}"] * len(records[0]))

        f = IteratorFile(line.format(*list(r.values())) for r in records)

funzione completa in sintesi qui .


0

Se stai usando SQLAlchemy, non devi fare confusione con la creazione manuale della stringa perché SQLAlchemy supporta la generazione di una VALUESclausola multi-riga per una singola INSERTistruzione :

rows = []
for i, name in enumerate(rawdata):
    row = {
        'id': i,
        'name': name,
        'valid': True,
    }
    rows.append(row)
if len(rows) > 0:  # INSERT fails if no rows
    insert_query = SQLAlchemyModelName.__table__.insert().values(rows)
    session.execute(insert_query)

Sotto il cofano SQLAlchemy utilizza il execemany () di psychopg2 per chiamate come questa e quindi questa risposta avrà seri problemi di prestazioni per query di grandi dimensioni. Vedi il metodo di esecuzione docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88,

2
Non penso sia così. È passato un po 'da quando l'ho visto, ma IIRC, in realtà sta costruendo una singola istruzione di inserimento nella insert_queryriga. Quindi, session.execute()sta solo chiamando l' execute()istruzione psycopg2 con una singola stringa enorme. Quindi il "trucco" sta costruendo prima l'intero oggetto dell'istruzione di inserimento. Sto usando questo per inserire 200.000 righe alla volta e ho visto enormi aumenti delle prestazioni usando questo codice rispetto al normale executemany().
Jeff Widman,

1
Il documento SQLAlchemy a cui ti sei collegato ha una sezione che mostra esattamente come funziona e dice anche: "È essenziale notare che passare più valori NON è lo stesso che usare il tradizionale modulo execemany ()". Quindi sta esplicitamente dicendo che funziona.
Jeff Widman,

1
Sono corretto. Non ho notato il tuo utilizzo del metodo limits () (senza di esso SQLAlchemy non fa altro che eseguire). Direi di modificare la risposta per includere un link a quel documento in modo da poter cambiare il mio voto, ma ovviamente l'hai già incluso. Forse dire che non è la stessa cosa che chiamare un insert () con execute () con un elenco di dicts?
sage88,

come si comporta rispetto a execute_values?
MrR

0

execute_batch è stato aggiunto a psycopg2 da quando è stata pubblicata questa domanda.

È più lento di execute_values ma più semplice da usare.


2
Vedi altri commenti. Il metodo di psycopg2 execute_valuesè più veloce diexecute_batch
Fierr

0

I dirigenti accettano una serie di tuple

https://www.postgresqltutorial.com/postgresql-python/insert/

    """ array of tuples """
    vendor_list = [(value1,)]

    """ insert multiple vendors into the vendors table  """
    sql = "INSERT INTO vendors(vendor_name) VALUES(%s)"
    conn = None
    try:
        # read database configuration
        params = config()
        # connect to the PostgreSQL database
        conn = psycopg2.connect(**params)
        # create a new cursor
        cur = conn.cursor()
        # execute the INSERT statement
        cur.executemany(sql,vendor_list)
        # commit the changes to the database
        conn.commit()
        # close communication with the database
        cur.close()
    except (Exception, psycopg2.DatabaseError) as error:
        print(error)
    finally:
        if conn is not None:
            conn.close()

-1

Se si desidera inserire più righe in uno stato di inserimento (supponendo che non si stia utilizzando ORM) il modo più semplice finora per me sarebbe utilizzare l'elenco dei dizionari. Ecco un esempio:

 t = [{'id':1, 'start_date': '2015-07-19 00:00:00', 'end_date': '2015-07-20 00:00:00', 'campaignid': 6},
      {'id':2, 'start_date': '2015-07-19 00:00:00', 'end_date': '2015-07-20 00:00:00', 'campaignid': 7},
      {'id':3, 'start_date': '2015-07-19 00:00:00', 'end_date': '2015-07-20 00:00:00', 'campaignid': 8}]

conn.execute("insert into campaign_dates
             (id, start_date, end_date, campaignid) 
              values (%(id)s, %(start_date)s, %(end_date)s, %(campaignid)s);",
             t)

Come puoi vedere, verrà eseguita una sola query:

INFO sqlalchemy.engine.base.Engine insert into campaign_dates (id, start_date, end_date, campaignid) values (%(id)s, %(start_date)s, %(end_date)s, %(campaignid)s);
INFO sqlalchemy.engine.base.Engine [{'campaignid': 6, 'id': 1, 'end_date': '2015-07-20 00:00:00', 'start_date': '2015-07-19 00:00:00'}, {'campaignid': 7, 'id': 2, 'end_date': '2015-07-20 00:00:00', 'start_date': '2015-07-19 00:00:00'}, {'campaignid': 8, 'id': 3, 'end_date': '2015-07-20 00:00:00', 'start_date': '2015-07-19 00:00:00'}]
INFO sqlalchemy.engine.base.Engine COMMIT

1
Mostrare la registrazione dal motore sqlalchemy NON è una dimostrazione dell'esecuzione di una sola query, significa solo che il motore sqlalchemy ha eseguito un comando. Sotto il cofano si sta usando la compagnia di psychopg2 che è molto inefficiente. Vedi il metodo di esecuzione docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88,

-3

Utilizzo di aiopg : lo snippet di seguito funziona perfettamente

    # items = [10, 11, 12, 13]
    # group = 1
    tup = [(gid, pid) for pid in items]
    args_str = ",".join([str(s) for s in tup])
    # insert into group values (1, 10), (1, 11), (1, 12), (1, 13)
    yield from cur.execute("INSERT INTO group VALUES " + args_str)


-4

Infine nella versione SQLalchemy1.2, questa nuova implementazione viene aggiunta per utilizzare psycopg2.extras.execute_batch () invece di execemany quando si inizializza il motore con use_batch_mode = True like:

engine = create_engine(
    "postgresql+psycopg2://scott:tiger@host/dbname",
    use_batch_mode=True)

http://docs.sqlalchemy.org/en/latest/changelog/migration_12.html#change-4109

Quindi qualcuno dovrebbe usare SQLalchmey non si preoccuperà di provare diverse combinazioni di sqla e psycopg2 e dirigere SQL insieme.

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.