今天我花了一天时间改进Python脚本的性能,该脚本将数据推送到Postgres数据库中.我之前插入过这样的记录:

query = "INSERT INTO my_table (a,b,c ... ) VALUES (%s, %s, %s ...)";
for d in data:
    cursor.execute(query, d)

然后我重新编写了脚本,这样它就创建了一个内存中的文件,而不是Postgres的COPY命令,该命令允许我将数据从文件复制到表中:

f = StringIO(my_tsv_string)
cursor.copy_expert("COPY my_table FROM STDIN WITH CSV DELIMITER AS E'\t' ENCODING 'utf-8' QUOTE E'\b' NULL ''", f)

COPY法为staggeringly faster.

METHOD      | TIME (secs)   | # RECORDS
=======================================
COPY_FROM   | 92.998    | 48339
INSERT      | 1011.931  | 48377

但我找不到任何关于原因的信息?它的工作原理与多行INSERT有什么不同,从而使其速度更快?

参见第this benchmark页:

# original
0.008857011795043945: query_builder_insert
0.0029380321502685547: copy_from_insert

#  10 records
0.00867605209350586: query_builder_insert
0.003248929977416992: copy_from_insert

# 10k records
0.041108131408691406: query_builder_insert
0.010066032409667969: copy_from_insert

# 1M records
3.464181900024414: query_builder_insert
0.47070908546447754: copy_from_insert

# 10M records
38.96936798095703: query_builder_insert
5.955034017562866: copy_from_insert

推荐答案

这里有很多因素在起作用:

  • 网络延迟和往返延迟
  • PostgreSQL中的每语句开销
  • 上下文切换和调度程序延迟
  • 如果用户每次插入一次提交,则需要COMMIT美元(您没有)
  • 批量装载的COPY个具体优化

网络延迟

如果服务器是远程的,那么您可能要为每条语句"支付"固定时间的"价格",比如50毫秒(1/20秒).或者对于一些云托管的DBs来说更多.由于在最后一次插入成功完成之前,下一次插入无法开始,这意味着您的maximum次插入速率为每秒COPY0次/往返延迟,单位为毫秒行.在50毫秒("ping时间")的延迟下,即20行/秒.即使在本地服务器上,这种延迟也不是零.Wheras COPY只填充TCP发送和接收窗口,并以数据库写入行和网络传输行的速度传输行.它不受延迟的太大影响,并且可能每秒在同一网络链接上插入数千行.

PostgreSQL中的每语句成本

在PostgreSQL中解析、规划和执行一条语句也要付出代价.它必须使用锁、打switch 系文件、查找索引等等.COPYtry 在一开始执行所有这些操作一次,然后集中精力尽可能快地加载行.

任务/上下文转换成本

由于操作系统必须在应用程序准备并发送某一行时,在postgres等待该行,然后在postgres处理该行时,应用程序等待postgres的响应之间切换,因此需要支付更多的时间成本.每次从一个切换到另一个,都会浪费一点时间.当进程进入和离开等待状态时,挂起和恢复各种低级内核状态可能会浪费更多时间.

错过了拷贝优化

最重要的是,COPY有一些优化,它可以用于某些类型的负载.例如,如果没有生成键,并且任何默认值都是常量,它可以预先计算它们并完全绕过执行器,以较低的级别将数据快速加载到表中,从而完全跳过PostgreSQL的部分正常工作.如果在COPY的同一事务中使用CREATE TABLETRUNCATE,它可以通过绕过多客户端数据库中所需的正常事务簿记来实现更快的加载.

尽管如此,PostgreSQL的COPY仍然可以做很多事情来加快速度,这是它还不知道如何做的事情.如果您更改的表超过一定比例,它可以自动跳过索引更新,然后重建索引.它可以批量更新索引.还有很多.

promise 成本

最后要考虑的是promise 成本.这对你来说可能不是问题,因为psycopg2默认打开一个事务,在你告诉它之前不提交.除非你告诉它使用自动提交.但对于许多DB驱动程序来说,自动提交是默认设置.在这种情况下,你将每INSERT次做一次提交.这意味着一次磁盘刷新,服务器确保将内存中的所有数据写入磁盘,并告诉磁盘将自己的缓存写入持久存储.这可能需要long个时间,并且根据硬件的不同而变化很大.我的基于SSD的NVMe BTRFS笔记本电脑每秒只能进行200次同步,而不是每秒30万次非同步写入.所以它每秒只能加载200行!有些服务器每秒只能执行50次fsyncs.有些人能做到两万.因此,如果您必须定期提交,请try 批量加载和提交,执行多行插入,等等.因为COPY在最后只执行一次提交,所以提交成本可以忽略不计.但这也意味着COPY无法在数据传输过程中从错误中恢复;它可以卸下整个散装Cargo .

Postgresql相关问答推荐

锁定模式与PostgreSQL中的另一种锁定模式冲突到底是什么意思?

Postgres从spark触发post-write

supabase 中的交易

如何在 PostgreSQL 的回归测试中测试 TYPE 发送和接收函数

如何在 PSQL 中查找从另一个表继承的子表

获取 OperationalError: FATAL: sorry, too many clients already using psycopg2

从 sql 查询 postgres 9.4 创建嵌套 json

更详细地解释 JOIN 与 LEFT JOIN 和 WHERE 条件性能建议

并发刷新materialized视图

如何将两个 PostgreSQL 列聚合到一个用括号分隔的数组

?(问号)运算符在 Rails 中查询 Postgresql JSONB 类型

Postgres 表中列的顺序会影响性能吗?

理解 Postgres 行大小

使用 PostGIS 查找给定点的 n 个最近邻?

大型查询后 psycopg2 泄漏内存

如何在 postgresql 交叉表中用零替换空值

全文的 Postgresql 前缀通配符

如何使 array_agg() 像 mySQL 中的 group_concat() 一样工作

在 pg_restore 期间排除表

Ruby 中 DateTime 的毫秒分辨率