J'ai besoin d'insérer plusieurs lignes avec une requête (le nombre de lignes n'est pas constant), j'ai donc besoin d'exécuter une requête comme celle-ci:
INSERT INTO t (a, b) VALUES (1, 2), (3, 4), (5, 6);
Le seul moyen que je connaisse est
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)
mais je veux un moyen plus simple.
J'ai construit un programme qui insère plusieurs lignes sur un serveur situé dans une autre ville.
J'ai découvert que cette méthode était environ 10 fois plus rapide que executemany
. Dans mon cas, tup
est un tuple contenant environ 2000 lignes. Cela a pris environ 10 secondes avec cette méthode:
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)
et 2 minutes en utilisant cette méthode:
cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)
Nouvelle méthode execute_values
dans 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 façon de le faire en Pythonic dans 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)
Explanation: Si les données à insérer sont données sous forme de liste de n-uplets comme dans
data = [(1,'x'), (2,'y')]
alors il est déjà dans le format exact requis
la syntaxe values
de la clause insert
attend une liste d'enregistrements comme dans
insert into t (a, b) values (1, 'x'),(2, 'y')
Psycopg
adapte a Python Tuple
à un Postgresql record
.
Le seul travail nécessaire est de fournir un modèle de liste d'enregistrements à remplir par psycopg.
# We use the data list to be sure of the template length
records_list_template = ','.join(['%s'] * len(data))
et le placer dans la requête insert
insert_query = 'insert into t (a, b) values {}'.format(records_list_template)
Imprimer les sorties insert_query
insert into t (a, b) values %s,%s
Passons maintenant à la substitution habituelle des arguments Psycopg
cursor.execute(insert_query, data)
Ou simplement tester ce qui sera envoyé au serveur
print (cursor.mogrify(insert_query, data).decode('utf8'))
Sortie:
insert into t (a, b) values (1, 'x'),(2, 'y')
Mise à jour avec psycopg2 2.7:
Le classique executemany()
est environ 60 fois plus lent que l'implémentation de @ ant32 (appelé "replié"), comme expliqué dans ce fil de discussion: https://www.postgresql.org/message-id/ 20170130215151.GA7081% 40deb76.aryehleib.com
Cette implémentation a été ajoutée à psycopg2 dans la version 2.7 et s'appelle 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)])
Réponse précédente:
Pour insérer plusieurs lignes, l'utilisation de la syntaxe multirow VALUES
avec execute()
est environ 10 fois plus rapide que l'utilisation de psycopg2 executemany()
. En effet, executemany()
exécute simplement plusieurs instructions individuelles INSERT
.
Le code de @ ant32 fonctionne parfaitement dans Python 2. Mais dans Python 3, cursor.mogrify()
renvoie des octets, cursor.execute()
prend des octets ou des chaînes, et ','.join()
s'attend à une instance str
.
Donc, dans Python 3, vous devrez peut-être modifier le code de @ ant32 en ajoutant .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)
Ou en utilisant des octets (avec b''
Ou b""
) Uniquement:
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)
Un extrait de la page du tutoriel de Psycopg2 à l'adresse Postgresql.org (voir ci-dessous) :
Un dernier élément que je voudrais vous montrer est comment insérer plusieurs lignes à l'aide d'un dictionnaire. Si vous aviez:
namedict = ({"first_name":"Joshua", "last_name":"Drake"},
{"first_name":"Steven", "last_name":"Foo"},
{"first_name":"David", "last_name":"Bar"})
Vous pouvez facilement insérer les trois lignes du dictionnaire en utilisant:
cur = conn.cursor()
cur.executemany("""INSERT INTO bar(first_name,last_name) VALUES (%(first_name)s, %(last_name)s)""", namedict)
Cela ne sauve pas beaucoup de code, mais il a définitivement l’air meilleur.
cursor.copy_from est de loin la solution la plus rapide que j'ai trouvée pour les insertions en bloc. Voici un résumé J'ai créé une classe nommée IteratorFile qui permet à un itérateur de générer des chaînes à lire comme un fichier. Nous pouvons convertir chaque enregistrement d'entrée en chaîne à l'aide d'une expression génératrice. Donc, la solution serait
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'))
Pour cette taille triviale d'arguments, cela ne fera pas beaucoup de différence de vitesse, mais je vois de grandes accélérations lorsque l'on traite plusieurs milliers de lignes. La mémoire sera également plus efficace que la création d’une chaîne de requête géante. Un itérateur ne conservera jamais qu'un seul enregistrement d'entrée en mémoire à la fois, où vous manquerez de mémoire dans votre processus Python ou dans Postgres en créant la chaîne de requête.
Toutes ces techniques sont appelées "Extended Inserts" dans la terminologie de Postgres, et depuis le 24 novembre 2016, elles sont encore beaucoup plus rapides que executemany () de psychopg2 et toutes les autres méthodes répertoriées dans ce fil de discussion (que j'ai essayé avant d'arriver à cette répondre).
Voici un code qui n’utilise pas cur.mogrify, c’est gentil et tout simplement pour se faire une idée:
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()
Mais notez que si vous pouvez utiliser copy_from (), vous devez utiliser copy_from;)
Pour bien insérer dans la base de données une liste de lignes, en utilisant la taille de lot donnée par l'utilisateur et avec psycopg2!
def get_batch(iterable, size=100):
for i in range(0, len(iterable), size):
yield iterable[i: i + size]
def insert_rows_batch(table, rows, batch_size=500, target_fields=None):
"""
A utility method to insert batch of tuples(rows) into a table
NOTE: Handle data type for fields in rows yourself as per your table
columns' type.
:param table: Name of the target table
:type table: str
:param rows: The rows to insert into the table
:type rows: iterable of tuples
:param batch_size: The size of batch of rows to insert at a time
:type batch_size: int
:param target_fields: The names of the columns to fill in the table
:type target_fields: iterable of strings
"""
conn = cur = None
if target_fields:
target_fields = ", ".join(target_fields)
target_fields = "({})".format(target_fields)
else:
target_fields = ''
conn = get_conn() # get connection using psycopg2
if conn:
cur = conn.cursor()
count = 0
for mini_batch in get_batch(rows, batch_size):
mini_batch_size = len(mini_batch)
count += mini_batch_size
record_template = ','.join(["%s"] * mini_batch_size)
sql = "INSERT INTO {0} {1} VALUES {2};".format(
table,
target_fields,
record_template)
cur.execute(sql, mini_batch)
conn.commit()
print("Loaded {} rows into {} so far".format(count, table))
print("Done loading. Loaded a total of {} rows".format(count))
if cur:cur.close()
if conn:conn.close()
Si vous voulez UPSERT (Insert + Update) aussi bien dans postgres avec des lots: postgres_utilities
Une autre approche efficace et agréable consiste à transmettre les lignes à insérer sous la forme d'un argument (tableau d'objets json).
Par exemple. vous passez l'argument:
[ {id: 18, score: 1}, { id: 19, score: 5} ]
C'est un tableau qui peut contenir n'importe quelle quantité d'objets à l'intérieur. Ensuite, votre SQL ressemble à:
INSERT INTO links (parent_id, child_id, score)
SELECT 123, (r->>'id')::int, (r->>'score')::int
FROM unnest($1::json[]) as r
Remarque: votre post-congrès doit être suffisamment récent pour prendre en charge json.
J'utilise ant32's ci-dessus depuis plusieurs années. Cependant, j’ai trouvé que c’était une erreur dans python 3 car mogrify
renvoie une chaîne d’octets.
La conversion explicite en chaînes bytse est une solution simple pour rendre le code python 3 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)
Si vous utilisez SQLAlchemy, vous n'avez pas besoin de manipuler la chaîne manuellement car SQLAlchemy prend en charge la génération d'une clause VALUES
à plusieurs lignes pour une seule instruction INSERT
:
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)