PostgreSQL 19:那些经得起版本升级的实践,以及真正需要更新的细节

2026-08-19 50 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:15 分钟

PostgreSQL 19 仍处于 beta 阶段,最终版本的细节可能变化。不过,从当前版本可以看出一个重要趋势:很多针对 PostgreSQL 10、11 写下的建议并没有失效,真正变化的是执行这些建议时的性能边界和运维工具。

COPY 依然是批量导入的首选,B-tree 仍是默认索引,BRIN 仍适合物理顺序与业务顺序高度相关的数据,分区表仍然首先服务于生命周期管理。PostgreSQL 18 和 19 做的是把这些路径变得更快、更有弹性,也更容易维护。

异步 I/O 改变了扫描和 vacuum 的比较方式

过去比较 BRIN、B-tree 和并行顺序扫描时,结论很依赖磁盘延迟。一个看起来“聪明”的 BRIN 查询,并不一定比多个并行 worker 扫描堆表更快。

PostgreSQL 18 引入异步 I/O 后,后端可以同时排队多个磁盘读取,不必每次读取都同步等待。顺序扫描、BRIN 常见的 bitmap heap scan,以及 vacuum 都能受益。在冷数据、云盘或延迟较高的存储上,社区测试观察到过约 3 倍的提升。

PostgreSQL 19 继续扩展这条路径:I/O worker 可以自动伸缩,预读调度得到改进,EXPLAIN (ANALYZE, IO) 也能展示更多 I/O 行为。并行查询依旧有价值,因为每个 worker 都可以在部分读取仍处于进行中时继续处理其他工作。

维护操作也有变化。PostgreSQL 19 支持并行 autovacuum worker,可以通过 autovacuum_max_parallel_workers 和表级 autovacuum_parallel_workers 扩大大型表的维护吞吐量。不过默认值仍然偏保守,应根据 vacuum 延迟和存储能力进行调整。

另一个容易被忽略的变化是:JIT 在 PostgreSQL 19 中默认关闭。它从 PostgreSQL 12 起默认开启,但旧的成本模型并不总是可靠。升级后,如果数据仓库或大型分析查询依赖 JIT,应显式重新开启,并重新检查执行计划。

建议升级后使用新的 I/O 观测方式重新比较方案:

EXPLAIN (ANALYZE, BUFFERS, IO)
SELECT count(*)
FROM sensor_readings
WHERE recorded_at >= timestamptz '2025-01-01'
  AND recorded_at <  timestamptz '2025-02-01';

不要因为“有索引”就默认索引路径更快。应在实际存储设备上比较 BRIN、并行顺序扫描和 bitmap heap scan。

COPY 仍然是批量加载的主路径

逐行 INSERT 依然适合少量事务性写入,但面对 CSV、JSONL 或外部批处理文件时,COPY ... FROM STDIN 仍是更合理的选择。PostgreSQL 16 到 19 主要增强了容错能力和格式处理能力,而不是重新发明加载路径:

  • PostgreSQL 16 支持把指定哨兵字符串映射为列默认值。
  • PostgreSQL 17 支持 ON_ERROR ignore,并可报告被跳过的数据。
  • PostgreSQL 18 增加 REJECT_LIMIT,可以限制允许忽略的错误行数。
  • PostgreSQL 19 支持更快的 SIMD 文本和 CSV 解析、ON_ERROR set_null、跳过多行表头,以及将查询结果导出为 JSON。

例如,一个前面带有两行说明文字的、不完全可靠的 CSV,可以这样导入:

COPY events (event_id, occurred_at, payload)
FROM STDIN
WITH (
  FORMAT csv,
  HEADER 2,
  DEFAULT '__DEFAULT__',
  ON_ERROR set_null,
  LOG_VERBOSITY verbose
);

这里的 HEADER 2 会跳过开头两行。__DEFAULT__ 是业务约定的哨兵值,遇到它时使用列的默认表达式。不要随意使用真实数据可能出现的字符串,也要注意 CSV 中的 \\N 容易与 NULL 标记产生混淆。

如果错误字段对应的整行都不应该保留,可以改用 ignore,并设置错误上限:

COPY events (event_id, occurred_at, payload)
FROM STDIN
WITH (
  FORMAT csv,
  HEADER 2,
  ON_ERROR ignore,
  REJECT_LIMIT 1000,
  LOG_VERBOSITY verbose
);

对于新建或刚清空的 staging 表,一次性加载后就要投入使用时,FREEZE 仍然是减少后续 freeze vacuum 的实用技巧。导出方面,PostgreSQL 19 可以直接从分区表根表执行:

COPY events TO STDOUT WITH (FORMAT json);
COPY events TO STDOUT WITH (FORMAT json, FORCE_ARRAY);

普通 FORMAT json 适合逐行 JSON,也就是 NDJSON 或 JSONL。FORCE_ARRAY 则生成一个完整 JSON 数组,适合某些 Web API。JSON 数据仍应优先存储为 jsonb,除非确实需要保留原始格式、空白或键顺序。

TOAST 的模型没变,默认压缩器变快了

TOAST 依旧围绕 8 KB 页面、约 2 KB 的 toast_tuple_target 和不同存储策略工作。大字段被压缩或拆分到 toast 表后,读取、更新和维护都会产生代价。

最重要的建议没有改变:经常参与过滤、连接或排序的大型 JSON 和文本,不应长期作为一个巨大的 toasted blob 存在。能拆成结构化列,就应优先拆分。已经自行压缩的数据则可以考虑 EXTERNAL,避免重复压缩。

PostgreSQL 14 已支持 LZ4,PostgreSQL 19 将 LZ4 设为默认 TOAST 压缩算法。它通常比 pglz 更快地压缩和解压,同时保持相近的压缩率。升级旧集群后,已有 toast 数据不会自动改用 LZ4;旧数据继续使用原来的算法,新写入的数据才遵循新默认值。

可以用下面的查询粗略观察原始大小和存储大小:

SELECT
  pg_column_size(payload) AS stored_bytes,
  octet_length(payload) AS raw_bytes,
  pg_column_compression(payload) AS compression
FROM events
WHERE length(payload) > 100
LIMIT 5;

PostgreSQL 19 还提供原生 REPACK,可以在不长时间阻塞读写的情况下重写表:

REPACK (CONCURRENTLY, ANALYZE) events;

并发重写需要主键或基于索引的 replica identity,而且不能直接用于分区父表,应分别处理子表。重写期间大约需要额外一份完整表空间,因此至少要按两倍表大小规划磁盘。它也不会自动重新压缩已有的 pglz toast chunk;要改变旧数据的压缩算法,仍需要通过更新或其他重写方式触发。

BRIN 选择更多,但仍不是 B-tree 替代品

BRIN 的核心优势一直没有变化:索引很小,并且特别适合物理存储顺序与逻辑值顺序高度相关的列,例如按时间追加的传感器数据。

PostgreSQL 14 增加了 minmax_multi 和 Bloom BRIN。普通 min/max 为每个范围保存一组边界;如果少量乱序值把范围拉得很宽,minmax_multi 可以保存多组边界,减少无效堆扫描。Bloom BRIN 则适合相关性较低、但需要等值判断的场景,例如某些 UUID、MAC 地址或分类字段。

例如,存在轻微乱序写入的时间序列表可以使用:

CREATE INDEX scans_created_at_brin_idx
ON scans
USING brin (created_at timestamptz_minmax_multi_ops)
WITH (pages_per_range = 32);

默认 pages_per_range 是 128,大约对应 1 MB 堆表范围。设置为 32 会让索引略微变大,但能提高摘要分辨率、减少时间范围查询触碰的堆页面数量。具体值应根据查询时间窗口和数据写入顺序测试。

对不太有序的等值查询,可以尝试 Bloom BRIN:

CREATE INDEX events_id_brin_bloom
ON events
USING brin (event_id uuid_bloom_ops);

PostgreSQL 17 起,大型 BRIN 创建可以使用并行 worker。PostgreSQL 18 和 19 的异步 I/O 又会同时加速 BRIN 命中后的 bitmap heap scan,以及可能胜出的并行顺序扫描。因此,BRIN 的判断标准仍然是相关性,而不是“表很大”这一条本身。

INCLUDE 与 skip scan:先检查已有索引

覆盖索引的基本原则没有改变。INCLUDE 列不参与排序键,却可以让查询直接从索引返回结果:

CREATE UNIQUE INDEX visits_visitor_visited_at_geocode_idx
ON visits (visitor, visited_at)
INCLUDE (geocode);

要真正获得 index-only scan,还需要 visibility map 足够新,因此要让 vacuum 或 autovacuum 正常工作。INCLUDE 列越多,索引越大,写入放大和维护成本越高。

PostgreSQL 18 的 B-tree skip scan 则扩大了多列索引的适用范围。假设索引为 (visitor, visited_at),而查询只过滤 visited_at,如果前导列 visitor 的 distinct 值很少,planner 现在可能跳过前导列的部分索引范围来执行查询。

这不意味着每个复合索引都能替代单列索引。高基数前导列、范围条件,以及 skip scan 成本较高的查询仍可能需要专用索引。实际加索引前,先用 EXPLAIN 检查现有复合索引是否已经能够满足工作负载。PostgreSQL 17 对 WHERE id IN (...) 一类多值 B-tree 查找也做了优化,升级后应先重新基准测试,再决定是否需要额外的定制方案。

分区表:优先解决生命周期问题

分区最稳固的价值仍然是生命周期管理:按天、月或季度创建子表,归档时 detach 或直接 drop,避免对数十亿行执行巨大的 DELETE

PostgreSQL 14 支持并发 detach,可以降低日常轮换的锁影响:

ALTER TABLE iot_thermostat
DETACH PARTITION iot_thermostat07142022 CONCURRENTLY;

默认分区仍然适合作为错误时间戳和迟到数据的安全网,但必须持续监控并定期清空。pg_partman 依然适合负责创建和保留周期;原生 DDL 变强,并不等于它已经替代了日历调度自动化。

PostgreSQL 19 增加了原生合并和拆分分区的语法,例如:

ALTER TABLE events
MERGE PARTITIONS (
  events_2024_01,
  events_2024_02,
  events_2024_03
)
INTO events_2024_q1;

也可以拆分一个范围:

ALTER TABLE events
SPLIT PARTITION events_2024_q1 INTO (
  PARTITION events_2024_01
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'),
  PARTITION events_2024_02
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'),
  PARTITION events_2024_03
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01')
);

合并和拆分目前需要父表上的 ACCESS EXCLUSIVE 锁,而且会物理复制元组,锁可能持续到整个复制完成。它们还会删除源分区上直接定义的本地索引和约束;父表定义的对象会在新子表上自动重建。因此,高吞吐表的在线轮换仍应优先使用预建子表、attach 和 concurrent detach,把 merge/split 留给低峰维护窗口。

分区父表上的全局唯一性依旧受限制:如果希望整个分区集合保持唯一,唯一索引通常必须包含分区键。

升级后的实用检查清单

PostgreSQL 19 改进了很多边界,但没有推翻基础建模原则:

  • 批量写入继续优先使用 COPY,而不是逐行 INSERT
  • JSON 查询场景继续优先使用 jsonb,需要包含查询时考虑 GIN。
  • 不要把频繁过滤和连接的大型字段当作一个 toasted blob。
  • B-tree 仍是通用默认选择,其他索引类型应由访问模式驱动。
  • BRIN 适合与物理顺序高度相关的追加型数据。
  • INCLUDE 能支持覆盖查询,但每个包含列都会增加索引成本。
  • 分区首先服务于保留策略、归档和快速释放空间。
  • 使用 EXPLAIN (ANALYZE, BUFFERS, IO) 验证真实收益,不要根据索引存在与否推断性能。
  • 升级后检查 JIT、异步 I/O、autovacuum 并行度和 effective_io_concurrency 等配置。
  • PostgreSQL 19 仍在 beta 阶段,涉及 COPY 容错、分区 merge/split 和 REPACK 的细节应以最终文档为准。

真正需要更新的不是整套数据库方法论,而是评估方式:重新在目标存储、真实数据分布和实际查询负载上测量。旧建议仍然成立,只是 PostgreSQL 现在能更快地把这些建议执行出来。


相关推荐