psycopg2: masukkan beberapa baris dengan satu query

141

Saya perlu memasukkan beberapa baris dengan satu kueri (jumlah baris tidak konstan), jadi saya perlu menjalankan kueri seperti ini:

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

Satu-satunya cara saya tahu adalah

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)

tapi saya ingin cara yang lebih sederhana.

Sergey Fedoseev
sumber

Jawaban:

219

Saya membangun sebuah program yang menyisipkan banyak baris ke server yang terletak di kota lain.

Saya menemukan bahwa menggunakan metode ini sekitar 10 kali lebih cepat daripada executemany. Dalam kasus saya tupadalah tuple yang berisi sekitar 2000 baris. Butuh sekitar 10 detik saat menggunakan metode ini:

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) 

dan 2 menit saat menggunakan metode ini:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)
ant32
sumber
15
Masih sangat relevan hampir dua tahun kemudian. Pengalaman hari ini menunjukkan bahwa semakin banyak baris yang ingin Anda dorong, semakin baik menggunakan executestrategi. Saya melihat percepatan sekitar 100x berkat ini!
Rob Watts
4
Mungkin executemanymenjalankan komit setelah setiap sisipan. Jika Anda malah membungkus semuanya dalam suatu transaksi, mungkin itu akan mempercepat hal-hal?
Richard
4
Baru saja mengkonfirmasi peningkatan ini sendiri. Dari apa yang saya baca psycopg2 executemanytidak melakukan sesuatu yang optimal, hanya loop dan melakukan banyak executepernyataan. Dengan menggunakan metode ini, sisipan 700 baris ke server jarak jauh berubah dari 60-an menjadi <2s.
Nelson
5
Mungkin saya menjadi paranoid, tetapi menyatukan kueri dengan +sepertinya bisa membuka injeksi sql, saya merasa execute_values()solusi @Clodoaldo Neto lebih aman.
Will Munn
26
dalam kasus seseorang menemukan kesalahan berikut: [TypeError: item urutan 0: str diharapkan contoh, byte ditemukan] jalankan perintah ini sebagai gantinya [args_str = ','. join (cur.mogrify ("(% s,% s)", x ) .decode ("utf-8") untuk x dalam tup)]
mrt
146

execute_valuesMetode baru di 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
)

Cara pythonic melakukannya di 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)

Penjelasan: Jika data yang akan dimasukkan diberikan sebagai daftar tupel seperti di

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

maka sudah dalam format yang diperlukan sebagai

  1. yang valuessintaks dari insertklausul mengharapkan daftar catatan seperti di

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

  2. Psycopgmengadaptasi Python tupleke Postgresql record.

Satu-satunya pekerjaan yang diperlukan adalah menyediakan template daftar catatan untuk diisi oleh psycopg

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

dan letakkan di insertkueri

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

Mencetak insert_queryoutput

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

Sekarang dengan Psycopgsubstitusi argumen biasa

cursor.execute(insert_query, data)

Atau hanya menguji apa yang akan dikirim ke server

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

Keluaran:

insert into t (a, b) values (1, 'x'),(2, 'y')
Clodoaldo Neto
sumber
1
Bagaimana kinerja metode ini dibandingkan dengan cur.copy_from?
Michael Goldshteyn
1
Inilah intisari dengan tolok ukur . copy_from skala menjadi sekitar 6,5X lebih cepat pada mesin saya dengan catatan 10 juta.
Joseph Sheedy
Terlihat bagus - saya pikir Anda memiliki nyasar, pada akhir definisi awal Anda dari insert_query (kecuali Anda mencoba menjadikannya tuple?) Dan hilang setelah% untuk% s juga dalam definisi awal insert_query.
deadcode
2
menggunakan execute_valuessaya bisa menjalankan sistem saya pada catatan 1k per menit hingga 128k catatan per menit
Conrad.Dean
66

Perbarui dengan psycopg2 2.7:

Klasik executemany()sekitar 60 kali lebih lambat dari implementasi @ ant32 (disebut "dilipat") seperti yang dijelaskan dalam utas ini: https://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

Implementasi ini ditambahkan ke psycopg2 dalam versi 2.7 dan disebut 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)])

Jawaban sebelumnya:

Untuk menyisipkan banyak baris, menggunakan VALUESsintaks multirow dengan execute()sekitar 10x lebih cepat daripada menggunakan psycopg2 executemany(). Memang, executemany()hanya menjalankan banyak INSERTpernyataan individual .

@ ant32 's kode berfungsi dengan baik di Python 2. Tetapi dalam Python 3, cursor.mogrify()mengembalikan byte, cursor.execute()mengambil byte atau string, dan ','.join()mengharapkan strinstance.

Jadi dalam Python 3 Anda mungkin perlu memodifikasi kode @ ant32, dengan menambahkan .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)

Atau dengan menggunakan byte (dengan b''atau b"") saja:

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
sumber
26

cursor.copy_from adalah solusi tercepat yang saya temukan untuk sisipan massal sejauh ini. Inilah inti yang saya buat yang mengandung kelas bernama IteratorFile yang memungkinkan iterator menghasilkan string untuk dibaca seperti file. Kami dapat mengonversi setiap rekaman input ke string menggunakan ekspresi generator. Jadi solusinya

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

Untuk ukuran args yang sepele ini tidak akan membuat banyak perbedaan kecepatan, tapi saya melihat speedup besar ketika berhadapan dengan ribuan baris. Ini juga akan lebih hemat memori daripada membangun string kueri raksasa. Sebuah iterator hanya akan menyimpan satu catatan input dalam memori pada suatu waktu, di mana pada titik tertentu Anda akan kehabisan memori dalam proses Python Anda atau di Postgres dengan membangun string kueri.

Joseph Sheedy
sumber
3
Berikut ini adalah perbandingan membandingkan copy_from / IteratorFile dengan solusi pembuat kueri. copy_from skala menjadi sekitar 6,5X lebih cepat pada mesin saya dengan catatan 10 juta.
Joseph Sheedy
3
apakah Anda harus bermain-main dengan melarikan diri string dan cap waktu dll?
CpILL
Ya, Anda harus memastikan Anda memiliki catatan TSV yang baik.
Joseph Sheedy
24

Cuplikan dari halaman tutorial Psycopg2 di Postgresql.org (lihat bawah) :

Item terakhir yang ingin saya tunjukkan kepada Anda adalah bagaimana memasukkan beberapa baris menggunakan kamus. Jika Anda memiliki yang berikut:

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

Anda dapat dengan mudah memasukkan ketiga baris dalam kamus dengan menggunakan:

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

Itu tidak menyimpan banyak kode, tetapi secara definitif terlihat lebih baik.

ptrn
sumber
35
Ini akan menjalankan banyak INSERTpernyataan individual . Berguna, tetapi tidak sama dengan VALUEinsert multi- d tunggal .
Craig Ringer
7

Semua teknik ini disebut 'Sisipan Diperpanjang "dalam terminologi Postgres, dan pada tanggal 24 November 2016, masih satu ton lebih cepat daripada pelaksanaan psychopg2 () dan semua metode lain yang tercantum dalam utas ini (yang saya coba sebelum datang ke ini menjawab).

Berikut adalah beberapa kode yang tidak menggunakan cur.mogrify dan bagus dan hanya untuk mendapatkan kepala Anda:

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

Tetapi harus dicatat bahwa jika Anda dapat menggunakan copy_from (), Anda harus menggunakan copy_from;)

JJ
sumber
Membangkitkan dari kematian, tetapi apa yang terjadi dalam situasi beberapa baris terakhir? Saya berasumsi Anda benar-benar menjalankan klausa akhir itu lagi pada baris terakhir yang tersisa, jika Anda memiliki jumlah baris genap?
mcpeterson
Benar, maaf saya pasti lupa melakukan itu ketika saya menulis contoh - itu cukup bodoh dari saya. Tidak melakukan hal itu tidak akan membuat kesalahan orang, yang membuat saya khawatir berapa banyak orang yang menyalin / menempel solusi dan melanjutkan bisnis mereka ..... Lagi pula, mcpeterson sangat berterima kasih - terima kasih!
JJ
2

Saya telah menggunakan jawaban ant32 di atas selama beberapa tahun. Namun saya telah menemukan bahwa ada kesalahan di python 3 karena mogrifymengembalikan byte string.

Konversi secara eksplisit untuk memotong string adalah solusi sederhana untuk membuat kode python 3 kompatibel.

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
sumber
1

Pendekatan lain yang bagus dan efisien - adalah untuk melewatkan baris untuk dimasukkan sebagai 1 argumen, yang merupakan array dari objek json.

Misalnya Anda menyampaikan argumen:

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

Ini adalah array, yang dapat berisi sejumlah objek di dalamnya. Kemudian SQL Anda terlihat seperti:

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

Perhatikan: Kemajuan Anda harus cukup baru, untuk mendukung json

Daniel Garmoshka
sumber
1

The cursor.copyfrom solusi yang disediakan oleh @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) di atas ( https://stackoverflow.com/a/30721460/11100064 ) memang cepat kilat.

Namun, contoh yang dia berikan tidak secara umum dapat digunakan untuk catatan dengan sejumlah bidang dan saya butuh waktu untuk mencari tahu bagaimana menggunakannya dengan benar.

IteratorFile perlu di-instantiated dengan bidang-bidang yang dipisahkan-tab seperti ini ( radalah daftar dicts di mana setiap dict adalah catatan):

    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)

Untuk menggeneralisasi jumlah bidang yang berubah-ubah, pertama-tama kita akan membuat string garis dengan jumlah tab dan placeholder bidang yang benar: "{}\t{}\t{}....\t{}"dan kemudian gunakan .format()untuk mengisi nilai bidang *list(r.values())) for r in records::

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

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

fungsi lengkap di intinya di sini .

Bart Jonk
sumber
0

Jika Anda menggunakan SQLAlchemy, Anda tidak perlu dipusingkan dengan pembuatan string karena SQLAlchemy mendukung pembuatan VALUESklausa multi-baris untuk satu INSERTpernyataan :

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
sumber
Di bawah tenda SQLAlchemy menggunakan executemany () psychopg2 untuk panggilan seperti ini dan karenanya jawaban ini akan memiliki masalah kinerja yang parah untuk kueri besar. Lihat metode eksekusi docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
2
Saya pikir bukan itu masalahnya. Sudah sedikit sejak saya melihat ini, tapi IIRC, ini sebenarnya membangun pernyataan penyisipan tunggal di insert_querybaris. Kemudian, session.execute()hanya memanggil execute()pernyataan psycopg2 dengan string masif tunggal. Jadi "trik" adalah membangun seluruh objek pernyataan penyisipan terlebih dahulu. Saya menggunakan ini untuk memasukkan 200.000 baris sekaligus dan melihat peningkatan kinerja besar-besaran menggunakan kode ini dibandingkan dengan yang normal executemany().
Jeff Widman
1
Dokumen SQLAlchemy yang Anda tautkan memiliki bagian yang menunjukkan dengan tepat bagaimana ini bekerja dan bahkan mengatakan: "Sangat penting untuk dicatat bahwa melewati beberapa nilai TIDAK sama dengan menggunakan bentuk executemany () tradisional"). Jadi secara eksplisit menyebut ini bekerja.
Jeff Widman
1
Saya berdiri dikoreksi. Saya tidak memperhatikan penggunaan metode values ​​() (tanpanya SQLAlchemy hanya mengeksekusi banyak). Saya akan mengatakan edit jawaban untuk menyertakan tautan ke dokumen itu sehingga saya dapat mengubah suara saya, tetapi jelas Anda sudah memasukkannya. Mungkin menyebutkan bahwa ini tidak sama dengan memanggil insert () dengan execute () dengan daftar dicts?
sage88
bagaimana kinerjanya dibandingkan dengan execute_values?
MrR
0

execute_batch telah ditambahkan ke psycopg2 sejak pertanyaan ini diposting.

Itu lebih lambat dari execute_values tetapi lebih mudah digunakan.

gerardw
sumber
2
Lihat komentar lain. Metode psycopg2 ini execute_valuesadalah lebih cepat daripadaexecute_batch
Fierr
0

executemebanyak menerima array tupel

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
sumber
-1

Jika Anda ingin memasukkan beberapa baris dalam satu statemens insert (dengan asumsi Anda tidak menggunakan ORM) cara termudah sejauh ini bagi saya adalah dengan menggunakan daftar kamus. Berikut ini sebuah contoh:

 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)

Seperti yang Anda lihat, hanya satu permintaan yang akan dieksekusi:

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
sumber
Menampilkan logging dari mesin sqlalchemy BUKAN demonstrasi hanya menjalankan permintaan tunggal, itu hanya berarti bahwa mesin sqlalchemy menjalankan satu perintah. Di bawah tenda ini menggunakan eksekusi psychopg2 yang sangat tidak efisien. Lihat metode eksekusi docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
-3

Menggunakan aiopg - Cuplikan di bawah berfungsi dengan baik

    # 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
sumber
-4

Akhirnya dalam versi SQLalchemy1.2, implementasi baru ini ditambahkan untuk menggunakan psycopg2.extras.execute_batch () alih-alih menjalankan banyak ketika Anda menginisialisasi mesin Anda dengan use_batch_mode = Benar seperti:

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

Maka seseorang harus menggunakan SQLalchmey tidak akan repot untuk mencoba berbagai kombinasi sqla dan psycopg2 dan mengarahkan SQL bersama ..

pengguna2189731
sumber