psycopg2: bir sorgu ile birden çok satır ekle


141

Bir sorgu ile birden çok satır eklemek gerekir (satır sayısı sabit değil), bu yüzden böyle bir sorgu yürütmek gerekir:

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

Bilmemin tek yolu

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)

ama daha basit bir yol istiyorum.

Yanıtlar:


219

Başka bir şehirde bulunan bir sunucuya birden çok satır ekleyen bir program oluşturdum.

Bu yöntemi kullanmanın 10 kat daha hızlı olduğunu öğrendim executemany. Benim durumumda tupyaklaşık 2000 satır içeren bir demet. Bu yöntemi kullanırken yaklaşık 10 saniye sürdü:

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) 

ve bu yöntemi kullanırken 2 dakika:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)

15
Neredeyse iki yıl sonra hala çok alakalı. Bugünkü bir deneyim, itmek istediğiniz satır sayısının artmasıyla executestratejiyi kullanmanın daha iyi olduğunu göstermektedir . Bu sayede 100x civarında hız gördüm!
Rob Watts

4
Belki de executemanyher eklemeden sonra bir taahhütte bulunur. Bunun yerine her şeyi bir işlemde sararsanız, bu belki de işleri hızlandırır?
Richard

4
Sadece bu gelişmeyi kendim doğruladım. Ne okudum psycopg2 en executemanyuygun bir şey yapmaz, sadece döngüler ve birçok executeifade yapar . Bu yöntemi kullanarak, uzak sunucuya 700 satırlı ekleme, 60'lardan <2s'ye gitti.
Nelson

5
Belki paranoyak oluyorum, ancak sorguyu +sql enjeksiyonuna kadar açılmış gibi görünüyor, @Clodoaldo Neto execute_values()çözümünün daha güvenli olduğunu hissediyorum .
Munn

26
birisinin aşağıdaki hatayla karşılaşması durumunda: [TypeError: sıra öğesi 0: beklenen str örneği, bayt bulundu] bunun yerine bu komutu çalıştırın [args_str = ','. join (cur.mogrify ("(% s,% s)", x ) .decode ("in tup x için" utf-8 "))
mrt

147

Psycopg 2.7'deki yeni execute_valuesyöntem :

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'da bunu yapmanın pythonic yolu:

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)

Açıklama: Eklenecek veriler, aşağıdaki gibi tuples listesi olarak verilirse

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

zaten tam olarak istenen biçimde

  1. valuessözdizimi insertmaddesi gibi kayıtların listesini ister

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

  2. Psycopgbir Python'u tuplebir Postgresql'e uyarlar record.

Gerekli olan tek iş, psycopg tarafından doldurulacak bir kayıt listesi şablonu sağlamaktır

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

ve insertsorguya yerleştir

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

insert_queryÇıktıları yazdırma

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

Şimdi olağan Psycopgargümanların ikamesine

cursor.execute(insert_query, data)

Veya sadece sunucuya ne gönderileceğini test edin

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

Çıktı:

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

1
Bu yöntemin performansı cur.copy_from ile nasıl karşılaştırılır?
Michael Goldshteyn

1
İşte bir karşılaştırma ölçütü olan bir öz . copy_from, 10M kayıtlarla makinemde yaklaşık 6,5 kat daha hızlı ölçeklenir.
Joseph Sheedy

Güzel görünüyor - Ben insert_query ilk tanımının sonunda (bir demet yapmaya çalışmadıkça?) Sapık olduğunu düşünüyorum ve% s için% s sonra insert_query ilk tanımında olduğu gibi eksik.
deadcode

2
kullanarak execute_valuessistemimi dakikada 1
bin kayıtla

66

Psycopg2 2.7 ile güncelleme:

Klasik executemany(), bu başlıkta açıklandığı gibi @ ant32 uygulamasından ("katlanmış" olarak adlandırılır) yaklaşık 60 kat daha yavaştır: https://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

Bu uygulama, 2.7 sürümünde psycopg2'ye eklendi ve şöyle denir 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)])

Önceki Cevap:

Birden çok satır eklemek için, çok satırlı VALUESsözdizimini execute()kullanarak psycopg2 kullanmaktan yaklaşık 10 kat daha hızlıdır executemany(). Gerçekten de, executemany()birçok bireysel INSERTifade çalıştırır .

@ ant32'nin kodu Python 2'de mükemmel çalışır. Ancak Python 3'te cursor.mogrify()bayt döndürür, cursor.execute()bayt veya dize alır ve örnek ','.join()bekler str.

Python 3'te, @ ant32'nin kodunu ekleyerek değiştirmeniz gerekebilir .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)

Veya yalnızca bayt ( b''veya ile b"") kullanarak :

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 , toplu kesici uçlar için şimdiye kadar bulduğum en hızlı çözüm. İşte dizeleri bir dosya gibi okunmasını sağlayan bir yineleyiciye izin veren IteratorFile adlı bir sınıf içeren bir özgeçmiş. Her giriş kaydını bir jeneratör ifadesi kullanarak bir dizeye dönüştürebiliriz. Böylece çözüm

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

Bu önemsiz argüman boyutu için, hız farkından fazla olmayacaktır, ancak binlerce + satırla uğraşırken büyük hızlanmalar görüyorum. Ayrıca, dev bir sorgu dizesi oluşturmaktan daha fazla bellek verimliliği sağlayacaktır. Bir yineleyici, her seferinde yalnızca bir giriş kaydını bellekte tutacaktır; burada bir noktada Python işleminizde veya Postgres'te sorgu dizesini oluşturarak bellek kalmaz.


3
Burada copy_from / IteratorFile ile bir sorgu oluşturucu çözümünü karşılaştıran bir karşılaştırma ölçütü bulunmaktadır. copy_from, 10M kayıtlarla makinemde yaklaşık 6,5 kat daha hızlı ölçeklenir.
Joseph Sheedy

3
kaçan dizeleri ve zaman damgaları vb ile etrafında dick var mı?
CpILL

Evet, iyi biçimlendirilmiş bir TSV kaydına sahip olduğunuzdan emin olmanız gerekir.
Joseph Sheedy

24

Psycopg2'nin Postgresql.org'daki eğitim sayfasından bir pasaj (aşağıya bakınız) :

Size göstermek istediğim son öğe, sözlük kullanarak birden çok satırı nasıl ekleyeceğinizdir. Aşağıdakilere sahipseniz:

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

Şunları kullanarak sözlükteki her üç satırı da kolayca ekleyebilirsiniz:

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

Çok fazla kod kaydetmez, ancak kesinlikle daha iyi görünür.


35
Bu, birçok bireysel INSERTifadeyi çalıştırır . Yararlıdır, ancak tek bir çok VALUEboyutlu kesici uç ile aynı değildir .
Craig Ringer

7

Tüm bu tekniklere Postgres terminolojisinde 'Genişletilmiş Uçlar' denir ve 24 Kasım 2016'dan itibaren, psychopg2'nin executemany () ve bu iş parçacığında listelenen diğer tüm yöntemlerden bir ton daha hızlıdır (buna gelmeden önce denedim) Cevap).

İşte cur.mogrify kullanmayan ve güzel ve sadece başınızı almak için bazı kod:

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

Ancak copy_from () kullanabiliyorsanız copy_from;) kullanmanız gerektiği unutulmamalıdır.


Ölümden kalkmak, ama son birkaç satırın durumunda ne olur? Çift satırınız olması durumunda, son fıkrayı son satırlarda tekrar çalıştırdığınızı varsayıyorum.
mcpeterson

Doğru, üzgünüm örneği yazdığımda bunu yapmayı unutmuş olmalıyım - bu benim için çok aptalca. Bunu yapmamak insanlara bir hata vermezdi, bu da kaç kişinin çözümü kopyaladığını / yapıştırdığını ve işlerini sürdürdüğünü endişelendiriyor ..... Neyse, çok minnettar mcpeterson - teşekkür ederim!
JJ

2

Birkaç yıldır ant32'nin cevabını kullanıyorum. Ancak bu python 3 bir hata thorws olduğunu buldummogrify bir bayt dizesi döndürür .

Açıkça bytse dizelerine dönüştürmek, kod python 3'ü uyumlu hale getirmek için basit bir çözümdür.

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

Başka bir güzel ve verimli yaklaşım - ekleme için satırları json nesneleri dizisi olan 1 argüman olarak geçirmektir.

Örneğin, tartışmayı geçtiğinizde:

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

İçinde herhangi bir miktarda nesne içerebilen dizidir. Sonra SQL'iniz şöyle görünür:

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

Uyarı: Postsonunuz json'u destekleyecek kadar yeni olmalı


1

Yukarıda @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) tarafından sağlanan cursor.copyfrom çözümü ( https://stackoverflow.com/a/30721460/11100064 ) gerçekten yıldırım hızındadır .

Bununla birlikte, verdiği örnek, herhangi bir sayıda alana sahip bir kayıt için genel olarak kullanılabilir değildir ve bunu nasıl doğru bir şekilde kullanacağımı anlamak beni aldı.

IteratorFile röğesinin sekme ile ayrılmış alanlarla böyle başlatılması gerekir ( her bir diktenin kayıt olduğu diktelerin bir listesidir):

    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)

Rasgele sayıda alanı genelleştirmek için, önce doğru sayıda sekme ve alan yer tutucusu içeren bir satır dizesi oluştururuz: "{}\t{}\t{}....\t{}"ve sonra .format()alan değerlerini doldurmak için kullanırız *list(r.values())) for r in records:

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

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

burada özünde tam fonksiyon .


0

SQLAlchemy kullanıyorsanız, SQLAlchemy tek bir deyim için çok satırlı bir VALUEScümle oluşturmayı desteklediğindenINSERT , dizeyi el yapımı ile uğraşmanıza gerek yoktur :

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)

Kaputun altında SQLAlchemy, bu gibi çağrılar için psychopg2'nin executemany () yöntemini kullanır ve bu nedenle bu sorunun büyük sorgular için ciddi performans sorunları olacaktır. Docs.sqlalchemy.org/en/latest/orm/session_api.html yürütme yöntemine bakın .
sage88

2
Ben öyle düşünmüyorum. Buna baktığımdan beri biraz oldu, ama IIRC, bu aslında insert_querysatırda tek bir ekleme ifadesi oluşturuyor . Sonra, session.execute()sadece execute()tek bir büyük dize ile psycopg2 ifadesini çağırıyor . Yani "hile" önce tüm ekleme deyimi nesnesini inşa ediyor. Ben bir seferde 200.000 satır eklemek için kullanıyorum ve normal ile karşılaştırıldığında bu kodu kullanarak büyük performans artışları gördüm executemany().
Jeff Widman

1
Bağlandığınız SQLAlchemy dokümanı, bunun nasıl çalıştığını gösteren bir bölüme sahiptir ve hatta şunu söyler: "Birden çok değer iletmenin geleneksel executemany () formunu kullanmakla aynı OLMADIĞINI not etmek önemlidir". Bu yüzden açıkça bunun işe yaradığını söylüyor.
Jeff Widman

1
Ben düzeltilmiş duruyorum. (SQLAlchemy sadece executemany yapar) ((yöntem olmadan sadece) yöntemi kullanımınızı fark etmedi. Oyumu değiştirebilmem için cevabı o dokümanın bağlantısını içerecek şekilde düzenleyebilirim, ancak açıkçası zaten eklediniz. Belki de bunun bir dikte listesi içeren execute () ile bir insert () çağırmakla aynı şey olmadığını belirtin?
sage88

execute_values ​​ile karşılaştırıldığında nasıl çalışır?
19R

0

execute_batch bu soru yayınlanmıştır beri psycopg2 eklenmiştir.

Execute_values değerinden daha yavaş ancak kullanımı daha basittir.


2
Diğer yorumlara bakın. psycopg2 metodu execute_valuesolan hızlı dahaexecute_batch
Fierr

0

executemany tuples dizi kabul

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

Bir kesici uçtaki (ORM kullanmadığınızı varsayarak) birden çok satır eklemek istiyorsanız, benim için şimdiye kadarki en kolay yol sözlük listesini kullanmak olacaktır. İşte bir örnek:

 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)

Gördüğünüz gibi yalnızca bir sorgu yürütülecek:

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 motorundan günlüğe kaydetme yalnızca tek bir sorgu çalıştırmanın bir göstergesi DEĞİLDİR, sadece sqlalchemy motorunun bir komut çalıştırdığı anlamına gelir. Kaputun altında bu, çok verimsiz olan psychopg2'nin yöneticisini kullanıyor. Docs.sqlalchemy.org/en/latest/orm/session_api.html yürütme yöntemine bakın .
sage88

-3

Aiopg kullanma - Aşağıdaki kod parçası gayet iyi çalışıyor

    # 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

Son olarak SQLalchemy1.2 sürümünde, motorunuzu use_batch_mode = True like ile başlattığınızda executemany yerine psycopg2.extras.execute_batch () kullanmak için bu yeni uygulama eklenir:

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

Sonra birisi SQLalchmey kullanmak zorunda kalacak sqla ve psycopg2 farklı kombinasyonları denemek ve SQL doğrudan birlikte uğraşmak zorunda kalmayacaksınız ..

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.