COPY如何工作,为什么它比INSERT这么快?
今天,我整天都在改善Python脚本的性能,该脚本将数据推送到Postgres数据库中。我以前是这样插入记录的:
query = "INSERT INTO my_table (a,b,c ... ) VALUES (%s, %s, %s ...)";
for d in data:
cursor.execute(query, d)
然后,我重新编写了脚本,以使它创建的内存文件比PostgresCOPY
命令所使用的内存文件大,这使我可以将数据从文件复制到表中:
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
方法 惊人地更快 。
METHOD | TIME (secs) | # RECORDS
=======================================
COPY_FROM | 92.998 | 48339
INSERT | 1011.931 | 48377
但是我找不到关于为什么的任何信息?它与多行的工作方式有何不同INSERT
,从而使其变得更快?
也请参阅此基准测试:
# 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秒)。对于某些云托管数据库,甚至更多。由于下一个插入操作要等到最后一个操作成功完成后才能开始,所以这意味着您的
最大 插入速率是每秒1000 /来回延迟毫秒数行。延迟为50毫秒(“
ping时间”),即20行/秒。即使在本地服务器上,此延迟也不为零。WherasCOPY
只是填充TCP发送和接收窗口,并且以DB可以写入它们并网络可以传输它们的速度来传输流。它不受延迟的影响很大,并且可能每秒在同一网络链接上插入数千行。PostgreSQL中的每个陈述式成本
在PostgreSQL中解析,计划和执行语句也有成本。它必须要加锁,打开关系文件,查找索引等。
COPY
在开始时尝试一次完成所有这些操作,然后只专注于尽可能快地加载行。任务/上下文切换成本
由于操作系统必须在您的应用程序准备和发送时在等待行的postgres之间切换,然后在postgres处理该行的同时等待postgres的响应的应用程序之间切换,因此还要支付更多的时间成本。每次从一个切换到另一个时,都会浪费一点时间。当进程进入和离开等待状态时,可能会浪费更多时间来挂起和恢复各种低级内核状态。
错过了COPY优化
最重要的是,
COPY
有一些优化可以用于某些负载。例如,如果没有生成的键并且任何默认值都是常量,它可以预先计算它们并完全绕过执行程序,从而将数据快速加载到较低级别的表中,从而完全跳过了PostgreSQL的部分正常工作。如果您CREATE TABLE
或TRUNCATE
与您处于同一事务中COPY
,则可以绕过多客户端数据库中所需的常规事务簿记,从而做更多的技巧来加快加载速度。尽管如此,PostgreSQL
COPY
仍然可以做更多的事情来加快速度,这是它尚不知道如何做的事情。如果您要更改表的一定比例,它可能会自动跳过索引更新,然后重建索引。它可以批量进行索引更新。还有更多。提交费用
最后要考虑的一件事是提交成本。对于您来说,这可能不是问题,因为
psycopg2
默认情况下会打开一个事务,直到您告知后才提交。除非您告诉它使用自动提交。但是对于许多数据库驱动程序,自动提交是默认设置。在这种情况下,您将每次执行一次提交INSERT
。这意味着一个磁盘刷新,服务器确保将内存中的所有数据写出到磁盘上,并告诉磁盘将自己的缓存写出到持久性存储中。这可能需要很
长时间 时间,并且因硬件而异。我的基于SSD的NVMe BTRFS笔记本电脑只能执行200 fsyncs
/秒,而不是每秒300,000次非同步写入。因此它只会加载200行/秒!某些服务器只能执行50 fsync
/秒。有些可以做到20,000。因此,如果必须定期提交,则尝试分批加载和提交,进行多行插入等。由于最后COPY
只提交一次,因此提交成本可以忽略不计。但这也意味着COPY
无法从数据中途的错误中恢复;它会取消整个批量加载。