psycopg2: insérer plusieurs lignes avec une seule requête

141

J'ai besoin d'insérer plusieurs lignes avec une requête (le nombre de lignes n'est pas constant), je dois donc exécuter une requête comme celle-ci:

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

La seule façon dont je sais 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.

Sergey Fedoseev
la source

Réponses:

219

J'ai construit un programme qui insère plusieurs lignes sur un serveur situé dans une autre ville.

J'ai découvert que l'utilisation de cette méthode était environ 10 fois plus rapide que executemany. Dans mon cas, il tups'agit d'un tuple contenant environ 2000 lignes. Cela a pris environ 10 secondes lors de l'utilisation de 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 avec cette méthode:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)
fourmi32
la source
15
Toujours très pertinent près de deux ans plus tard. Une expérience d'aujourd'hui suggère que plus le nombre de lignes que vous souhaitez pousser augmente, mieux il est d'utiliser la executestratégie. J'ai vu une accélération d'environ 100x grâce à cela!
Rob Watts
4
Peut executemany- être exécute un commit après chaque insertion. Si vous enveloppez le tout dans une transaction, cela accélérerait peut-être les choses?
Richard
4
Je viens de confirmer cette amélioration moi-même. D'après ce que j'ai lu, psycopg2 executemanyne fait rien de optimal, il boucle juste et fait de nombreuses executedéclarations. En utilisant cette méthode, une insertion de 700 lignes vers un serveur distant est passée de 60 s à <2 s.
Nelson
5
Peut-être que je suis paranoïaque, mais concaténer la requête avec un +semble pouvoir s'ouvrir à une injection SQL, j'ai l'impression que la execute_values()solution @Clodoaldo Neto est plus sûre.
Will Munn
26
au cas où quelqu'un rencontre l'erreur suivante: [TypeError: élément de séquence 0: instance de str attendue, octets trouvés] exécutez cette commande à la place [args_str = ','. join (cur.mogrify ("(% s,% s)", x ) .decode ("utf-8") for x in tup)]
mrt
147

Nouvelle execute_valuesméthode 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 manière pythonique de le faire 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)

Explication: Si les données à insérer sont données sous forme de liste de tuples comme dans

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

alors il est déjà dans le format exact requis comme

  1. la valuessyntaxe de la insertclause attend une liste d'enregistrements comme dans

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

  2. Psycopgadapte un 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 placez-le dans la insertrequête

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

Impression des insert_querysorties

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

Passons maintenant à la Psycopgsubstitution d'arguments habituelle

cursor.execute(insert_query, data)

Ou simplement tester ce qui sera envoyé au serveur

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

Production:

insert into t (a, b) values (1, 'x'),(2, 'y')
Clodoaldo Neto
la source
1
Comment les performances de cette méthode se comparent-elles à cur.copy_from?
Michael Goldshteyn
1
Voici l'essentiel avec un repère . copy_from évolue jusqu'à environ 6,5 fois plus vite sur ma machine avec 10 millions d'enregistrements.
Joseph Sheedy
Ça a l'air sympa - je pense que vous avez un parasite, à la fin de votre définition initiale de insert_query (à moins que vous n'essayiez d'en faire un tuple?) Et qu'il manque comme après le% for% s également dans la définition initiale de insert_query.
deadcode
2
en utilisant, execute_valuesj'ai pu faire fonctionner mon système à 1k enregistrements par minute jusqu'à 128k enregistrements par minute
Conrad.Dean
66

Mise à jour avec psycopg2 2.7:

Le classique executemany()est environ 60 fois plus lent que l'implémentation de @ ant32 (appelée "pliée") comme expliqué dans ce fil: 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 VALUESsyntaxe multirow avec execute()est environ 10 fois plus rapide que l'utilisation de psycopg2 executemany(). En effet, executemany()exécute juste de nombreuses INSERTdéclarations individuelles .

Le code de @ ant32 fonctionne parfaitement en Python 2. Mais en Python 3, cursor.mogrify()retourne des octets, cursor.execute()prend des octets ou des chaînes et ','.join()attend une strinstance.

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 uniquement des octets (avec b''ou 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) 
Antoine Dusséaux
la source
26

curseur.copy_from est de loin la solution la plus rapide que j'ai trouvée pour les insertions en vrac. Voici l'essentiel que j'ai créé contenant une classe nommée IteratorFile qui permet à un itérateur produisant des chaînes d'être lu comme un fichier. Nous pouvons convertir chaque enregistrement d'entrée en une chaîne à l'aide d'une expression de générateur. 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 des milliers + de lignes. Il sera également plus efficace en termes de mémoire que de créer une chaîne de requête géante. Un itérateur ne conserverait jamais qu'un seul enregistrement d'entrée en mémoire à la fois, où à un moment donné vous manquerez de mémoire dans votre processus Python ou dans Postgres en construisant la chaîne de requête.

Joseph Sheedy
la source
3
Voici un benchmark comparant copy_from / IteratorFile avec une solution de générateur de requêtes. copy_from évolue jusqu'à environ 6,5 fois plus vite sur ma machine avec 10 millions d'enregistrements.
Joseph Sheedy
3
est-ce que vous devez vous amuser avec des chaînes et des horodatages qui s'échappent, etc.?
CpILL
Oui, vous devrez vous assurer que vous avez des enregistrements TSV bien formés.
Joseph Sheedy
24

Un extrait de la page de tutoriel de Psycopg2 sur Postgresql.org (voir en bas) :

Un dernier élément que je voudrais vous montrer est comment insérer plusieurs lignes à l'aide d'un dictionnaire. Si vous aviez ce qui suit:

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 dans le dictionnaire en utilisant:

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

Cela n'économise pas beaucoup de code, mais il est définitivement meilleur.

ptrn
la source
35
Cela exécutera de nombreuses INSERTdéclarations individuelles . Utile, mais pas la même chose qu'un seul VALUEinsert multi- d.
Craig Ringer
7

Toutes ces techniques sont appelées «insertions étendues» dans la terminologie Postgres, et à partir du 24 novembre 2016, c'est toujours une tonne plus rapide que l'executemany () de psychopg2 et toutes les autres méthodes répertoriées dans ce fil (que j'ai essayé avant d'en venir à cela) répondre).

Voici un code qui n'utilise pas cur.mogrify et qui est agréable et simple à comprendre:

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 il faut noter que si vous pouvez utiliser copy_from (), vous devez utiliser copy_from;)

JJ
la source
Ressusciter d'entre les morts, mais que se passe-t-il dans la situation des derniers rangs? Je suppose que vous exécutez à nouveau cette clause finale sur les dernières lignes restantes, dans le cas où vous avez un nombre pair de lignes?
mcpeterson
Correct, désolé, j'ai dû oublier de faire ça quand j'ai écrit l'exemple - c'est assez stupide de ma part. Ne pas le faire n'aurait pas donné aux gens une erreur, ce qui m'inquiète du nombre de personnes qui ont copié / collé la solution et se sont lancées dans leurs affaires ..... Quoi qu'il en soit, mcpeterson très reconnaissant - merci!
JJ
2

J'utilise la réponse de ant32 ci-dessus depuis plusieurs années. Cependant, j'ai trouvé que c'est une erreur dans python 3 car mogrifyrenvoie une chaîne d'octets.

La conversion explicite en chaînes bytse est une solution simple pour rendre le code compatible 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)
jprockbelly
la source
1

Une autre approche agréable et efficace consiste à passer des lignes à insérer en tant qu'argument 1, qui est un tableau d'objets json.

Par exemple, vous passez un 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 postgress doit être suffisamment nouveau pour prendre en charge json

Daniel Garmoshka
la source
1

La solution cursor.copyfrom fournie par @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) ci-dessus ( https://stackoverflow.com/a/30721460/11100064 ) est en effet ultra-rapide.

Cependant, les exemples qu'il donne ne sont pas utilisables de manière générique pour un enregistrement avec un nombre quelconque de champs et il m'a fallu du temps pour comprendre comment l'utiliser correctement.

Le IteratorFile doit être instancié avec des champs séparés par des tabulations comme celui-ci ( rest une liste de dictionnaires où chaque dict est un enregistrement):

    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)

Pour généraliser pour un nombre arbitraire de champs, nous allons d'abord créer une chaîne de ligne avec le nombre correct d'onglets et d'espaces réservés de champ: "{}\t{}\t{}....\t{}"puis utiliser .format()pour remplir les valeurs de champ *list(r.values())) for r in records::

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

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

fonction complète dans essentiel ici .

Bart Jonk
la source
0

Si vous utilisez SQLAlchemy, vous n'avez pas besoin de jouer avec la création manuelle de la chaîne, car SQLAlchemy prend en charge la génération d'une VALUESclause à plusieurs lignes pour une seule INSERTinstruction :

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
la source
Sous le capot, SQLAlchemy utilise l'executemany () de psychopg2 pour des appels comme celui-ci et cette réponse aura donc de graves problèmes de performances pour les requêtes volumineuses. Voir la méthode d'exécution docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
2
Je ne pense pas que ce soit le cas. Cela fait un peu que j'ai regardé cela, mais IIRC, c'est en fait la construction d'une seule instruction d'insertion dans la insert_queryligne. Ensuite, il session.execute()suffit d'appeler la execute()déclaration de psycopg2 avec une seule chaîne massive. Donc, le "truc" consiste à construire tout d'abord l'objet de l'instruction d'insertion. J'utilise ceci pour insérer 200 000 lignes à la fois et j'ai vu des performances massives augmenter en utilisant ce code par rapport à la normale executemany().
Jeff Widman
1
Le document SQLAlchemy auquel vous avez lié a une section qui montre exactement comment cela fonctionne et dit même: "Il est essentiel de noter que passer plusieurs valeurs n'est PAS la même chose que d'utiliser le formulaire traditionnel executemany ()". Donc, il appelle explicitement que cela fonctionne.
Jeff Widman
1
Je me suis trompé. Je n'ai pas remarqué votre utilisation de la méthode values ​​() (sans elle, SQLAlchemy exécute juste beaucoup). Je dirais que modifiez la réponse pour inclure un lien vers ce document afin que je puisse modifier mon vote, mais vous l'avez évidemment déjà inclus. Peut-être mentionner que ce n'est pas la même chose que d'appeler un insert () avec execute () avec une liste de dictées?
sage88
comment fonctionne-t-il par rapport à execute_values?
MrR
0

execute_batch a été ajouté à psycopg2 depuis que cette question a été publiée.

Il est plus lent que execute_values mais plus simple à utiliser.

Gérardw
la source
2
Voir les autres commentaires. La méthode de psycopg2 execute_valuesest plus rapide queexecute_batch
Fierr
0

executemany accepte un tableau de tuples

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
la source
-1

Si vous souhaitez insérer plusieurs lignes dans un même état d'insertion (en supposant que vous n'utilisez pas ORM), le moyen le plus simple jusqu'à présent pour moi serait d'utiliser une liste de dictionnaires. Voici un exemple:

 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)

Comme vous pouvez le voir, une seule requête sera exécutée:

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
la source
1
L'affichage de la journalisation depuis le moteur sqlalchemy n'est PAS une démonstration de l'exécution d'une seule requête, cela signifie simplement que le moteur sqlalchemy a exécuté une commande. Sous le capot, cela utilise l'exécutif de psychopg2 qui est très inefficace. Voir la méthode d'exécution docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
-3

Utilisation de aiopg - L'extrait ci-dessous fonctionne parfaitement

    # 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
la source
-4

Enfin dans la version SQLalchemy1.2, cette nouvelle implémentation est ajoutée pour utiliser psycopg2.extras.execute_batch () au lieu d'executemany lorsque vous initialisez votre moteur avec use_batch_mode = True comme:

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

Ensuite, quelqu'un devrait utiliser SQLalchmey ne prend pas la peine d'essayer différentes combinaisons de sqla et psycopg2 et de SQL direct ensemble.

user2189731
la source