PostgreSQL 教程: 优化批量数据加载性能

九月 12, 2024

摘要:在本教程中,您将学习 PostgreSQL 中批量数据加载的一些性能优化技巧。

目录

背景

有时,PostgreSQL 数据库需要通过单个或最少的步骤,来导入大量数据。这通常称为批量数据导入,其中数据源通常是一个或多个大文件。这个过程有时可能会慢得令人无法接受。

导致性能如此糟糕的原因有很多,例如:索引、触发器、外键、GUID 主键,甚至预写式日志(WAL)也可能导致延迟。

在本文中,我们将介绍将数据批量导入 PostgreSQL 数据库的一些最佳实践技巧。但是,在某些情况下,这些方法也可能都不是那么有效。我们建议您在应用任何方法之前,先考虑好它的优缺点。

方法 1: 将目标表更改为 UNLOGGED 模式

对于 PostgreSQL 9.5 及更高版本,可以先将目标表更改为 UNLOGGED,然后在加载完数据后将其更改回 LOGGED:

ALTER TABLE <target table> SET UNLOGGED
<bulk data insert operations>
ALTER TABLE <target table> LOGGED

UNLOGGED 模式可确保 PostgreSQL 不会将表的写入操作记录到预写式日志(WAL)。这可以使加载过程非常快。但是,由于未记录操作日志,因此,如果在加载期间服务器发生崩溃或不正常的停机,则无法恢复数据。PostgreSQL 将在重新启动后自动截断任何 UNLOGGED 模式的表。

此外,UNLOGGED 模式的表不会同步到备用服务器。在这种情况下,必须在加载之前删除现有的复制,并在加载后重新创建现有复制。根据主节点中的数据量和备用节点的数量,重新创建复制的时间可能相当长,并且无法满足高可用的要求。

我们建议采用以下最佳实践,将数据批量插入到 UNLOGGED 模式的表中:

  • 在将表和数据更改为 UNLOGGED 模式之前对其进行备份
  • 数据加载完成后,重新创建到备用服务器的任何复制
  • 对可以轻松重新填充的表(例如,大型查找表或维度表),才使用 UNLOGGED 模式的批量插入

方法 2: 删除并重新创建索引

现有索引可能会导致批量数据插入期间出现严重延迟。这是因为在添加每一行时,相应的索引记录也必须更新。

我们建议在开始批量插入之前,尽可能删除目标表中的索引,并在加载完成后重新创建索引。同样,在大型表上创建索引可能很耗时,但通常比在加载期间更新索引更快。

DROP INDEX <index_name1>, <index_name2>  <index_name_n>
<bulk data insert operations>
CREATE INDEX <index_name> ON <target_table>(column1, ,column n)

在创建索引之前,临时调大 maintenance_work_mem 配置参数可能是值得的。增加的工作内存有助于更快地创建索引。

另一个安全的措施是,在同一数据库中创建目标表的副本,其中包含现有数据和索引。然后,可以使用这个新复制的表,对批量插入测试两种情况:删除并重新创建索引,或动态更新索引。然后,就可以将性能验证更好的方法应用到生产表上面。

方法 3: 删除并重新创建外键

与索引一样,外键约束也会影响批量加载性能。这是因为必须检查每个插入行中的每个外键是否存在相应的主键。在后台,PostgreSQL 使用触发器来执行检查。加载大量行时,必须为每行触发此触发器,这会增加开销。

除非受业务规则限制,否则我们建议从目标表中删除所有外键,在单个事务中加载数据,然后在提交事务后重新创建外键。

ALTER TABLE <target_table>
    DROP CONSTRAINT <foreign_key_constraint>

BEGIN TRANSACTION
    <bulk data insert operations>
COMMIT

ALTER TABLE <target_table>
    ADD CONSTRAINT <foreign key constraint>
    FOREIGN KEY (<foreign_key_field>)
    REFERENCES <parent_table>(<primary key field>)...

同样,调大 maintenance_work_mem 配置参数,可以提高重新创建外键约束的性能。

方法 4: 禁用触发器

INSERT 或 DELETE 触发器(如果加载过程还涉及从目标表中删除记录)可能会导致批量数据加载延迟。这是因为,每个触发器在每行被 INSERT 或 DELETE 后,都有需要检查的逻辑和需要立即完成的操作。

我们建议,在批量加载数据之前禁用目标表中的所有触发器,并在加载完成后启用它们。禁用的所有触发器也包括强制执行外键约束检查的内部触发器。

ALTER TABLE <target table> DISABLE TRIGGER ALL
<bulk data insert operations>
ALTER TABLE <target table> ENABLE TRIGGER ALL

方法 5: 使用 COPY 命令

我们建议使用 PostgreSQL 的 COPY 命令,从一个或多个文件加载数据。COPY 针对批量数据加载进行了优化。它比运行大量 INSERT 语句或者多行 INSERT 都要更加高效。

COPY <target table> [( column1>,  , <column_n>)]
    FROM  '<file_name_and_path>'
    WITH  (<option1>, <option2>,  , <option_n>)

使用 COPY 的其他好处包括:

  • 它支持文本和二进制文件导入
  • 它天然是事务性的
  • 它允许指定输入文件的结构
  • 它可以使用 WHERE 子句有条件地加载数据

方法 6: 使用多行 INSERT

对于批量数据加载来说,运行几千或几十万个 INSERT 语句,可能是一个糟糕的选择。这是因为,每个单独的 INSERT 命令都必须由查询优化器解析和准备,完成所有约束检查,作为单独的事务运行,并记录在 WAL 中。使用多行的单个 INSERT 语句可以节省此开销。

INSERT INTO <target_table> (<column1>, <column2>, , <column_n>) 
VALUES 
    (<value a>, <value b>, , <value x>),
    (<value 1>, <value 2>, , <value n>),
    (<value A>, <value B>, , <value Z>),
    (<value i>, <value ii>, , <value L>),
    ...

多行 INSERT 的性能受现有索引的影响。我们建议在运行命令之前删除索引,然后在运行之后重新创建索引。

另一个需要注意的方面是,PostgreSQL 可用于运行多行 INSERT 的内存大小。当运行多行 INSERT 时,内存中必须要容纳大量的输入值,除非有足够的可用内存,否则该过程可能会失败。

我们建议将 effective_cache_size 参数设置为机器总内存的 50%,shared_buffer 参数设置为总内存的 25%。此外,为了安全起见,最好运行一系列的多行 INSERT,每条语句都有 1000 行的值。

方法 7: 运行 ANALYZE

这与提高批量数据导入性能无关,但我们强烈建议,在批量导入后立即对目标表运行 ANALYZE 命令。大量新行会显著改变列中的数据分布,并导致表上的任何现有统计信息过时。当查询优化器使用过时的统计信息时,查询性能可能会差得令人无法接受。运行 ANALYZE 命令,可以确保任何现有的统计信息得到更新。

最后的思考

数据库应用程序可能并非每天都要进行批量数据导入,但运行时会对查询的性能产生影响。这就是为什么有必要尽可能减少加载时间的原因。为了最大限度地减少任何意外,DBA 可以做的一件事是,在具有类似服务器规格和 PostgreSQL 配置的开发环境或灰度环境中,测试负载的优化效果。每个数据加载方案都是不同的,最好尝试下每种方法,并找出最有效的方法。

了解更多

PostgreSQL 优化