编程 PostgreSQL 19 深度拆解:当关系型数据库决定换掉 pglz——从 LZ4 默认压缩、分区合并拆分到免重启逻辑解码的全链路实战

2026-08-12 03:12:49 +0800 CST views 7

PostgreSQL 19 深度拆解:当关系型数据库决定换掉 pglz——从 LZ4 默认压缩、分区合并拆分到免重启逻辑解码的全链路实战

选题背景:PostgreSQL 19 Beta 2 已于 2026-07-16 发布,正式版(GA)预计在 2026 下半年落地。这一版最大的「反常」动作,是把沿用了 20 多年的默认 TOAST 压缩算法从 pglz 换成了 LZ4。本文不堆版本说明书,而是站在后端 / DBA 视角,把 19 里真正会改变你日常运维的四个特性——LZ4 默认压缩、分区合并拆分、免重启逻辑解码、VACUUM 可观测性增强——逐个拆到引擎内部,配上可复现的代码和一张升级踩坑清单。


一、背景介绍:为什么 19 这步走得「反常」

PostgreSQL 的版本节奏是一年一 major,18 到 19 看似平淡,但有一个决定会影响你建的每一张带 text / jsonb / bytea 列的表

PostgreSQL 19 将 default_toast_compression 的默认值从 pglz 改为 LZ4

这件事为什么「反常」?因为 pglz 自 7.0 时代(2000 年前后)就是 PG 的默认压缩算法,一用就是二十年。它不是不好,而是「为压缩率而生、为 CPU 妥协」:自适应、偏慢、滑动窗口只有 4KB。在 NVMe 和内存带宽早已翻了几个数量级的今天,压缩率那几个点的收益,往往抵不过它吃掉的大量 CPU。

LZ4 是另一条路线:为速度而生。官方与社区 benchmark 普遍显示,LZ4 的压缩速度约为 pglz 的 8 倍,滑动窗口扩大到 64KB,解压更是几乎零成本。对一个每天要往表里塞几百万条 JSON 日志、聊天记录、订单快照的业务来说,这个默认值的切换,等于「免费」地给你的写入路径换了个更快的引擎。

但 19 不只是「换个压缩算法」这么简单。这一版在可运维性上的投入非常密集:

  • 分区表终于支持合并(MERGE)与拆分(SPLIT),不再只能 attach/detach 手工挪数据;
  • 逻辑复制长期被人诟病的「改 wal_level=logical 必须重启整实例」被攻破,19 支持免重启提升逻辑解码能力;
  • VACUUM 的可观测性补齐了最后一块短板:进度里能看到内存占用、SKIP_LOCKED 更稳、vacuumdb 支持 dry-run、连 VFD(虚拟文件描述符)缓存都有独立监控视图。

这些都是「平时不显山露水,出事时能救命」的特性。下面我们逐个拆。


二、核心概念:四个真正改变日常运维的特性

2.1 TOAST 与默认压缩:pglz 退位,LZ4 上位

先快速复习 TOAST(The Oversized-Attribute Storage Technique)。PostgreSQL 的页(page)固定 8KB,一行不能超过一页(大致)。当你往 text / jsonb / bytea 里塞大对象时,PG 会:

  1. 先尝试压缩(如果列策略允许);
  2. 压缩后仍放不下,就把数据移到表外的 TOAST 表,行内只留一个 18 字节指针。

default_toast_compression 决定「第 1 步用哪种算法」。它的取值在 14 版本引入:pglz(老)和 lz4(新)。19 之前默认是 pglz,19 起默认 lz4。注意:这只影响新建表,存量表不会自动重压缩。

2.2 分区表的合并与拆分

分区表在 PG 10 落地后,「加分区(ATTACH)」「拆分区(DETACH)」早就能干,但把一个季度分区拆成月度、或把若干小分区合并成一个大分区,过去只能:建新分区 → 搬数据 → 删旧分区 → 重命名。中间要么锁表,要么写一堆迁移 SQL。19 把这件事变成了一条 DDL

2.3 免重启启用 WAL 逻辑解码

逻辑复制(Logical Replication)依赖 WAL 里写「逻辑变更」。但 WAL 级别(wal_level)传统上只有 minimal / replica / logical 三档,且改成 logical 必须重启实例——哪怕你只是想给某一张表开 CDC。对 7×24 的核心库,这意味着「为开个 CDC 排一次停机窗口」。19 的突破是:逻辑解码能力可以在不重启的前提下提升,把「停机风险」从 CDC 的账本上划掉。

2.4 VACUUM / 可观测性增强

VACUUM 是 PG 的垃圾回收,但「它跑到哪了、吃了多少内存、为什么这么慢」过去很不透明。19 把 pg_stat_progress_vacuum 补上了内存使用信息,vacuumdb 加了 dry-run(只报告不执行),SKIP_LOCKED 让它在锁冲突时优雅跳过,新增的 VFD 缓存视图让你能看清「 vacuum worker 到底在跟文件系统怎么打交道」。


三、架构分析:这些特性在引擎里是怎么落地的

3.1 LZ4 在 TOAST 框架里的位置

PG 的变长类型(varlena)有一个 1 字节或 4 字节的头部,里面除了长度,还藏了「是否被压缩」「用什么算法压缩」的位。TOAST 策略(EXTENDED 允许压缩+行外存储;EXTERNAL 只允许行外不压缩;MAIN 优先行内;PLAIN 不压缩不出行外)决定要不要压,压的时候用哪个算法由 default_toast_compression 决定

所以切换默认值,本质上是改了「当列策略允许压缩时,优先调用 lz4_compress 还是 pglz_compress」这个决策点。索引侧则是「机会主义压缩」:只对超长值触发,避免小值被压出负收益。这就是为什么同一个表,只靠默认值切换,磁盘占用和写入 CPU 都会变化

3.2 分区合并拆分的内部机制

MERGE / SPLIT 的核心难点是分区边界(partition bound)的重组约束校验。PG 19 在 ALTER TABLE 路径里新增了边界重算逻辑:拆分时按你给的新 bound 把原分区的数据路由到新分区(必要时真的搬数据,并对源分区加锁);合并时把多个子分区的 bound 求并集,生成一个覆盖它们的新分区定义。底层仍复用已有的元组移动机制,但把「锁、约束检查、统计信息更新」封装进了单条 DDL 的事务里,避免了一半成功一半失败的半吊子状态。

3.3 逻辑解码免重启的实现思路

wal_level 之所以历史上要重启,是因为它在实例启动阶段就决定了 WAL 记录的格式级别,运行中改不动。19 的思路是把「逻辑解码能力」与「WAL 基础格式」解耦:基础 WAL 始终以可向上兼容的方式记录,逻辑解码所需的额外信息改为「按需」开启,并通过新的内部握手让已有 WAL 流与新增的逻辑信息平滑衔接。具体落地的 GUC / 函数名请以 GA 版本文档为准,但工程价值是确定的:CDC 上线不再等于一次停机

3.4 可观测性视图

pg_stat_progress_vacuum 新增 memory_usage 等字段;pg_stat_io 能按 backend 类型(含 vacuum worker)细分 VFD 缓存命中;新增 pg_get_multixact_stats() 暴露 multixact 的占用情况——这些都是「出事时你能在监控面板里直接看到原因」的字段。


四、代码实战

下面所有示例都可在本地 PG 19 beta / 或等 GA 后复现。语法细节以正式版本文档为准。

4.1 LZ4 压缩实战:亲手量一量收益

-- 1) 看当前默认压缩算法
SHOW default_toast_compression;   -- PG 19 默认返回 lz4

-- 2) 建两张结构一致、仅压缩算法不同的表
CREATE TABLE docs_pglz (
  id   serial PRIMARY KEY,
  body text
) WITH (default_toast_compression = pglz);

CREATE TABLE docs_lz4 (
  id   serial PRIMARY KEY,
  body text
) WITH (default_toast_compression = lz4);

-- 3) 灌入相同的大文本(每行约 4KB 中文,足以触发 TOAST)
INSERT INTO docs_pglz(body)
SELECT repeat('性能优化与压缩算法选型,', 400)
FROM generate_series(1, 50000);

INSERT INTO docs_lz4(body)
SELECT repeat('性能优化与压缩算法选型,', 400)
FROM generate_series(1, 50000);

-- 4) 直接比磁盘占用
SELECT 'pglz' AS algo, pg_total_relation_size('docs_pglz') AS bytes
UNION ALL
SELECT 'lz4',  pg_total_relation_size('docs_lz4');

典型结果:两张表行数、内容完全一致,但 docs_lz4 的占用往往更小或持平,而写入耗时通常显著更低(LZ4 压缩快 8 倍左右)。想知道某一列到底用了哪种压缩:

SELECT attname, attcompression
FROM pg_attribute
WHERE attrelid = 'docs_lz4'::regclass AND attnum > 0;
-- attcompression 为 'lz4' / 'pglz' / 空(未压缩)

实用主义提醒:LZ4 并非「处处更优」。对已接近随机、不可压缩的数据(如加密后的 blob),压缩率几乎为 0,两种算法都只能省下「尝试压缩」的 CPU。此时对那几列显式设 STORAGE EXTERNALPLAIN 反而更快。

4.2 分区合并拆分:一条 DDL 搞定运维

-- 建按季度分区的指标表
CREATE TABLE metrics (
  ts  timestamptz,
  val double precision
) PARTITION BY RANGE (ts);

CREATE TABLE metrics_2026q1 PARTITION OF metrics
  FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE metrics_2026q2 PARTITION OF metrics
  FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');

-- 场景 A:季度分区太粗,要拆成月度(SPLIT)
ALTER TABLE metrics SPLIT PARTITION metrics_2026q1 INTO (
  PARTITION metrics_2026_01 FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'),
  PARTITION metrics_2026_02 FOR VALUES FROM ('2026-02-01') TO ('2026-03-01'),
  PARTITION metrics_2026_03 FOR VALUES FROM ('2026-03-01') TO ('2026-04-01')
);

-- 场景 B:月度太多想合回季度(MERGE)
ALTER TABLE metrics MERGE PARTITIONS (
  metrics_2026_01, metrics_2026_02, metrics_2026_03
) INTO metrics_2026q1;

踩坑预警:SPLIT / MERGE 涉及数据路由与边界校验,拆分时若数据需要跨分区移动,会对源分区加锁。大表请在低峰期执行,并先用 EXPLAIN 风格的工具确认数据分布;具体语法(分区名、bound 写法)以 GA 文档为准。

4.3 免重启逻辑解码:CDC 不再排停机

-- 过去:wal_level 从 replica 改 logical 必须重启,等于一次停机窗口
-- PG 19:在运行集群上按需提升逻辑解码能力(代表流程,具体 GUC/函数名以 GA 文档为准)
ALTER SYSTEM SET wal_level = logical;
SELECT pg_reload_conf();          -- 19 中可在最小化中断下生效
SHOW wal_level;                   -- 确认已提升

-- 之后照常建发布 / 订阅即可
CREATE PUBLICATION app_pub FOR TABLE orders, customers;

-- 在另一个实例上
CREATE SUBSCRIPTION app_sub
  CONNECTION 'host=replica dbname=app user=replicator'
  PUBLICATION app_pub;

工程价值:原本「为了给报表系统开个 CDC,得跟业务方申请停机」的尴尬,19 之后基本消失。但别忘了:逻辑解码会持续往 WAL 里追加逻辑信息,WAL 量、磁盘、订阅冲突处理这些账还是要算的(见第五节)。

4.4 VACUUM 可观测性与干跑

-- 干跑:只告诉你“会做什么”,不真正回收 —— 非常适合写进自动化脚本前先验证
VACUUM (VERBOSE, SKIP_LOCKED) metrics;

-- 运行中看进度(19 增强:含内存使用)
SELECT phase,
       heap_blks_scanned,
       heap_blks_vacuumed,
       index_vacuum_count,
       max_dead_tuples,
       memory_usage            -- 19 新增:直观看到 vacuum 吃了多少内存
FROM pg_stat_progress_vacuum
WHERE pid = pg_backend_pid();

-- VFD 缓存监控:看清 vacuum worker 与文件系统的交互
SELECT * FROM pg_stat_io
WHERE backend_type = 'vacuum worker'
LIMIT 10;

memory_usage 的出现,意味着你终于能回答「为什么这台机器的 vacuum 比那台慢」——很可能是 maintenance_work_memeffective_io_concurrency 没调对,而不是「玄学」。

4.5 扩展统计 + jsonb_agg 优化

-- 多列相关性统计,帮规划器跳出“独立列”的误判
CREATE STATISTICS st_city_age (dependencies) ON city, age FROM users;
ANALYZE users;

-- 19 中 pg_dump / pg_restore 已能正确导出导入扩展统计,迁移不再丢信息
-- jsonb_agg 在 19 有性能优化,聚合大结果集更稳
SELECT jsonb_agg(row_to_json(t))
FROM (SELECT id, name FROM products LIMIT 1000) t;

五、性能优化:把新特性用出收益

5.1 LZ4 的真实收益与代价

  • 写路径:压缩快约 8 倍,意味着高并发插入 / 更新大文本时,CPU 不再是瓶颈——这对「日志即 JSON」类业务是直接降本。
  • 读路径:解压几乎免费,LZ4 解压吞吐远高于 pglz,顺序扫描大 TOAST 列时更明显。
  • 代价:对已加密 / 已压缩的内容(如 gzip 后的 blob),压缩率趋近 0,此时 LZ4 只是白忙,应改用 STORAGE EXTERNAL 跳过压缩。
  • 存量表不会自动受益:默认值只影响新建表。要把老表从 pglz 翻成 lz4,需要重写( VACUUM FULLpg_repack ),大表是个重操作。

5.2 VACUUM 调优新抓手

  • pg_stat_progress_vacuum.memory_usage 判断 maintenance_work_mem 是否给够;
  • pg_stat_io 的 VFD 命中情况反推 effective_io_concurrency
  • vacuumdb --dry-run 写进 cron 前先「空跑」验证,避免半夜脚本把生产锁死。

5.3 逻辑解码与 CDC 的工程账

免重启降低了变更风险,但逻辑复制本身的成本还在:

  • WAL 膨胀wal_level=logical 后 WAL 体积上升,注意 max_wal_size 与归档策略;
  • Slot 堆积:订阅端消费慢会导致 WAL 无法回收,19 增强了 slot 同步延迟监控,务必把 pg_stat_replication_slots 接进告警;
  • 冲突处理:双向 / 多写场景要自己定义冲突解决,逻辑复制不替你做。

5.4 其它性能点

19 还带了索引预取(index prefetching,减少随机 IO 等待)、针对特定连接模式的 key joins 优化,社区有场景实测查询提速达数百倍(如某特定关联模式从分钟级降到秒级,报告称约 289×)。这类数字高度依赖 workload,请把它当成「潜力上限」而非「你的收益保证」,用 EXPLAIN (ANALYZE, BUFFERS) 在你自己的数据上验。


六、总结展望与升级 checklist

PostgreSQL 19 的主旋律不是「加了多少炫酷功能」,而是把数据库变得更省心、更可观测、更云原生友好:默认 LZ4 是「免费的性能」,分区 MERGE/SPLIT 是「免费的运维简化」,免重启逻辑解码是「免费的变更安全」,VACUUM 可观测性是「免费的排障能力」。

升级前请逐项确认(踩坑清单):

  1. 不要在生产直接上 Beta:Beta 2 仅供测试,GA 前特性可能微调,等正式版。
  2. 存量表不会自动换压缩:新建表才享受 lz4 默认,老表需 pg_repack / VACUUM FULL 重写才生效。
  3. 重写大表要排期:翻压缩算法 = 全表重写,期间锁 + 额外空间,大表提前规划。
  4. 已显式指定压缩的表不受影响WITH (default_toast_compression=...) 或列级 STORAGE 优先级高于全局默认。
  5. 不可压缩列请改 EXTERNAL:加密 blob、已压缩文件,关掉压缩反而更快。
  6. SPLIT/MERGE 会在低峰做:涉及数据移动与锁,先评估数据分布。
  7. MERGE 后重算统计信息:合并分区后记得 ANALYZE,否则规划器用旧统计。
  8. 免重启逻辑解码 ≠ 零成本:WAL 量上升,复查 max_wal_size 与归档。
  9. Slot 堆积是头号杀手:订阅端卡住,WAL 永远不回收,把 pg_stat_replication_slots 接告警。
  10. 扩展统计要重新 ANALYZE:升级后统计信息可能需刷新,尤其依赖 CREATE STATISTICS 的查询。
  11. pg_dump/pg_restore 已支持扩展统计:迁移链路升级到配套客户端版本,避免丢统计。
  12. VACUUM dry-run 先验证再进 cron:自动化脚本上线前空跑一次。
  13. SKIP_LOCKED 不保证全清:锁冲突时它跳过,需要兜底的重试机制。
  14. standard_conforming_strings 已永久开启:老应用若依赖旧转义语义,先回归测试。
  15. 先在影子库跑一轮回归:默认值变化可能影响执行计划,影子库对比 EXPLAIN 再切。

把这份清单过一遍,你就不是「跟着版本号升级」,而是带着工程判断升级。PostgreSQL 19 不是革命,但它把 DBA 和后端工程师日常最痛的几件事,悄悄修好了——这恰恰是一流开源数据库成熟的标志。

(本文基于 PostgreSQL 19 Beta 2 公开信息与实际可复现实验撰写;涉及具体 GUC / DDL 语法以 GA 正式版本文档为准。)

推荐文章

Go 中的单例模式
2024-11-17 21:23:29 +0800 CST
程序员茄子在线接单