psycopg2: вставить несколько строк одним запросом


157

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

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)

но я хочу более простой способ.

Ответы:


230

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

Я выяснил, что использование этого метода было примерно в 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)

15
Спустя почти два года все еще очень актуален. Сегодняшний опыт подсказывает, что чем больше количество строк, которые вы хотите разместить, тем лучше использовать эту executeстратегию. Благодаря этому я увидел ускорение примерно в 100 раз!
Роб Уоттс,

4
Возможно, executemanyзапускает фиксацию после каждой вставки. Если вместо этого вы оберните все это в транзакцию, может быть, это ускорит процесс?
Ричард

4
Только что подтвердил это улучшение сам. Из того, что я читал, psycopg2 executemanyне делает ничего оптимального, просто зацикливается и выполняет множество executeоператоров. Используя этот метод, время вставки 700 строк на удаленный сервер уменьшилось с 60 до <2 с.
Нельсон

5
Возможно, я параноик, но, объединив запрос с a, +кажется, что он может открыться для SQL-инъекции, я чувствую, что execute_values()решение @Clodoaldo Neto безопаснее.
Уилл Манн

33
в случае, если кто-то обнаружит следующую ошибку: [TypeError: элемент последовательности 0: ожидаемый экземпляр str, найдены байты] вместо этого выполните эту команду [args_str = ','. join (cur.mogrify ("(% s,% s)", x ) .decode ("utf-8") for x in tup)]
мрт

166

Новый 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')

1
Как производительность этого метода сравнивается с cur.copy_from?
Михаил Гольдштейн

1
Вот суть теста . copy_from масштабируется примерно до 6,5 раз быстрее на моей машине с 10M записями.
Джозеф Шиди

Выглядит неплохо - я думаю, у вас есть случайная ошибка в конце вашего первоначального определения insert_query (если вы не пытались сделать его кортежем?), И она отсутствует, как и после% for% s, также в исходном определении insert_query.
дедкод

2
с помощью execute_valuesя смог заставить мою систему работать со скоростью от 1k записей в минуту до 128k записей в минуту,
Конрад. Дин

1
Мои вставки не регистрировались должным образом, пока я не позвонил connection.commit()после execute_values(...).
Филипп

69

Обновление с 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) 

28

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, построив строку запроса.


3
Вот тест, сравнивающий copy_from / IteratorFile с решением построителя запросов. copy_from масштабируется примерно до 6,5 раз быстрее на моей машине с 10M записями.
Джозеф Шиди

3
вам нужно возиться с экранирующими строками, отметками времени и т. д.?
CpILL

Да, вам нужно убедиться, что у вас есть хорошо сформированные записи TSV.
Джозеф Шиди

25

Фрагмент страницы руководства 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)

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


37
Это приведет к запуску множества отдельных INSERTоператоров. Полезно, но не то же самое, что одинарная многослойная VALUEпластина.
Craig Ringer

7

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

Вот код, который не использует 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;)


Воскрешение из мертвых, но что происходит в последних рядах? Я предполагаю, что вы действительно запускаете это последнее предложение еще раз для последних оставшихся строк, если у вас четное количество строк?
mcpeterson

Правильно, извините, я, должно быть, забыл это сделать, когда писал пример - это довольно глупо с моей стороны. Если этого не сделать, у людей не возникнет ошибка, что заставляет меня беспокоиться, сколько людей скопировали / вставили решение и занялись своими делами ..... В любом случае, очень благодарен Макпетерсон - спасибо!
JJ

4

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

Явное преобразование в строки bytse - простое решение для обеспечения совместимости кода с 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)

Более простой вариант - расшифроватьcur.mogrify()
Hemil

1

Еще один приятный и эффективный подход - передать строки для вставки в качестве 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


1

Если вы используете SQLAlchemy, вам не нужно возиться с ручной обработкой строки, потому что SQLAlchemy поддерживает создание многострочного VALUESпредложения для одного 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)

Под капотом SQLAlchemy использует executemany () psychopg2 для подобных вызовов, и поэтому этот ответ будет иметь серьезные проблемы с производительностью для больших запросов. См. Метод execute docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88

2
Я не думаю, что это так. Прошло немного времени с тех пор, как я смотрел на это, но IIRC, это фактически создает один оператор вставки в insert_queryстроке. Затем session.execute()просто вызывает оператор psycopg2 execute()с одной массивной строкой. Таким образом, «трюк» состоит в том, чтобы сначала построить весь объект оператора вставки. Я использую это для вставки 200 000 строк за раз, и увидел значительное увеличение производительности с использованием этого кода по сравнению с обычным executemany().
Джефф Видман

1
Документ SQLAlchemy, на который вы ссылаетесь, имеет раздел, который точно показывает, как это работает, и даже говорит: «Важно отметить, что передача нескольких значений НЕ совпадает с использованием традиционной формы executemany ()». Таким образом, он явно указывает на то, что это работает.
Джефф Видман

1
Я исправился. Я не заметил, как вы используете метод values ​​() (без него SQLAlchemy просто выполняет выполнение). Я бы сказал, отредактируйте ответ, включив ссылку на этот документ, чтобы я мог изменить свой голос, но, очевидно, вы уже включили его. Возможно, упомяните, что это не то же самое, что вызов insert () с помощью execute () со списком dicts?
sage88

как он работает по сравнению с execute_values?
MrR

1

Executemany принимает массив кортежей

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

1

Решение cursor.copyfrom, предоставленное @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) выше ( https://stackoverflow.com/a/30721460/11100064 ), действительно работает молниеносно.

Однако приведенный им пример в общем случае нельзя использовать для записи с любым количеством полей, и мне потребовалось время, чтобы понять, как его правильно использовать.

IteratorFile должен быть создан с полями, разделенными табуляцией, как это ( rэто список dicts, где каждый dict является записью):

    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)

Чтобы обобщить произвольное количество полей, мы сначала создадим строку строки с правильным количеством вкладок и заполнителей полей: "{}\t{}\t{}....\t{}"а затем воспользуемся .format()для заполнения значений полей *list(r.values())) for r in records:

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

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

полная функция здесь .


0

execute_batch был добавлен в psycopg2 с момента публикации этого вопроса.

Он медленнее, чем execute_values, но его проще использовать.


2
См. Другие комментарии. Метод psycopg2 в execute_valuesэто быстрее , чемexecute_batch
Fierr

-1

Если вы хотите вставить несколько строк в одно состояние вставки (при условии, что вы не используете ORM), то для меня самым простым способом было бы использовать список словарей. Вот пример:

 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)

Как видите, будет выполнен только один запрос:

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

1
Отображение ведения журнала из движка sqlalchemy НЕ является демонстрацией выполнения только одного запроса, это просто означает, что механизм sqlalchemy выполнил одну команду. Под капотом это очень неэффективное выполнение программы psychopg2. См. Метод execute docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88

-3

Использование aiopg - приведенный ниже фрагмент отлично работает

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


-5

Наконец, в версии 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 вместе ..

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.