261

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

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

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

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

4

7 回答 7

546

请参阅PostgreSQL 手册中的填充数据库、 depesz关于该主题的优秀文章以及这个 SO question

(请注意,此答案是关于将数据批量加载到现有数据库中或创建新数据库。如果您对使用pg_restorepsql执行pg_dump输出的数据库恢复性能感兴趣,其中大部分内容并不适用,因为pg_dump并且pg_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 配置如果 PostgreSQL 安装上的任何数据库中已经存在您关心的任何数据,请不要这样做。如果你设置了fsync=off,你也可以设置full_page_writes=off;同样,请记住在导入后重新打开它,以防止数据库损坏和数据丢失。请参阅Pg 手册中的非持久设置

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

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

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

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

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

您可能还对优化 PostgreSQL 以进行快速测试感兴趣。

于 2012-08-30T23:58:56.250 回答
17

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

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

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

于 2018-08-10T23:19:26.710 回答
16

COPY table TO ... WITH BINARY根据文档使用“比文本和 CSV 格式要快一些”。仅当您有数百万行要插入并且您对二进制数据感到满意时才这样做。

这是Python 中的示例配方,使用带有二进制输入的 psycopg2

于 2014-06-13T07:05:09.657 回答
11

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

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

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

于 2014-11-04T14:12:07.810 回答
8

如果您碰巧插入带有 UUID 的列(这不完全是您的情况)并添加到@Dennis答案(我还不能评论),建议不要使用 gen_random_uuid() (需要 PG 9.4 和 pgcrypto 模块)是(a很多)比 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)

此外,这是建议的官方方法

笔记

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

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

于 2019-10-23T21:12:50.310 回答
2

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

于 2012-08-30T22:43:06.330 回答
-6

我也遇到了这个插入性能问题。我的解决方案是生成一些 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()

我的项目的加载速度要快得多。这个代码片段只是给出了它是如何工作的想法。读者应该能够轻松地对其进行修改。

于 2018-03-13T21:29:04.040 回答