psycopg2: вставка нескольких строк одним запросом - PullRequest
114 голосов
/ 15 ноября 2011

Мне нужно вставить несколько строк одним запросом (количество строк не является постоянным), поэтому мне нужно выполнить запрос, подобный этому:

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

Единственный способ, которым я знаю, это

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)

но я хочу какой-нибудь более простой способ.

Ответы [ 13 ]

194 голосов
/ 13 апреля 2012

Я создал программу, которая вставляет несколько строк на сервер, расположенный в другом городе.

Я обнаружил, что использование этого метода было примерно в 10 раз быстрее, чем executemany.В моем случае tup - это кортеж, содержащий около 2000 строк.При использовании этого метода потребовалось около 10 секунд:

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) 

и 2 минуты при использовании этого метода:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)
121 голосов
/ 22 июня 2015

Новый execute_values метод в 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
)

Питонический способ сделать это в 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)

Объяснение: Если данные для вставки представлены в виде списка кортежей, как в

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

тогда он уже находится в точном требуемом формате как

  1. синтаксис values предложения insert ожидает список записей, как в

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

  2. Psycopg адаптирует Python tuple к Postgresql record.

Единственная необходимая работа - предоставить шаблон списка записей, который будет заполнен psycopg

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

и поместите его в insert запрос

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

Печать insert_query выходов

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

Теперь к обычной Psycopg замене аргументов

cursor.execute(insert_query, data)

Или просто тестирование того, что будет отправлено на сервер

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

Выход:

insert into t (a, b) values (1, 'x'),(2, 'y')
46 голосов
/ 19 августа 2016

Обновление с psycopg2 2.7:

Классический executemany() примерно в 60 раз медленнее, чем реализация @ ant32 (называемая «свернутой»), как объясняется в этой теме: https://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

Эта реализация была добавлена ​​в psycopg2 в версии 2.7 и называется 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)])

Предыдущий ответ:

Чтобы вставить несколько строк, использование многострочного синтаксиса VALUES с execute() примерно в 10 раз быстрее, чем при использовании psycopg2 executemany(). Действительно, executemany() просто запускает множество отдельных INSERT операторов.

Код

@ ant32 отлично работает в Python 2. Но в Python 3 cursor.mogrify() возвращает байты, cursor.execute() принимает либо байты, либо строки, а ','.join() ожидает str экземпляр.

Так что в Python 3 вам может понадобиться изменить код @ ant32, добавив .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)

Или используя только байты (с b'' или 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) 
23 голосов
/ 14 декабря 2011

Фрагмент страницы учебника Psycopg2 по адресу Postgresql.org (см. Внизу) :

Последний пункт, который я хотел бы показать вам, это как вставить несколько строк, используя словарь. Если у вас было следующее:

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

Вы можете легко вставить все три строки в словарь, используя:

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

Это не экономит много кода, но, безусловно, выглядит лучше.

21 голосов
/ 09 июня 2015

cursor.copy_from - самое быстрое решение, которое я нашел для массовых вставок на сегодняшний день. Вот суть Я сделал содержащий класс с именем IteratorFile, который позволяет итератору, дающему строки, читаться как файл. Мы можем преобразовать каждую входную запись в строку, используя выражение генератора. Таким образом, решение будет

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

Для этого тривиального размера аргументов не будет большой разницы в скорости, но я вижу большие ускорения при работе с тысячами + строк. Это также будет более эффективно использовать память, чем создание гигантской строки запроса. Итератор будет хранить только одну входную запись в памяти за раз, когда в какой-то момент вам не хватит памяти в вашем процессе Python или в Postgres, создав строку запроса.

6 голосов
/ 04 мая 2015

Все эти методы называются «расширенными вставками» в терминологии Postgres, и по состоянию на 24 ноября 2016 года они все еще работают намного быстрее, чем executemany () psychopg2 и все другие методы, перечисленные в этой теме (которые я пробовал ранее)приходит к этому ответу).

Вот некоторый код, который не использует cur.mogrify и который хорош и прост в использовании:

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

Но следует отметить, что если вы можетеиспользуйте copy_from (), вы должны использовать copy_from;)

3 голосов
/ 06 апреля 2018

Чтобы красиво вставить в БД список строк, используя заданный пользователем размер пакета и с помощью 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()

Если вы хотите, чтобы UPSERT (Insert + Update) также в postgres с пакетами: postgres_utilities

1 голос
/ 27 июля 2017

Я использую ответ ant32 выше в течение нескольких лет.Однако я обнаружил, что это ошибка в Python 3, потому что mogrify возвращает строку байтов.

Преобразование явно в строки байтов является простым решением для обеспечения совместимости кода 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 голос
/ 18 октября 2015

Еще один приятный и эффективный подход - передать строки для вставки в виде 1 аргумента, который является массивом объектов json.

Например, вы передаете аргумент:

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

Это массив,который может содержать любое количество объектов внутри.Тогда ваш SQL будет выглядеть так:

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

Примечание: ваш postgress должен быть достаточно новым, чтобы поддерживать json

0 голосов
/ 02 января 2018

Наконец, в версии SQLalchemy1.2 эта новая реализация добавлена ​​для использования psycopg2.extras.execute_batch () вместо executemany, когда вы инициализируете свой движок с помощью use_batch_mode = True, например:

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

Тогда кто-то должен будет использовать SQLalchmey, не потрудитесь попробовать разные комбинации sqla и psycopg2 и прямого SQL вместе ...

...