psycopg2: chèn nhiều hàng với một truy vấn


141

Tôi cần chèn nhiều hàng với một truy vấn (số lượng hàng không phải là hằng số), vì vậy tôi cần thực hiện truy vấn như thế này:

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

Cách duy nhất tôi biết là

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)

nhưng tôi muốn một số cách đơn giản hơn.

Câu trả lời:


219

Tôi đã xây dựng một chương trình chèn nhiều dòng vào một máy chủ được đặt tại một thành phố khác.

Tôi phát hiện ra rằng sử dụng phương pháp này nhanh hơn khoảng 10 lần executemany. Trong trường hợp của tôi tuplà một tuple chứa khoảng 2000 hàng. Mất khoảng 10 giây khi sử dụng phương pháp này:

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) 

và 2 phút khi sử dụng phương pháp này:

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

15
Vẫn rất có liên quan gần hai năm sau. Một kinh nghiệm ngày nay cho thấy rằng khi số lượng hàng bạn muốn đẩy càng tăng thì sử dụng executechiến lược càng tốt. Tôi đã thấy speedup khoảng 100 lần nhờ vào điều này!
Rob Watts

4
Có lẽ executemanychạy một cam kết sau mỗi lần chèn. Nếu bạn thay vì bọc toàn bộ trong một giao dịch, có lẽ điều đó sẽ đẩy nhanh mọi thứ?
Richard

4
Chỉ cần xác nhận cải thiện này bản thân mình. Từ những gì tôi đã đọc psycopg2 executemanykhông làm gì tối ưu, chỉ là các vòng lặp và thực hiện nhiều executetuyên bố. Sử dụng phương pháp này, chèn 700 hàng vào máy chủ từ xa đã chuyển từ 60 giây sang <2 giây.
Nelson

5
Có lẽ tôi đang bị hoang tưởng, nhưng kết hợp truy vấn với một +dường như nó có thể mở ra để tiêm sql, tôi cảm thấy như execute_values()giải pháp Neto @Clodoaldo an toàn hơn.
Will Munn

26
trong trường hợp ai đó gặp phải lỗi sau: [TypeError: chuỗi mục 0: đối tượng str dự kiến, byte được tìm thấy] chạy lệnh này thay vì [args_str = ','. tham gia (cur.mogrify ("(% s,% s)", x ) .decode ("utf-8") cho x in tup)]
mrt

146

execute_valuesPhương pháp mới trong 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
)

Cách làm pythonic trong 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)

Giải thích: Nếu dữ liệu được chèn được đưa ra dưới dạng danh sách các bộ dữ liệu như trong

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

sau đó nó đã ở định dạng chính xác như

  1. các valuescú pháp của insertkhoản dự đoán một danh sách các hồ sơ như trong

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

  2. Psycopgđiều chỉnh một Python tuplecho Postgresql record.

Công việc cần thiết duy nhất là cung cấp một mẫu danh sách hồ sơ được điền bởi psycopg

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

và đặt nó trong inserttruy vấn

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

In các insert_queryđầu ra

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

Bây giờ để Psycopgthay thế đối số thông thường

cursor.execute(insert_query, data)

Hoặc chỉ kiểm tra những gì sẽ được gửi đến máy chủ

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

Đầu ra:

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

1
Làm thế nào để hiệu suất của phương pháp này so với cur.copy_from?
Michael Goldshteyn 3/03/2016

1
Đây là một ý chính với điểm chuẩn . copy_from chia tỷ lệ nhanh hơn khoảng 6,5 lần trên máy của tôi với các bản ghi 10M.
Joseph Sheedy

Trông thật tuyệt - Tôi nghĩ rằng bạn có một sự đi lạc, ở cuối định nghĩa ban đầu của bạn về insert_query (trừ khi bạn đang cố gắng biến nó thành một tuple?) Và bị thiếu như sau% cho% s trong định nghĩa ban đầu của insert_query.
deadcode

2
bằng cách sử dụng, execute_valuestôi có thể khiến hệ thống của mình chạy với tốc độ 1 nghìn bản ghi một phút lên tới
128 nghìn

66

Cập nhật với psycopg2 2.7:

Cổ điển executemany()chậm hơn khoảng 60 lần so với triển khai của @ ant32 (được gọi là "gấp") như được giải thích trong chủ đề này: https://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

Việc triển khai này đã được thêm vào psycopg2 trong phiên bản 2.7 và được gọi là 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)])

Trả lời trước:

Để chèn nhiều hàng, sử dụng VALUEScú pháp multirow với execute()tốc độ nhanh hơn khoảng 10 lần so với sử dụng psycopg2 executemany(). Thật vậy, executemany()chỉ chạy nhiều INSERTtuyên bố cá nhân .

Mã của @ ant32 hoạt động hoàn hảo trong Python 2. Nhưng trong Python 3, cursor.mogrify()trả về byte, cursor.execute()lấy byte hoặc chuỗi và ','.join()mong muốn str.

Vì vậy, trong Python 3, bạn có thể cần sửa đổi mã của @ ant32, bằng cách thêm .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)

Hoặc bằng cách chỉ sử dụng byte (có b''hoặc 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

con trỏ.copy_from là giải pháp nhanh nhất tôi đã tìm thấy để chèn số lượng lớn cho đến nay. Đây là một ý chính tôi đã thực hiện có chứa một lớp có tên IteratorFile, cho phép một chuỗi lặp mang lại chuỗi được đọc giống như một tệp. Chúng ta có thể chuyển đổi từng bản ghi đầu vào thành một chuỗi bằng biểu thức trình tạo. Vì vậy, giải pháp sẽ là

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

Đối với kích thước tầm thường này, nó sẽ không tạo ra sự khác biệt lớn về tốc độ, nhưng tôi thấy sự tăng tốc lớn khi xử lý hàng ngàn + hàng. Nó cũng sẽ hiệu quả hơn về bộ nhớ so với việc xây dựng một chuỗi truy vấn khổng lồ. Một trình vòng lặp sẽ chỉ giữ một bản ghi đầu vào trong bộ nhớ tại một thời điểm, tại một số điểm, bạn sẽ hết bộ nhớ trong quy trình Python hoặc trong Postgres bằng cách xây dựng chuỗi truy vấn.


3
Dưới đây là điểm chuẩn so sánh copy_from / IteratorFile với giải pháp xây dựng truy vấn. copy_from chia tỷ lệ nhanh hơn khoảng 6,5 lần trên máy của tôi với các bản ghi 10M.
Joseph Sheedy

3
Bạn có phải tinh ranh với thoát chuỗi và dấu thời gian vv?
CpILL

Có, bạn sẽ phải đảm bảo rằng bạn có một hồ sơ TSV được hình thành tốt.
Joseph Sheedy

24

Một đoạn trích từ trang hướng dẫn của Psycopg2 tại Postgresql.org (xem phía dưới) :

Một mục cuối cùng tôi muốn chỉ cho bạn là cách chèn nhiều hàng bằng từ điển. Nếu bạn có những điều sau đây:

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

Bạn có thể dễ dàng chèn cả ba hàng trong từ điển bằng cách sử dụng:

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

Nó không tiết kiệm nhiều mã, nhưng nó chắc chắn trông tốt hơn.


35
Điều này sẽ chạy nhiều INSERTbáo cáo cá nhân . Hữu ích, nhưng không giống như một VALUEchèn nhiều lần.
Craig Ringer

7

Tất cả các kỹ thuật này được gọi là 'Chèn thêm "theo thuật ngữ của Postgres và kể từ ngày 24 tháng 11 năm 2016, nó vẫn nhanh hơn rất nhiều so với giám đốc điều hành của psychopg2 () và tất cả các phương pháp khác được liệt kê trong chủ đề này (mà tôi đã thử trước khi đến đây câu trả lời).

Đây là một số mã không sử dụng cur.mogrify và rất hay và đơn giản chỉ để hiểu ý bạn:

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

Nhưng cần lưu ý rằng nếu bạn có thể sử dụng copy_from (), bạn nên sử dụng copy_from;)


Đưa lên từ cõi chết, nhưng điều gì xảy ra trong tình huống của vài hàng cuối? Tôi giả sử bạn thực sự chạy lại mệnh đề cuối cùng đó trên các hàng còn lại cuối cùng, trong trường hợp bạn có số hàng chẵn?
mcpeterson

Đúng, xin lỗi tôi đã quên làm điều đó khi tôi viết ví dụ - điều đó khá ngu ngốc đối với tôi. Không làm như vậy sẽ không gây ra lỗi cho mọi người, điều này khiến tôi lo lắng có bao nhiêu người sao chép / dán giải pháp và đi về doanh nghiệp của họ ..... Dù sao, mcpeterson rất biết ơn - cảm ơn bạn!
JJ

2

Tôi đã sử dụng câu trả lời của ant32 ở trên trong nhiều năm. Tuy nhiên, tôi thấy rằng đó là một lỗi trong python 3 vì mogrifytrả về một chuỗi byte.

Chuyển đổi rõ ràng sang chuỗi bytse là một giải pháp đơn giản để làm cho mã python 3 tương thích.

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

Một cách tiếp cận hiệu quả và tốt đẹp khác - là chuyển các hàng để chèn thành 1 đối số, đó là mảng các đối tượng json.

Ví dụ: bạn truyền đối số:

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

Nó là mảng, có thể chứa bất kỳ số lượng đối tượng bên trong. Thì SQL của bạn trông như sau:

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

Lưu ý: Postback của bạn phải đủ mới, để hỗ trợ json


1

Các cursor.copyfrom giải pháp theo quy định của @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) trên ( https://stackoverflow.com/a/30721460/11100064 ) thực sự là nhanh như chớp.

Tuy nhiên, ví dụ anh ta đưa ra không thể sử dụng chung cho một bản ghi với bất kỳ số lượng trường nào và tôi phải mất thời gian để tìm ra cách sử dụng nó một cách chính xác.

IteratorFile cần được khởi tạo với các trường được phân tách bằng tab như thế này ( rlà danh sách các ký tự trong đó mỗi lệnh là một bản ghi):

    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ổng quát hóa cho một số trường tùy ý, trước tiên chúng ta sẽ tạo một chuỗi dòng với số lượng tab và giữ chỗ trường chính xác: "{}\t{}\t{}....\t{}"và sau đó sử dụng .format()để điền vào các giá trị trường *list(r.values())) for r in records::

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

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

hoàn thành chức năng trong ý chính ở đây .


0

Nếu bạn đang sử dụng SQLAlchemy, bạn không cần phải lộn xộn với việc tạo thủ công chuỗi vì SQLAlchemy hỗ trợ tạo VALUESmệnh đề nhiều hàng cho một INSERTcâu lệnh :

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 sử dụng bộ điều hành () của psychopg2 cho các cuộc gọi như thế này và do đó câu trả lời này sẽ có vấn đề nghiêm trọng về hiệu năng đối với các truy vấn lớn. Xem phương thức thực thi docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88

2
Tôi không nghĩ đó là trường hợp. Có một chút kể từ khi tôi xem xét điều này, nhưng IIRC, đây thực sự đang xây dựng một tuyên bố chèn duy nhất trong insert_querydòng. Sau đó, session.execute()chỉ cần gọi execute()câu lệnh của psycopg2 bằng một chuỗi lớn. Vì vậy, "mẹo" là xây dựng toàn bộ đối tượng câu lệnh chèn trước tiên. Tôi đang sử dụng điều này để chèn 200.000 hàng cùng một lúc và thấy hiệu suất tăng mạnh khi sử dụng mã này so với bình thường executemany().
Jeff Widman

1
Tài liệu SQLAlchemy mà bạn đã liên kết có một phần cho biết chính xác cách thức hoạt động của nó và thậm chí nói: "Điều cần thiết là lưu ý rằng việc truyền nhiều giá trị KHÔNG giống như sử dụng biểu mẫu truyền thống () truyền thống". Vì vậy, nó được gọi rõ ràng rằng điều này làm việc.
Jeff Widman

1
Tôi đứng sửa. Tôi không nhận thấy việc bạn sử dụng phương thức value () (không có SQLAlchemy chỉ thực thi). Tôi sẽ nói chỉnh sửa câu trả lời để bao gồm một liên kết đến tài liệu đó để tôi có thể thay đổi phiếu bầu của mình, nhưng rõ ràng bạn đã bao gồm nó. Có lẽ đề cập rằng điều này không giống với việc gọi một insert () với exec () với một danh sách các dicts?
sage88

Làm thế nào để nó thực hiện so với exec_values?
MrR

0

exec_batch đã được thêm vào psycopg2 kể từ khi câu hỏi này được đăng.

Nó chậm hơn exec_values nhưng sử dụng đơn giản hơn.


2
Xem ý kiến ​​khác. phương pháp psycopg2 của execute_valuesnhanh hơnexecute_batch
Fierr

0

executemany chấp nhận mảng của các bộ

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

Nếu bạn muốn chèn nhiều hàng trong một lần chèn (giả sử bạn không sử dụng ORM), cách dễ nhất cho đến nay đối với tôi là sử dụng danh sách từ điển. Đây là một ví dụ:

 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)

Như bạn có thể thấy chỉ có một truy vấn sẽ được thực thi:

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

Hiển thị đăng nhập từ công cụ sqlalchemy KHÔNG phải là một minh chứng cho việc chỉ chạy một truy vấn duy nhất, điều đó chỉ có nghĩa là công cụ sqlalchemy đã chạy một lệnh. Dưới vỏ bọc này, việc sử dụng giám đốc điều hành của psychopg2 rất kém hiệu quả. Xem phương thức thực thi docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88

-3

Sử dụng aiopg - Đoạn mã dưới đây hoạt động hoàn toàn tốt

    # 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

Cuối cùng, trong phiên bản SQLalchemy1.2, triển khai mới này được thêm vào để sử dụng psycopg2.extras.execute_batch () thay vì thực thi khi bạn khởi tạo công cụ của mình bằng use_batch_mode = True như:

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

Sau đó, ai đó sẽ phải sử dụng SQLalchmey sẽ không bận tâm để thử các kết hợp khác nhau của sqla và psycopg2 và SQL trực tiếp cùng nhau ..

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.