如何加快 PostgreSQL 中的插入性能

Posted

技术标签:

【中文标题】如何加快 PostgreSQL 中的插入性能【英文标题】:How to speed up insertion performance in PostgreSQL 【发布时间】:2012-08-25 18:07:48 【问题描述】:

我正在测试 Postgres 插入性能。我有一个表,其中有一列以数字作为其数据类型。上面也有索引。我用这个查询填满了数据库:

insert into aNumber (id) values (564),(43536),(34560) ...

我使用上面的查询非常快速地一次插入了 10,000 行 400 万行。在数据库达到 600 万行后,性能急剧下降到每 15 分钟 100 万行。有什么技巧可以提高插入性能吗?我需要此项目的最佳插入性能。

在具有 5 GB RAM 的计算机上使用 Windows 7 Pro。

【问题讨论】:

在问题中也值得一提你的 Pg 版本。在这种情况下,它并没有太大的区别,但它确实可以解决很多问题。 删除表上的索引并触发(如果有)并运行插入脚本。完成批量加载后,您可以重新创建索引。 【参考方案1】:

请参阅 PostgreSQL 手册中的 populate a database、有关主题的 depesz's excellent-as-usual article 和 this SO question。

(请注意,此答案是关于将数据批量加载到现有数据库或创建新数据库。如果您对pg_restorepsql 执行pg_dump 输出的数据库恢复性能感兴趣,其中大部分不适用,因为 pg_dumppg_restore 已经在完成架构+数据恢复后创建触发器和索引等操作)

还有很多事情要做。理想的解决方案是导入没有索引的UNLOGGED 表,然后将其更改为记录并添加索引。不幸的是,在 PostgreSQL 9.4 中,不支持将表从 UNLOGGED 更改为已记录。 9.5 添加了ALTER TABLE ... SET LOGGED 以允许您这样做。

如果您可以将数据库脱机以进行批量导入,请使用pg_bulkload

否则:

禁用表上的所有触发器

在开始导入之前删除索引,然后重新创建它们。 (与逐步向其中添加相同数据相比,一次构建索引所花费的时间要少得多,并且生成的索引更加紧凑)。

如果在单个事务中执行导入,则删除外键约束、执行导入并在提交前重新创建约束是安全的。如果导入拆分为多个事务,请不要这样做,因为您可能会引入无效数据。

如果可能,请使用COPY 而不是INSERTs

如果您不能使用COPY,请考虑在可行的情况下使用多值INSERTs。你似乎已经在这样做了。不要试图在单个VALUES 中列出太多 个值;这些值必须在内存中适应几次,所以每条语句保持几百个。

批量插入显式事务,每个事务执行数十万或数百万次插入。 AFAIK 没有实际限制,但是通过在输入数据中标记每个批次的开始,批处理可以让您从错误中恢复。同样,您似乎已经这样做了。

使用 synchronous_commit=off 和巨大的 commit_delay 来降低 fsync() 成本。但是,如果您将工作批量处理成大事务,这将无济于事。

INSERTCOPY 从多个连接并行。多少取决于硬件的磁盘子系统;根据经验,如果使用直连存储,则每个物理硬盘驱动器需要一个连接。

设置较高的max_wal_size 值(旧版本为checkpoint_segments)并启用log_checkpoints。查看 PostgreSQL 日志并确保它没有抱怨检查点发生得太频繁。

当且仅当您不介意在导入过程中系统崩溃时将整个 PostgreSQL 集群(您的数据库和同一集群上的任何其他集群)丢失至灾难性损坏,您可以停止 Pg,设置 fsync=off,开始Pg,进行导入,然后(重要地)停止 Pg 并再次设置 fsync=on。见WAL configuration。 如果您的 PostgreSQL 安装中的任何数据库中已经存在您关心的任何数据,请不要这样做。 如果您设置了fsync=off,您也可以设置full_page_writes=off;同样,请记住在导入后重新打开它,以防止数据库损坏和数据丢失。请参阅 Pg 手册中的non-durable settings。

您还应该考虑调整您的系统:

尽可能使用优质 SSD 进行存储。具有可靠、受电源保护的回写高速缓存的优质 SSD 可让提交速度快得令人难以置信。当您遵循上述建议时,它们的用处不大 - 这减少了磁盘刷新/fsync()s 的数量 - 但仍然可以提供很大帮助。不要使用没有适当电源故障保护的廉价 SSD,除非您不关心保存数据。

如果您将 RAID 5 或 RAID 6 用于直接连接的存储,请立即停止。备份您的数据,将您的 RAID 阵列重组为 RAID 10,然后重试。 RAID 5/6 对于批量写入性能毫无希望 - 尽管具有大缓存的良好 RAID 控制器会有所帮助。

如果您可以选择使用具有大电池支持的回写缓存的硬件 RAID 控制器,这可以真正提高具有大量提交的工作负载的写入性能。如果您使用带有 commit_delay 的异步提交,或者您在批量加载期间执行的大事务较少,则没有多大帮助。

如果可能,将 WAL(pg_wal,或旧版本中的pg_xlog)存储在单独的磁盘/磁盘阵列上。在同一个磁盘上使用单独的文件系统没有什么意义。人们经常选择为 WAL 使用 RAID1 对。同样,这对具有高提交率的系统影响更大,如果您使用未记录的表作为数据加载目标,则影响不大。

您可能也对Optimise PostgreSQL for fast testing感兴趣。

【讨论】:

如果使用优质 SSD,您是否同意 RAID 5/6 的写入损失会有所减轻?显然还是有惩罚的,但我认为区别远没有硬盘驱动器那么痛苦。 我还没有测试过。我想说这可能没那么糟糕 - 讨厌的写入放大效应和(对于小写入)对读取-修改-写入周期的需求仍然存在,但过度搜索的严重惩罚应该不是问题。 我们是否可以只禁用索引而不是删除它们,例如,通过将indisvalid (postgresql.org/docs/8.3/static/catalog-pg-index.html) 设置为 false,然后加载数据,然后通过 REINDEX 使索引联机? @CraigRinger 我已经在 Perc H730 上使用 SSD 测试了 RAID-5 与 RAID-10。 RAID-5 实际上更快。另外值得注意的是,插入/事务与大字节茶的结合似乎比复制更快。不过总体来说还是不错的建议。 有人看到UNLOGGED 的速度有任何重大改进吗?快速测试显示大约有 10-20% 的改进。【参考方案2】:

我今天在同一个问题上花了大约 6 个小时。插入以“常规”速度(每 100K 不到 3 秒)直到 5MI(总共 30MI)行,然后性能急剧下降(一直下降到每 100K 1 分钟)。

我不会列出所有不起作用的东西,直接切入肉中。

在目标表(这是一个 GUID)上删除了一个主键,我的 30MI 或行以每 100K 不到 3 秒的恒定速度愉快地流向目的地。

【讨论】:

这对我帮助很大 @Dennis 所以你加入的表或从中获取数据无关紧要,只有你插入的表?我绝对只是尝试过这个,它更快哇谢谢【参考方案3】:

根据文档“is somewhat faster than the text and CSV formats”使用COPY table TO ... WITH BINARY。仅当您要插入数百万行并且对二进制数据感到满意时才这样做。

这是example recipe in Python, using psycopg2 with binary input。

【讨论】:

二进制模式可以节省一些输入的大量时间,例如时间戳,在这些输入中解析它们是非常重要的。对于许多数据类型,它并没有提供太多好处,或者由于带宽增加(例如小整数)甚至可能会稍微慢一些。好点提高它。【参考方案4】:

除了优秀的 Craig Ringer 的文章和 depesz 的博客文章之外,如果您想通过在事务中使用准备好的语句插入来通过 ODBC (psqlodbc) 接口加速插入,您还需要一些额外的东西如何让它快速工作:

    通过在连接字符串中指定Protocol=-1 将错误回滚级别设置为“事务”。默认情况下,psqlodbc 使用“语句”级别,它为每个语句而不是整个事务创建一个 SAVEPOINT,从而使插入速度变慢。 通过在连接字符串中指定UseServerSidePrepare=1 来使用服务器端准备好的语句。如果没有这个选项,客户端会发送整个插入语句以及插入的每一行。 使用SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF), 0);禁用每个语句的自动提交 插入所有行后,使用SQLEndTran(SQL_HANDLE_DBC, conn, SQL_COMMIT); 提交事务。无需显式打开事务。

不幸的是,psqlodbc “实现”SQLBulkOperations 通过发出一系列未准备好的插入语句,因此要实现最快的插入,需要手动编写上述步骤。

【讨论】:

大套接字缓冲区大小,连接字符串中的A8=30000000 也应该用于加速插入。【参考方案5】:

如果您碰巧插入了带有 UUID 的列(这不是 完全您的情况)并添加到 @Dennis answer(我还不能评论),建议不要使用 gen_random_uuid( )(需要 PG 9.4 和 pgcrypto 模块)比 uuid_generate_v4() 快(很多)

=# explain analyze select uuid_generate_v4(),* from generate_series(1,10000);
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=11.674..10304.959 rows=10000 loops=1)
 Planning time: 0.157 ms
 Execution time: 13353.098 ms
(3 filas)


=# explain analyze select gen_random_uuid(),* from generate_series(1,10000);
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=252.274..418.137 rows=10000 loops=1)
 Planning time: 0.064 ms
 Execution time: 503.818 ms
(3 filas)

另外,这是suggested官方的做法

注意

如果您只需要随机生成的(版本 4)UUID,请考虑使用 pgcrypto 模块中的 gen_random_uuid() 函数。

这将 370 万行的插入时间从约 2 小时减少到约 10 分钟。

【讨论】:

【参考方案6】:

为了获得最佳插入性能,如果您愿意,请禁用索引。除此之外,更好的硬件(磁盘、内存)也很有帮助

【讨论】:

【参考方案7】:

我也遇到了这个插入性能问题。我的解决方案是生成一些 goroutine 来完成插入工作。同时,SetMaxOpenConns 应该被赋予一个适当的数字,否则会警告过多的打开连接错误。

db, _ := sql.open() 
db.SetMaxOpenConns(SOME CONFIG INTEGER NUMBER) 
var wg sync.WaitGroup
for _, query := range queries 
    wg.Add(1)
    go func(msg string) 
        defer wg.Done()
        _, err := db.Exec(msg)
        if err != nil 
            fmt.Println(err)
        
    (query)

wg.Wait()

我的项目的加载速度要快得多。这段代码 sn-p 只是说明了它是如何工作的。读者应该能够轻松地对其进行修改。

【讨论】:

好吧,你可以这么说。但对于我的案例,它确实将数百万行的运行时间从几个小时减少到了几分钟。 :)

以上是关于如何加快 PostgreSQL 中的插入性能的主要内容,如果未能解决你的问题,请参考以下文章

带有索引的 MySQL 表:许多插入/更新会影响性能吗?如果是,如何再次加快速度?

如何加快计算 PostgreSQL 表中的行数?

加快在 PostgreSQL 中的搜索

SQL加快插入的性能?

尝试使用 java 将数据插入 PostgresQL 表时,序列应用程序的权限被拒绝

如果在单个请求中与 Oracle 和 PostgreSql 交互以处理超过 20 万条记录,如何提高 EF Core 性能