我正在测试Postgres插入性能。我有一张表,其中一列以数字作为其数据类型。也有一个索引。我使用以下查询填充数据库:
insert into aNumber (id) values (564),(43536),(34560) ...
通过上面的查询,我一次非常快地一次插入了400万行10,000。数据库达到600万行后,性能每15分钟急剧下降到100万行。有什么技巧可以提高插入性能?我需要此项目的最佳插入性能。
在具有5 GB RAM的计算机上使用Windows 7 Pro。
我正在测试Postgres插入性能。我有一张表,其中一列以数字作为其数据类型。也有一个索引。我使用以下查询填充数据库:
insert into aNumber (id) values (564),(43536),(34560) ...
通过上面的查询,我一次非常快地一次插入了400万行10,000。数据库达到600万行后,性能每15分钟急剧下降到100万行。有什么技巧可以提高插入性能?我需要此项目的最佳插入性能。
在具有5 GB RAM的计算机上使用Windows 7 Pro。
Answers:
请参阅《 PostgreSQL手册》中的“ 填充数据库”,有关该主题的depesz优秀文章,以及有关SO的问题。。
(请注意,这个答案是关于将数据批量加载到现有数据库中或创建一个新数据库。如果您对数据库恢复性能pg_restore
或psql
执行pg_dump
输出感兴趣,那么此操作就不适用了,因为pg_dump
它pg_restore
已经做了诸如创建完成架构+数据还原后触发和索引)。
有很多事情要做。理想的解决方案是导入UNLOGGED
没有索引的表,然后将其更改为已记录并添加索引。不幸的是,在PostgreSQL 9.4中,不支持将表从更改UNLOGGED
为已记录。9.5 ALTER TABLE ... SET LOGGED
允许您执行此操作。
如果您可以使数据库脱机以进行批量导入,请使用pg_bulkload
。
除此以外:
禁用表格上的所有触发器
在开始导入之前删除索引,然后再重新创建它们。(一次建立索引所花费的时间要比向其逐步添加相同数据所花费的时间少得多,并且所产生的索引要紧凑得多)。
如果在单个事务中进行导入,则在提交之前,可以安全地删除外键约束,进行导入并重新创建约束。如果导入分散在多个事务中,则不要执行此操作,因为这可能会引入无效数据。
如果可能,使用COPY
代替INSERT
s
如果不能使用,请COPY
考虑使用多值INSERT
s(如果可行)。您似乎已经在执行此操作。但是,不要试图在一个列表中列出太多的值VALUES
。这些值必须多次存储在内存中,因此每个语句将其保留为几百个。
将插入的内容批量处理为显式事务,每个事务执行数十万或数百万个插入。AFAIK没有实际限制,但批处理可通过在输入数据中标记每个批处理的开始来使您从错误中恢复。同样,您似乎已经在执行此操作。
使用synchronous_commit=off
和commit_delay
减少fsync()成本。但是,如果您将工作分批进行大笔交易,这将无济于事。
INSERT
或COPY
通过多个连接并行连接。有多少取决于您的硬件的磁盘子系统;根据经验,如果使用直接连接的存储,则每个物理硬盘驱动器需要一个连接。
设置一个较高的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感兴趣以进行快速测试。
indisvalid
(postgresql.org/docs/8.3/static/catalog-pg-index.html)设置为false,然后加载数据,然后使索引联机REINDEX
?
UNLOGGED
吗?快速测试显示出10-20%的改善。
使用COPY table TO ... WITH BINARY
它根据文档“ 一定程度上比文本和CSV格式更快。” 仅当您要插入数百万行并且对二进制数据感到满意时,才执行此操作。
除了出色的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
在连接字符串中也应使用,以加快插入速度。
如果你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分钟。
我也遇到了插入性能问题。我的解决方案是生成一些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()
对于我的项目,加载速度要快得多。该代码段只是说明了它是如何工作的。读者应该能够轻松地对其进行修改。