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)

但我想要一些更简单的方法。

Answers:


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行插入从60s变为<2s。
尼尔森

5
也许我有点偏执,但是用一个查询连接起来+似乎可以进行sql注入,我觉得@Clodoaldo Neto execute_values()解决方案更安全。
Will Munn

26
如果有人遇到以下错误:[TypeError:序列项目0:预期的str实例,找到的字节]运行此命令,而不是[args_str =','。join(cur.mogrify(“(%s,%s)”,x ).decode(“ utf-8”),用于x in tup)]
mrt

146

Psycopg 2.7中的新execute_values方法

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中执行此操作的pythonic方法:

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适应tuplePostgresql 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相比如何?
Michael Goldshteyn '16

1
这是一个有基准的要点。在具有1000万条记录的计算机上,copy_from的扩展速度约为6.5倍。
约瑟夫·希迪

看起来不错-我认为您在insert_query的初始定义的末尾有一个流浪(除非您试图使其成为一个元组?),并且在insert_query的初始定义中也缺少%s之后的%s。
死码

2
使用execute_values我,我的系统能够以每分钟1k条记录的速度运行至每分钟128k条记录
Conrad.Dean

66

使用psycopg2 2.7更新:

executemany()以下线程中所述,经典版本比@ ant32的实现(称为“折叠”)慢约60倍:https ://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

此实现已在2.7版中添加到psycopg2中,称为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)])

上一个答案:

要插入多行,使用multirow VALUES语法execute()比使用psycopg2快约10倍executemany()。确实,executemany()只是运行许多单独的INSERT语句。

@ ant32的代码在Python 2中可以完美地工作。但是在Python 3中,cursor.mogrify()返回字节,cursor.execute()采用字节或字符串并','.join()需要str实例。

因此,在Python 3中,您可能需要通过添加.decode('utf-8')以下内容来修改@ ant32的代码:

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

对于这个琐碎的args来说,速度差异不会太大,但是在处理数千行时,我看到了很大的加速。与构建巨大的查询字符串相比,它还将提高内存效率。迭代器一次只能在内存中保存一个输入记录,在某个时候,通过构建查询字符串,您的Python进程或Postgres中的内存将用完。


3
这是一个比较copy_from / IteratorFile与查询构建器解决方案的基准。在拥有1000万条记录的计算机上,copy_from的速度提高了大约6.5倍。
约瑟夫·希迪16'Sep

3
您是否需要转义字符串和时间戳等内容呢?
CpILL

是的,您必须确保您拥有格式正确的TSV记录。
Joseph Sheedy

24

来自Postgresql.org的 Psycopg2的教程页面的摘要(请参见底部)

我想向您展示的最后一项是如何使用字典插入多行。如果您具有以下条件:

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插入不同。
Craig Ringer 2013年

7

所有这些技术在Postgres术语中都称为“扩展插入”,截至2016年11月24日,它仍然比psychopg2的executemany()和该线程中列出的所有其他方法快了很多(我在尝试此方法之前曾尝试过)回答)。

这是一些不使用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;)


从死里复活,但在最后几行情况下会发生什么?我假设在行数为偶数的情况下,您实际上在剩余的最后几行上再次运行了final子句?
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 

注意:您的postgress必须足够新,才能支持json


1

上面(https://stackoverflow.com/a/30721460/11100064)的@ jopseph.sheedyhttps://stackoverflow.com/users/958118/joseph-sheedy)提供的cursor.copyfrom解决方案确实快如闪电。

但是,他给出的示例不适用于具有任意多个字段的记录,因此我花了一些时间才弄清楚如何正确使用它。

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的executemany()进行此类调用,因此对于大型查询,此答案将带来严重的性能问题。请参阅执行方法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
我站得住了。我没有注意到您对values()方法的使用(没有它,SQLAlchemy确实会执行很多)。我会说编辑答案以包括指向该文档的链接,以便我可以更改我的投票,但是显然您已经添加了它。也许要提到这与调用带有字典列表的execute()的insert()不同吗?
sage88

与execute_values相比,它的表现如何?
MrR


0

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

如果要在一个插入状态表中插入多行(假设您未使用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的executemany,效率很低。请参阅执行方法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版本中,此新实现被添加为在使用use_batch_mode = True初始化引擎时使用psycopg2.extras.execute_batch()而不是executemany,例如:

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一起使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.