psycopg2: вставити кілька рядків за допомогою одного запиту


141

Мені потрібно вставити кілька рядків з одним запитом (кількість рядків не є постійною), тому мені потрібно виконати такий запит:

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)

але я хочу більш простий спосіб.

Відповіді:


219

Я створив програму, яка вставляє кілька рядків на сервер, який знаходився в іншому місті.

Я з’ясував, що використання цього методу було приблизно в 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
Можливо, я параноїдний, але, поєднуючи запит, +схоже, він може відкрити до sql введення, я відчуваю, що execute_values()рішення @Clodoaldo Neto є більш безпечним.
Буде Манн

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

147

Новий 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.5X швидше на моїй машині з 10M записами.
Джозеф Шеді

Виглядає приємно - я думаю, що у вас є заблуканий, наприкінці початкового визначення insert_query (якщо ви не намагалися зробити його кортежем?) І відсутній як після% для% s також у початковому визначенні insert_query.
мертвий код

2
за допомогою execute_valuesмене вдалося змусити мою систему працювати на 1 к записів за хвилину до 128 к записів на хвилину
Conrad.Dean

66

Оновлення з 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) 

26

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.5X швидше на моїй машині з 10M записами.
Джозеф Шеді

3
чи потрібно вам обмінятись утечевими рядками та мітками тощо?
CpILL

Так, вам доведеться переконатися, що у вас є добре сформовані записи TSV.
Джозеф Шеді

24

Фрагмент зі сторінки підручника 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)

Це не економить багато коду, але остаточно виглядає краще.


35
Це запустить багато окремих INSERTзаяв. Корисно, але не те саме, що одна VALUEвкладка з декількома dми.
Крейг Рінгер

7

Усі ці методи в термінології Postgres називаються "розширеними вставками", і станом на 24 листопада 2016 року це все-таки на тонну швидше, ніж виконавча система 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;)


Виховання з мертвих, але що відбувається в ситуації останніх кількох рядів? Я припускаю, що ви фактично запускаєте цей остаточний пункт ще раз в останніх рядках, якщо у вас є парна кількість рядків?
mcpeterson

Правильно, вибачте, що я, мабуть, забув це зробити, коли я писав приклад, - це досить дурно про мене. Якщо цього не зробити, люди не помилилися б, через що я переживаю, скільки людей копіює / вставляє рішення та займається своєю справою ..... У будь-якому випадку, дуже вдячний mcpeterson - дякую!
JJ

2

Я вже кілька років використовую відповідь 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)

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 

Примітка: Ваш постгрес повинен бути досить новим, щоб підтримувати json


1

Рішення cursor.copyfrom , надане @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) вище ( https://stackoverflow.com/a/30721460/11100064 ), дійсно блискавично.

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

IteratorFile потрібно інстанціювати поділеними вкладками полями на зразок цього ( rце список диктовок, де кожен дікт є записом):

    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

Якщо ви використовуєте 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 використовує виконавчий файл psychopg2 () для подібних дзвінків, і тому ця відповідь матиме серйозні проблеми з продуктивністю для великих запитів. Дивіться метод виконання docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88

2
Я не думаю, що це так. Минуло трохи, як я переглянув це, але IIRC, це насправді будує єдину операцію вставки в insert_queryрядку. Тоді, session.execute()просто називаємо execute()заяву psycopg2 однією масивною рядком. Отже, "хитрість" спочатку будує весь об'єкт оператора вставки. Я використовую це, щоб вставити 200 000 рядків за один раз і побачив значне підвищення продуктивності за допомогою цього коду порівняно зі звичайним executemany().
Джефф Відман

1
Документ SQLAlchemy, до якого ви пов’язані, має розділ, який показує, як саме це працює, і навіть каже: "Важливо зазначити, що передача декількох значень НЕ є такою ж, як використання традиційної форми Executemany ()". Тож явно закликає, що це працює.
Джефф Відман

1
Я стою виправлений. Я не помітив вашого використання методу значень () (без нього SQLAlchemy просто виконує). Я б сказав, відредагуйте відповідь, щоб включити посилання на цей документ, щоб я міг змінити свій голос, але, очевидно, ви вже включили його. Можливо, зауважте, що це не те саме, що викликати вставку () з Execute () зі списком диктів?
sage88

як це працює порівняно з Execute_values?
MrR

0

Execute_batch додано до psycopg2 з моменту опублікування цього питання.

Він повільніше, ніж Execute_values, але простіший у використанні.


2
Дивіться інші коментарі. Метод psycopg2 в execute_valuesце швидше , ніжexecute_batch
Fierr

0

Виконати прийняти масив кортежів

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

Якщо ви хочете вставити декілька рядків у межах однієї таблиці вставки (припустимо, що ви не використовуєте 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

Показано ведення журналу з двигуна sqlalchemy НЕ демонстрація запуску одного запиту, це просто означає, що движок sqlalchemy виконував одну команду. Під кришкою для цього використовується виконавча система psychopg2, що дуже неефективно. Дивіться метод виконання 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)


-4

Нарешті, у версії 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.