psycopg2: inserte varias filas con una consulta

141

Necesito insertar varias filas con una consulta (el número de filas no es constante), por lo que necesito ejecutar una consulta como esta:

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

La única manera que sé es

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)

Pero quiero una manera más simple.

Sergey Fedoseev
fuente

Respuestas:

219

Creé un programa que inserta varias líneas en un servidor que se encuentra en otra ciudad.

Descubrí que usar este método era aproximadamente 10 veces más rápido que executemany. En mi caso tupes una tupla que contiene aproximadamente 2000 filas. Tomó alrededor de 10 segundos al usar este método:

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) 

y 2 minutos al usar este método:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)
ant32
fuente
15
Sigue siendo muy relevante casi dos años después. Una experiencia de hoy sugiere que a medida que aumenta el número de filas que desea empujar, mejor es utilizar la executeestrategia. ¡Vi una aceleración de alrededor de 100x gracias a esto!
Rob Watts
44
Quizás executemanyejecuta una confirmación después de cada inserción. Si, en cambio, envuelve todo en una transacción, ¿tal vez eso aceleraría las cosas?
Richard
44
Acabo de confirmar esta mejora yo mismo. Por lo que he leído, psycopg2 executemanyno hace nada óptimo, solo realiza bucles y hace muchas executedeclaraciones. Con este método, una inserción de 700 filas en un servidor remoto pasó de 60 a <2 segundos.
Nelson
55
Tal vez estoy siendo paranoico, pero al concatenar la consulta con una +parece que podría abrirse a la inyección sql, siento que la execute_values()solución @Clodoaldo Neto es más segura.
Will Munn
26
en caso de que alguien encuentre el siguiente error: [TypeError: elemento de secuencia 0: instancia de str esperada, bytes encontrados] ejecute este comando en su lugar [args_str = ','. join (cur.mogrify ("(% s,% s)", x ) .decode ("utf-8") para x en tup)]
mrt
147

Nuevo execute_valuesmétodo en 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
)

La forma pitónica de hacerlo en 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)

Explicación: Si los datos a insertar se dan como una lista de tuplas como en

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

entonces ya está en el formato exacto requerido como

  1. la valuessintaxis de la insertcláusula espera una lista de registros como en

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

  2. Psycopgadapta un Python tuplea un Postgresql record.

El único trabajo necesario es proporcionar una plantilla de lista de registros para que sea completada por psycopg

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

y colocarlo en la insertconsulta

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

Imprimir las insert_querysalidas

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

Ahora a la Psycopgsustitución de argumentos habituales

cursor.execute(insert_query, data)

O simplemente probando lo que se enviará al servidor

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

Salida:

insert into t (a, b) values (1, 'x'),(2, 'y')
Clodoaldo Neto
fuente
1
¿Cómo se compara el rendimiento de este método con cur.copy_from?
Michael Goldshteyn
1
Aquí hay una esencia con un punto de referencia . copy_from escala aproximadamente 6.5 veces más rápido en mi máquina con 10M de registros.
Joseph Sheedy
Se ve bien: creo que tiene un parásito, al final de su definición inicial de insert_query (a menos que intentara convertirlo en una tupla?) Y faltan como después del% para% s también en la definición inicial de insert_query.
código muerto
2
usando execute_valuespude hacer que mi sistema funcionara con 1k registros por minuto hasta 128k registros por minuto
Conrad.Dean
66

Actualización con psycopg2 2.7:

El clásico executemany()es aproximadamente 60 veces más lento que la implementación de @ ant32 (llamada "plegada") como se explica en este hilo: https://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

Esta implementación se agregó a psycopg2 en la versión 2.7 y se llama 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)])

Respuesta anterior:

Para insertar varias filas, usar la VALUESsintaxis de múltiples filas execute()es aproximadamente 10 veces más rápido que usar psycopg2 executemany(). De hecho, executemany()solo corre muchos individuosINSERT declaraciones .

El código de @ ant32 funciona perfectamente en Python 2. Pero en Python 3, cursor.mogrify()devuelve bytes, cursor.execute()toma bytes o cadenas, y ','.join()esperastr instancia.

Entonces, en Python 3, es posible que deba modificar el código de @ ant32, agregando .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 mediante el uso de bytes (con b''o b"") solamente:

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) 
Antoine Dusséaux
fuente
26

cursor.copy_from es la solución más rápida que he encontrado para las inserciones masivas con diferencia. Aquí hay un resumen que hice que contiene una clase llamada IteratorFile que permite que un iterador que produce cadenas se lea como un archivo. Podemos convertir cada registro de entrada en una cadena usando una expresión generadora. Entonces la solución sería

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

Para este tamaño trivial de args, no habrá mucha diferencia de velocidad, pero veo grandes aceleraciones cuando se trata de miles de filas. También será más eficiente en la memoria que construir una cadena de consulta gigante. Un iterador solo mantendría un registro de entrada en la memoria a la vez, donde en algún momento se quedará sin memoria en su proceso de Python o en Postgres al construir la cadena de consulta.

Joseph Sheedy
fuente
3
Aquí hay un punto de referencia que compara copy_from / IteratorFile con una solución de generador de consultas. copy_from escala aproximadamente 6.5 veces más rápido en mi máquina con 10M de registros.
Joseph Sheedy
3
¿tienes que andar con cadenas y marcas de tiempo, etc.?
CpILL
Sí, tendrá que asegurarse de tener registros TSV bien formados.
Joseph Sheedy
24

Un fragmento de la página del tutorial de Psycopg2 en Postgresql.org (ver abajo) :

Un último elemento que me gustaría mostrarle es cómo insertar varias filas usando un diccionario. Si tuvieras lo siguiente:

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

Puede insertar fácilmente las tres filas dentro del diccionario utilizando:

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

No guarda mucho código, pero definitivamente se ve mejor.

ptrn
fuente
35
Esto ejecutará muchas INSERTdeclaraciones individuales . Útil, pero no es lo mismo que una sola VALUEinserción multid.
Craig Ringer el
7

Todas estas técnicas se llaman 'Inserciones extendidas "en la terminología de Postgres y, a partir del 24 de noviembre de 2016, sigue siendo mucho más rápido que executemany () de psychopg2 y todos los demás métodos enumerados en este hilo (que probé antes de llegar a este responder).

Aquí hay un código que no usa cur.mogrify y es agradable y simplemente para entenderlo:

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

Pero debe tenerse en cuenta que si puede usar copy_from (), debe usar copy_from;)

JJ
fuente
Resucitando de entre los muertos, pero ¿qué sucede en la situación de las últimas filas? Supongo que realmente ejecuta esa cláusula final nuevamente en las últimas filas restantes, en el caso de que tenga un número par de filas.
mcpeterson el
Correcto, lo siento, debo haber olvidado hacer eso cuando escribí el ejemplo, eso es bastante estúpido de mi parte. No hacerlo no le habría dado a la gente un error, lo que me preocupa cuánta gente copió / pegó la solución y se ocupó de sus asuntos ... De todos modos, muy agradecido mcpeterson - ¡gracias!
JJ
2

He estado usando la respuesta de ant32 anterior durante varios años. Sin embargo, he encontrado que hay un error en Python 3 porque mogrifydevuelve una cadena de bytes.

La conversión explícita a cadenas bytse es una solución simple para hacer que el código python 3 sea compatible.

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)
jprockbelly
fuente
1

Otro enfoque agradable y eficiente es pasar filas para la inserción como 1 argumento, que es una matriz de objetos json.

Por ejemplo, tu argumento pasajero:

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

Es una matriz, que puede contener cualquier cantidad de objetos en su interior. Entonces su SQL se ve así:

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

Aviso: su postgreso debe ser lo suficientemente nuevo para admitir json

Daniel Garmoshka
fuente
1

La solución cursor.copyfrom proporcionada por @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) anterior ( https://stackoverflow.com/a/30721460/11100064 ) es realmente muy rápida.

Sin embargo, el ejemplo que da no se puede usar genéricamente para un registro con cualquier número de campos y me tomó un tiempo descubrir cómo usarlo correctamente.

El rarchivo Iterator necesita ser instanciado con campos separados por tabuladores como este ( es una lista de dictados donde cada dict es un registro):

    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)

Para generalizar para un número arbitrario de campos, primero crearemos una cadena de línea con la cantidad correcta de pestañas y marcadores de posición de campo: "{}\t{}\t{}....\t{}"y luego la usaremos .format()para completar los valores de 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)

Función completa en esencia aquí .

Bart Jonk
fuente
0

Si está utilizando SQLAlchemy, no necesita meterse a mano en la elaboración de la cadena porque SQLAlchemy admite la generación de una VALUEScláusula de varias filas para una sola INSERTdeclaración :

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)
Jeff Widman
fuente
Bajo el capó SQLAlchemy utiliza executemany () de psychopg2 para llamadas como esta y, por lo tanto, esta respuesta tendrá graves problemas de rendimiento para consultas grandes. Consulte el método de ejecución docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
2
No creo que ese sea el caso. Ha pasado un poco desde que vi esto, pero IIRC, en realidad está construyendo una sola declaración de inserción en la insert_querylínea. Entonces, session.execute()solo llama a la execute()declaración de psycopg2 con una sola cadena masiva. Entonces, el "truco" es construir primero todo el objeto de la declaración de inserción. Estoy usando esto para insertar 200,000 filas a la vez y vi aumentos masivos de rendimiento usando este código en comparación con lo normal executemany().
Jeff Widman
1
El documento de SQLAlchemy al que se vinculó tiene una sección que muestra exactamente cómo funciona esto e incluso dice: "Es esencial tener en cuenta que pasar varios valores NO es lo mismo que usar el formulario executemany () tradicional". Por lo tanto, explícitamente dice que esto funciona.
Jeff Widman el
1
Estoy corregido. No noté su uso del método de valores () (sin él, SQLAlchemy solo ejecuta muchas). Diría que edite la respuesta para incluir un enlace a ese documento para que pueda cambiar mi voto, pero obviamente ya lo ha incluido. ¿Quizás mencionar que esto no es lo mismo que llamar a insert () con execute () con una lista de dictados?
sage88
¿Cómo funciona en comparación con execute_values?
MrR
0

ejecutar_batch se ha agregado a psycopg2 desde que se publicó esta pregunta.

Es más lento que execute_values pero más simple de usar.

gerardw
fuente
2
Ver otros comentarios. El método de psycopg2 execute_valueses más rápido queexecute_batch
Fierr
0

Muchos de ellos aceptan un conjunto de tuplas

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()
Grigory
fuente
-1

Si desea insertar varias filas dentro de un estado de inserción (suponiendo que no esté utilizando ORM), la forma más fácil hasta ahora para mí sería utilizar la lista de diccionarios. Aquí hay un ejemplo:

 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)

Como puede ver, solo se ejecutará una consulta:

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
Alex
fuente
Mostrar el registro desde el motor de sqlalchemy NO es una demostración de ejecutar solo una consulta, solo significa que el motor de sqlalchemy ejecutó un comando. Debajo del capó esto está usando la ejecución de psychopg2, que es muy ineficiente. Consulte el método de ejecución docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
-3

Usando aiopg : el fragmento a continuación funciona perfectamente bien

    # 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)
Nihal Sharma
fuente
-4

Finalmente en la versión SQLalchemy1.2, esta nueva implementación se agrega para usar psycopg2.extras.execute_batch () en lugar de executemany cuando inicializa su motor con use_batch_mode = True como:

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

Entonces alguien tendría que usar SQLalchmey no se molestará en probar diferentes combinaciones de sqla y psycopg2 y dirigir SQL juntos.

usuario2189731
fuente