web-dev-qa-db-fra.com

psycopg2: insère plusieurs lignes avec une requête

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.

117
Sergey Fedoseev

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)
198
ant32

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

  1. la syntaxe values de la clause insert attend une liste d'enregistrements comme dans

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

  2. 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')
124
Clodoaldo Neto

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

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.

23
ptrn

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.

21
Joseph Sheedy

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

6
J.J

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

2
MANU

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.

1
Daniel Garmoshka

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

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)
0
Jeff Widman