如何加快PostgreSQL中的插入性能


215

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

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

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

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


5
值得一提的还有您的Pg版本。在这种情况下,它并没有多大区别,但确实存在很多问题。
克雷格·林格

1
将索引放在表上并触发(如果有)并运行插入脚本。完成批量加载后,您可以重新创建索引。
Sandeep

Answers:


481

请参阅《 PostgreSQL手册》中的“ 填充数据库”,有关该主题的depesz优秀文章,以及有关SO的问题。

(请注意,这个答案是关于将数据批量加载到现有数据库中或创建一个新数据库。如果您对数据库恢复性能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=offcommit_delay减少fsync()成本。但是,如果您将工作分批进行大笔交易,这将无济于事。

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

  • 设置一个较高的checkpoint_segments值并启用log_checkpoints。查看PostgreSQL日志,确保它没有抱怨检查点过于频繁地出现。

  • 如果并且仅当您不介意在导入过程中系统崩溃时,将整个PostgreSQL群集(您的数据库和同一群集上的其他任何数据库)丢失而导致灾难性损坏,则可以停止Pg,set 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_xlog)存储在单独的磁盘/磁盘阵列上。在同一磁盘上使用单独的文件系统毫无意义。人们经常选择对WAL使用RAID1对。同样,这对具有高提交率的系统有更大的影响,并且如果您将未记录的表用作数据加载目标,则几乎没有影响。

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


1
您是否同意,如果使用高质量的SSD,可以从某种程度上减轻RAID 5/6的写入损失?显然仍然存在罚款,但是我认为这种区别所带来的痛苦远没有HDD那样痛苦。

1
我还没有测试。我想说这可能不太糟-讨厌的写放大效果和(对于小写)仍然需要读-修改-写周期,但是过度寻找的严厉惩罚应该是没有问题的。
Craig Ringer 2014年

我们是否可以禁用索引而不是删除索引,例如,将indisvalidpostgresql.org/docs/8.3/static/catalog-pg-index.html)设置为false,然后加载数据,然后使索引联机REINDEX
Vladislav Rastrusny 2014年

1
@CraigRinger我已经在Perc H730上测试了带有SSD的RAID-5和RAID-10。RAID-5实际上更快。同样值得注意的是,与大型bytea结合使用的插入/事务处理似乎比复制更快。总体而言,好的建议。
atlaste

2
有人看到有了任何重大的速度改进UNLOGGED吗?快速测试显示出10-20%的改善。
serg '02

15

使用COPY table TO ... WITH BINARY它根据文档“ 一定程度上比文本和CSV格式更快。” 仅当您要插入数百万行并且对二进制数据感到满意时,才执行此操作。

这是Python示例配方,将psycopg2与二进制输入结合使用


1
在某些输入(例如时间戳)上解析二进制文件很重要时,二进制模式可以节省大量时间。对于许多数据类型,它带来的好处并不多,或者由于带宽的增加(例如,小整数)而变得有些慢。好点了。
Craig Ringer

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);。无需显式打开事务。

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


较大的套接字缓冲区大小,A8=30000000在连接字符串中也应使用,以加快插入速度。
安德鲁斯

9

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

我不会列出所有无效的内容,并直接切成薄片。

将主键放在目标表(这是一个GUID)上,我的30MI或行以每100K小于3秒的恒定速度愉快地流到它们的目的地。


6

如果你happend插入用的UUID colums(这是不准确的情况下),并添加到@Dennis 答案(我不能评论还),是提醒不是使用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)

另外,这是建议的官方方式

注意

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

对于370M行,插入时间从〜2小时减少到〜10分钟。


1

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


-1

我也遇到了插入性能问题。我的解决方案是生成一些go例程以完成插入工作。在此期间,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()

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


好吧,你可以这么说。但是对于我的案例,它确实将数百万行的运行时间从数小时减少到了几分钟。:)
Patrick
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.