COPY 是如何工作的,为啥它比 INSERT 快得多?

Posted

技术标签:

【中文标题】COPY 是如何工作的,为啥它比 INSERT 快得多?【英文标题】:How does COPY work and why is it so much faster than INSERT?COPY 是如何工作的,为什么它比 INSERT 快得多? 【发布时间】:2018-03-24 17:31:53 【问题描述】:

今天我花了一天的时间来改进我的 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 方法快得惊人

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

【问题讨论】:

什么是“更快”?请提供一些基准。 INSERT: 1011.93 seconds | COPY: 92.99 seconds。我的插入中有些东西使它比应有的速度慢,但是我见过的每个人都报告了巨大的改进。例如,请参见此处:gist.github.com/jsheedy/efa9a69926a754bebf0e9078fd085df6 对于每个 INSERT 语句,您都在执行隐式事务。我很好奇 COPY 是否以不同的方式处理它们。 复制是一个事务,单个插入没有开始;提交;将它们包装起来是单独的交易。复制一个错误的值会导致整个事情失败。对于单个自动提交的事务,一个值错误意味着一个值失败。您可以通过多行插入获得接近复制速度,例如插入表值 (a,b,c),(d,e,f),(g,h,i)...(x,y,z); @Kyle 你确定吗? psycopg2 默认为非自动提交,在第一条语句上打开事务并保持打开状态直到显式提交。通常你是对的,但对于 Python 来说不一定。 【参考方案1】:

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

网络延迟和往返延迟 PostgreSQL 中的每条语句开销 上下文切换和调度程序延迟 COMMIT 成本,如果对于每个插入执行一次提交的人(你不是) COPY-针对批量加载的特定优化

网络延迟

如果服务器是远程的,您可能会“支付”每个语句的固定时间“价格”,例如 50 毫秒(1/20 秒)。或者对于一些云托管的数据库来说更多。由于在最后一个插入成功完成之前无法开始下一个插入,这意味着您的最大插入速率是每秒 1000/round-trip-latency-in-ms 行。延迟为 50 毫秒(“ping 时间”),即 20 行/秒。即使在本地服务器上,此延迟也不为零。 Wheras COPY 只是填充 TCP 发送和接收窗口,并以 DB 可以写入的速度和网络可以传输它们的速度流式传输行。它受延迟影响不大,并且可能在同一网络链接上每秒插入数千行。

PostgreSQL 中的每条语句成本

在 PostgreSQL 中解析、计划和执行语句也有成本。它必须使用锁、打开关系文件、查找索引等。COPY 尝试在开始时完成所有这些操作,然后只专注于尽可能快地加载行。

任务/上下文切换成本

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

错过了 COPY 优化

除此之外,COPY 还进行了一些优化,可用于某些类型的负载。例如,如果没有生成的键并且任何默认值都是常量,它可以预先计算它们并完全绕过执行程序,将数据快速加载到较低级别的表中,从而完全跳过 PostgreSQL 的部分正常工作。如果您 CREATE TABLETRUNCATE 在与 COPY 相同的事务中,它可以通过绕过多客户端数据库中所需的正常事务簿记来执行更多技巧来加快加载速度。

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

承担费用

最后要考虑的一件事是提交成本。这对您来说可能不是问题,因为psycopg2 默认打开事务并且在您告诉它之前不提交。除非你告诉它使用自动提交。但是对于许多数据库驱动程序来说,自动提交是默认设置。在这种情况下,您将为每个INSERT 进行一次提交。这意味着一次磁盘刷新,服务器确保将内存中的所有数据写入磁盘,并告诉磁盘将自己的缓存写入持久存储。这可能需要 很长 时间,并且根据硬件的不同而有很大差异。我的基于 SSD 的 NVMe BTRFS 笔记本电脑每秒只能执行 200 次 fsync,而每秒只能执行 300,000 次非同步写入。所以它只会加载 200 行/秒!有些服务器每秒只能执行 50 个 fsync。有些可以做20,000。所以如果你必须定期提交,尝试分批加载和提交,做多行插入等。因为COPY最后只做一次提交,提交成本可以忽略不计。但这也意味着COPY 无法从数据中途的错误中恢复;它撤消了整个批量加载。

【讨论】:

优秀而深入的答案。这就是我要找的。我可以询问其中一些主题的来源,以便我阅读吗? @Petar 我没有立即可用的参考资料,所以我会像你一样在谷歌上搜索。【参考方案2】:

复制使用批量加载,这意味着它每次插入多行,而简单插入一次只插入一个,但是您可以按照以下语法使用插入插入多行:

insert into table_name (column1, .., columnn) values (val1, ..valn), ..., (val1, ..valn)

有关使用批量加载的更多信息,请参阅例如The fastest way to load 1m rows in postgresql by Daniel Westermann.

一次应该插入多少行的问题取决于行长,一个好的经验法则是每个插入语句插入 100 行。

【讨论】:

虽然多插入是对单插入的优化,但\COPY 命令针对大型多插入进行了优化,而且它通常比多插入要快得多,操作也更复杂。 @mgoldwasser 只想说我正在寻找单插入与多行插入与COPY 命令之间的比较,我很高兴看到你的答案。有什么资料或基准可以阅读吗?【参考方案3】:

在事务中执行 INSERT 以加快速度。

在没有事务的情况下在 bash 中进行测试:

>  time ( for((i=0;i<100000;i++)); do echo 'INSERT INTO testtable (value) VALUES ('$i');'; done ) | psql root | uniq -c
 100000 INSERT 0 1

real    0m15.257s
user    0m2.344s
sys     0m2.102s

还有交易:

> time ( echo 'BEGIN;' && for((i=0;i<100000;i++)); do echo 'INSERT INTO testtable (value) VALUES ('$i');'; done && echo 'COMMIT;' ) | psql root | uniq -c
      1 BEGIN
 100000 INSERT 0 1
      1 COMMIT

real    0m7.933s
user    0m2.549s
sys     0m2.118s

【讨论】:

以上是关于COPY 是如何工作的,为啥它比 INSERT 快得多?的主要内容,如果未能解决你的问题,请参考以下文章

Interlocked 是如何工作的,为啥它比 lock 更快? [复制]

mysqldump 导出来.sql文件导入数据库的速度为啥比自己写的insert语句快

为啥 sax 解析比 dom 解析快?以及 stax 是如何工作的?

如果 PyPy 快 6.3 倍,为啥我不应该使用 PyPy 而不是 CPython?

为啥mysql中delete比insert要慢

为啥选择 JMS 作为异步解决方案?为啥它比简单的实体 bean 更好?