Postgres 19 拟默认启用 LZ4:从 TOAST 压缩到 B-tree 大键的完整路径

2026-07-16 32 预计阅读时间: 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.

预计阅读时间:10 分钟

Postgres 19 计划把默认 TOAST 压缩算法从 pglz 改为 LZ4。这不只是替换一个配置默认值:表内行、TOAST 表和索引都使用以 varlena 为基础的统一压缩框架,算法变化会同时影响写入 CPU、存储空间以及大索引键能否写入。

LZ4 从 Postgres 14 起已经可以显式启用。Postgres 19 的变化可以理解为一次谨慎推进:先让用户选择并积累实践,再考虑把它设为默认方案。

TOAST 解决的不是“字段类型”,而是物理存储

Postgres 早期存在严格的 8 kB 行大小限制。7.0 版本曾提供使用 pglz 的 lztext 类型,但它要求用户主动选择一种特殊类型,也没有从根本上解除行大小限制。

Postgres 7.1 引入 TOAST 后,压缩下沉到物理存储层。应用仍然使用 TEXTVARCHARBYTEAJSONB,数据库负责决定值应该:

  • 未压缩地放在主表行中;
  • 压缩后留在主表行中;
  • 移到 TOAST 表并在主表中留下约 18 字节的指针;
  • 在 TOAST 表中以压缩或未压缩形式保存。

这些变长类型使用自描述的 varlena 格式,其头部记录长度和压缩状态。INTFLOATBOOL 等定长类型不会经过这套压缩流程。

列的存储策略决定了 Postgres 可以采取哪些动作:

策略 行为
EXTENDED 允许压缩和移出行外,是多数变长类型的默认策略
PLAIN 保持行内且不压缩,通常用于定长类型
EXTERNAL 可以移到 TOAST,但不压缩
MAIN 优先压缩并留在主表,必要时才移到 TOAST

一次写入如何触发压缩

默认情况下,Postgres 会尝试把主表行控制在约 2 kB,也就是默认 toast_tuple_target 对应的 2040 字节附近。对于 EXTENDED 列,处理过程大致如下:

  1. 行本来就小于目标值时,直接写入,不为压缩额外消耗 CPU。
  2. 行过大时,按字段大小尝试压缩最大的可压缩列。
  3. 压缩后已经达到目标便停止,否则继续处理其他列。
  4. 仍然过大时,把最大的 EXTENDEDEXTERNAL 值移到 TOAST 表。
  5. 必要时再处理 MAIN 列,先压缩,最后才将其移出行外。

这解释了一个容易误解的现象:声明一个 TEXT COMPRESSION lz4 列,并不意味着每个短字符串都会被压缩。压缩由整行大小和存储策略共同触发。

pglz 当初针对低内存、跨平台和无外部依赖设计,使用 4 kB 滑动窗口,并在数据难以压缩时尽早放弃。LZ4 使用更大的 64 kB 窗口,在现代硬件上通常能更快完成压缩,有时也能找到更多重复片段。压缩比并非任何数据上都必然优于 pglz,因此迁移判断仍应基于真实负载。

可以这样实践:对比 pglz 与 LZ4

下面的实验适用于支持 LZ4 的 Postgres 14 及以上版本。先确认服务器编译时包含 LZ4,然后创建两个仅压缩算法不同的列。示例会删除同名临时表,请勿直接对生产表执行。

SHOW server_version;
SHOW default_toast_compression;

DROP TABLE IF EXISTS compression_lab;
CREATE TABLE compression_lab (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    payload_pglz text COMPRESSION pglz,
    payload_lz4  text COMPRESSION lz4
);

INSERT INTO compression_lab (payload_pglz, payload_lz4)
SELECT payload, payload
FROM (
    SELECT repeat(
        '{"service":"billing","level":"info","message":"payment accepted"}',
        250
    ) AS payload
) AS sample;

SELECT
    octet_length(payload_pglz) AS raw_bytes,
    pg_column_size(payload_pglz) AS pglz_bytes,
    pg_column_size(payload_lz4) AS lz4_bytes,
    pg_column_compression(payload_pglz) AS pglz_method,
    pg_column_compression(payload_lz4) AS lz4_method
FROM compression_lab;

SELECT
    pg_size_pretty(pg_total_relation_size('compression_lab')) AS total_size,
    pg_size_pretty(pg_relation_size('compression_lab')) AS heap_size;

octet_length 给出原始字符数据的字节数,pg_column_size 反映值实际存储时的大小。两者差距明显,说明压缩取得了空间收益;如果大小接近,数据可能不可压缩,较大的值仍可能被原样移入 TOAST。

测试系统级默认值时,可以先限定在当前会话,避免影响其他连接:

SET default_toast_compression = 'lz4';
SHOW default_toast_compression;

CREATE TEMP TABLE session_default_test (
    payload text
);

INSERT INTO session_default_test
VALUES (repeat('compressible payload ', 1000));

SELECT
    pg_column_compression(payload) AS method,
    octet_length(payload) AS raw_bytes,
    pg_column_size(payload) AS stored_bytes
FROM session_default_test;

修改默认算法或列的 COMPRESSION 属性时,应把它理解为后续写入策略,而不是一次自动的全表重写。评估已有数据迁移时,需要明确安排表重写或分批更新,并计算 WAL、复制延迟和锁竞争成本。

索引压缩决定“大键”能否存进去

B-tree 索引中的变长键同样使用 varlena。压缩在这里是机会式的:并非每个索引字符串都会压缩,只有单个未压缩键超过约 510 字节的 TOAST_INDEX_TARGET 时,索引代码才会尝试调用同一套压缩能力。

B-tree 单条索引记录仍受页面约束。对于标准 8 kB 页面,键值最终通常不能超过约三分之一页,也就是报错信息中常见的 2704 字节上限。高度重复的 5000 字符可能压缩到几十字节后成功入索引;相同长度的随机数据则可能压不下来并导致写入失败。

可以在测试库复现这条边界。最后一条语句是预期失败案例:

DROP TABLE IF EXISTS index_key_lab;
CREATE TABLE index_key_lab (
    body text COMPRESSION lz4
);
CREATE INDEX index_key_lab_body_idx ON index_key_lab (body);

-- 可压缩,通常能够写入索引。
INSERT INTO index_key_lab VALUES (repeat('x', 5000));
INSERT INTO index_key_lab VALUES (repeat('ab', 2000));

-- 伪随机 MD5 文本接近不可压缩,预期触发 B-tree 索引行过大错误。
INSERT INTO index_key_lab
SELECT string_agg(md5(g::text), '')
FROM generate_series(1, 88) AS g;

不要把压缩当作绕过 B-tree 大键限制的接口契约。数据分布一变,原本可写入的值就可能失败。长正文、文档或事件载荷通常更适合索引摘要、业务标识、表达式结果,或者采用适合检索目标的全文与倒排索引方案。

升级前应该验证什么

计划采用 LZ4 时,可以按以下清单执行:

  • 用真实 INSERTUPDATE 流量比较吞吐、CPU、WAL 量和复制延迟,而不只测试压缩比。
  • 分别采样重复文本、JSON、二进制文件和加密数据;后两类内容经常难以再次压缩。
  • 使用 pg_column_sizeoctet_lengthpg_column_compression 和关系大小函数核对实际结果。
  • 检查索引中是否直接包含长 TEXTVARCHARBYTEA 键,并加入不可压缩输入测试。
  • 区分“新写入采用 LZ4”和“历史数据已经转换”,不要默认配置修改会重写旧值。
  • 在生产启用前确认 Postgres 构建包含 LZ4 支持,并验证备份、恢复及副本环境一致。

从 pglz 转向 LZ4 的核心价值不是追求一个固定的压缩率数字,而是用更符合现代 CPU 和内存特征的算法降低写入成本。TOAST 的抽象让这次变化不需要引入新的业务字段类型,但统一框架也意味着表、TOAST 和索引必须放在同一套基准测试中评估。


相关推荐