Postgres 19 压缩:从 pglz 到 LZ4
摘要
PostgreSQL 19 计划将默认的 TOAST 压缩算法从 pglz 改为 LZ4,提供更好的性能和效率。文章介绍了 Postgres 压缩的历史以及切换的原因。
<p><a href="https://lobste.rs/s/piabis/postgres_19_compression_from_pglz_lz4">评论</a></p>
查看缓存全文
缓存时间: 2026/07/20 23:30
# Postgres 19 压缩:从 pglz 到 LZ4 | Crunchy Data 博客
来源:https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4
Postgres 19 计划将默认的 TOAST 压缩算法从 pglz 改为 LZ4,因此我们来看看 Postgres 是如何对表存储和索引中的数据进行压缩的。Postgres 使用单一的、统一的压缩框架来处理表(heap)、TOAST 和索引。在 heap 和 TOAST 中,压缩是自动进行的,并且默认对变长类型(如 TEXT、VARCHAR、BYTEA 和 JSONB)启用。在索引中,压缩是机会主义的:只有当某个键超过大小阈值时才会触发压缩,而不是对索引中存储的每个变长值都进行压缩。
Postgres 压缩与 TOAST 决策图*点击展开*
## Postgres 压缩的历史 (https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#history-of-postgres-compression)
压缩最初是在 2000 年发布的 Postgres 7.0 中添加的,但并非以今天的形式存在。当时,Postgres 有严格的 8kB 最大行大小限制,尝试插入超过 8kB 的数据会抛出错误。修复这个行大小限制是 Postgres 核心团队的优先事项。
尝试绕开这个限制的第一个方法是显式压缩字段。Postgres 7.0 引入了一个 `lztext` 数据类型,它使用 pglz 压缩算法。这个实现有取舍:8kB 的行限制仍然存在,用户必须显式选择压缩数据类型。
在 7.0 版本之后,下一个合乎逻辑的步骤可能是添加更多压缩数据类型,比如 `LONG` 或 `BLOB`(就像当时许多闭源数据库所做的那样)。相反,Postgres 核心团队拒绝了额外的数据类型,并团结在 TOAST 周围。pgsql-hackers 邮件列表中的讨论显示,团队将 8kB 问题分解为两个不同的问题:数据类型问题和物理存储问题。压缩和 TOAST 正是物理存储问题的答案。
## 使用 pglz 的 TOAST (https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#toast-with-pglz)
从 Postgres 7.1 开始,TOAST 使用 pglz 实现。
pglz 算法位于 `pg_lzcompress.c` 文件中。由于 Postgres 是开源的,我们可以直接在注释中阅读作者自行开发压缩算法的理由:
- **折衷比例换取速度:** pglz 压缩和解压缩速度很快,并愿意为此牺牲压缩率。源代码将此称为*算法“速度与比率”偏好的另一个特征。*
- **内存使用最小化:** 该算法使用 4096 字节的滑动窗口,足够小,不会影响 Postgres 的内存需求。注释指出,压缩器最适合大小在 1K 到 1M 之间的属性,因此它牺牲了对非常大值的性能。
- **激进的故障安全:** 该算法旨在在数据压缩效果不佳时快速失败,以避免浪费 CPU 周期。
- **无外部依赖:** 该算法是自包含的,不需要任何外部库。那时,Linux 并不是云服务器的默认操作系统(也没有“云”),因此 Postgres 需要一个在它编译的任何地方都能工作的压缩算法。
Jan Wieck 编写了该算法,并在文件底部留下了致谢:
> 非常感谢 Adisak Pochanayon,他关于 SLZ 的文章启发了我以这种方式编写 PostgreSQL 压缩算法。
Adisak Pochanayon 的 SLZ 文章描述了一种为游戏行业构建的压缩方案,用于压缩图形和音频数据。
## 为什么从 pglz 迁移到 LZ4?(https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#why-move-from-pglz-to-lz4)
pglz 是为不同时代构建的,而 LZ4 是一种现代算法,其取舍与现代硬件相匹配。Postgres 的 LZ4 部署遵循了类似原始 pglz 实现的路径:首先作为选项,然后成为标准。自 Postgres 14 以来,LZ4 可以通过系统级设置(`default_toast_compression = 'lz4'`)或列级设置(`column_name text COMPRESSION lz4`)使用。
- **更快的压缩:** LZ4 的压缩速度明显快于 pglz。在一个完全不科学的测试中,插入 2000 行约 10 kB 的数据,LZ4 大约需要 6 毫秒,而 pglz 需要 50 毫秒(快 8 倍)。使用此数据集,两种算法的解压缩吞吐量相似,因为 I/O 和内存延迟主导了 CPU 解码时间。
- **更好的压缩比(有时):** LZ4 的 64 kB 滑动窗口(相比之下 pglz 为 4 kB)可以在数据中找到更多反向引用,从而扩大压缩。在类似的不科学工作负载下,LZ4 每 10400 字节原始数据存储了 111 字节(减少 98.9%),而 pglz 为 186 字节(减少 98.2%),并且总堆空间使用量为 312 kB,而 pglz 为 472 kB。在某些情况下 pglz 可能有更好的压缩比。
- **继续快速失败:** LZ4 具有高效的中止机制,旨在快速检测随机或不可压缩的数据。
## Postgres 存储策略 (https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#postgres-storage-strategies)
首先,你需要知道 Postgres 对列有几种不同的存储策略:
**EXTENDED**(我们大写是因为 Postgres 文档如此,并非我们在喊叫)允许 Postgres 使用所有可用工具。这是变长类型(如 TEXT、VARCHAR、BYTEA 和 JSONB)的默认设置。值可以未压缩存储、压缩存储、放在 heap 中或放在 TOAST 中。
**PLAIN** 将列内联存储在 heap 中,未压缩。这是定宽类型(如 INT、FLOAT 和 BOOL)的默认设置。这些值很少从压缩中受益。
**EXTERNAL** 告诉 Postgres 将列存储在 TOAST 中,但不进行压缩。
**MAIN** 告诉 Postgres 尝试压缩列,但尽可能避免将其移至 TOAST。
## varlena 格式 (https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#the-varlena-format)
对于变长类型,Postgres 使用一种称为 `varlena` 的格式来存储数据。`varlena` 是一种自描述格式,其头部记录了数据的长度以及是否被压缩。它用于所有变长类型(包括 TEXT、VARCHAR、BYTEA 和 JSONB),并且是存储在 heap、TOAST 和索引中的内容。
只有变长类型才会被压缩。定长类型(如 INT、FLOAT 和 BOOL)永远不会被压缩;它们直接存储在 heap 中。
## 压缩决策树 (https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#the-compression-decision-tree)
写入数据(插入或更新)时,Postgres 试图将行大小降低到大约 2 kB(`toast_tuple_target`,默认 2040 字节)以下。对于 EXTENDED 列,它使用以下决策树:
1. 如果整行已经小于约 2 kB,Postgres 直接将未压缩的行写入 heap。
2. 如果行超过阈值,Postgres 按大小对 EXTENDED 列进行排序,并尝试压缩最大的列。如果压缩成功并且使总行大小低于阈值,则停止并写入 heap。如果没有,则移至下一个最大的列。
3. 如果所有符合条件的列都被压缩后行仍然超过阈值,Postgres 开始将最大的 EXTENDED 或 EXTERNAL 列移出行外到 TOAST 表中,在 main heap 元组中替换为 18 字节的指针,直到行适合为止。
4. 如果仍然不适合,Postgres 循环回来尝试内联压缩任何 MAIN 列。如果失败,则采取最后手段:将这些 MAIN 列移出行外到 TOAST。
有关 TOAST 的更多信息,请查看 Postgres TOAST:切片面包以来最伟大的东西?(https://www.crunchydata.com/blog/postgres-toast-the-greatest-thing-since-sliced-bread)
**检查实际的压缩节省**
```sql
-- pg_column_size:存储在 heap 或 TOAST 表中的字节数(压缩后)
-- octet_length:原始字符数据的字节数(未压缩)
SELECT
pg_column_size(payload) AS stored_bytes,
octet_length(payload) AS raw_bytes,
round(
(1 - pg_column_size(payload)::numeric
/ NULLIF(octet_length(payload), 0)) * 100, 1
) AS compression_pct
FROM events WHERE length(payload) > 100 LIMIT 5;
```
`pg_column_size` 返回压缩后数据的大小。当其值远小于 `octet_length` 时,表示值已成功压缩。当两者大致相等时,表示值压缩效果不佳,没有节省空间。在这种情况下,如果值很大(超过约 2 kB 阈值),它仍然会被未压缩地移动到 TOAST 表中。
## 索引中的压缩 (https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#compression-in-indexes)
B-tree 索引页面将键值存储为 `IndexTuple` 条目。每个条目包含一个 `IndexTupleData` 头部(8 字节),后跟键 datum。对于变长类型,datum 使用与 heap 元组相同的 `varlena` 格式(借用了为 8kB 行限制构建的变通方法)。
Postgres 使用与 TOAST 架构绑定的单一压缩框架。它在 varlena 头部中标记数据是否被压缩。如果 heap 已经压缩了一个值,那么它会在索引中保持压缩状态。如果一个值未压缩且超过 510 字节(`TOAST_INDEX_TARGET`,约为 8 kB 缓冲页面的 1/16),索引代码会内联调用相同的 TOAST 压缩例程,试图使其适合。如果压缩后的形式适合,则存储压缩后的形式。如果即使压缩后的形式也超过限制,则写入事务失败。
这就是为什么 `repeat('x', 5000)` 可以被 B-tree 索引:LZ4 将 5000 个重复字符压缩到约 38 字节,远在 2704 字节限制之内。相同长度的随机或伪随机数据会产生接近原始大小的压缩形式,这超过了限制,因此无法被索引。
```sql
-- 这些成功:两者都可压缩,经 LZ4 压缩后都适合
CREATE INDEX ON docs (body);
INSERT INTO docs VALUES (repeat('x', 5000)); -- 存储:约 38 字节压缩
INSERT INTO docs VALUES (repeat('ab', 2000)); -- 存储:约 35 字节压缩
-- 这失败:md5 输出是伪随机的,本质上不可压缩
INSERT INTO docs VALUES (
(SELECT string_agg(md5(g::text), '') FROM generate_series(1, 88) g)
);
-- 2816 个 MD5 字符 → 压缩形式 ≈ 2816 字节 → 索引行大小 2832 > 2704
-- ERROR: index row size 2832 exceeds btree version 4 maximum 2704 for index "..."
-- HINT: Values larger than 1/3 of a buffer page cannot be indexed.
```
## Postgres 压缩的未来 (https://www.crunchydata.com/blog/postgres-19-compression-from-pglz-to-lz4#the-future-of-compression-in-postgres)
通过观察压缩,我们可以学到很多关于 Postgres 如何向前发展的信息。早期专门压缩数据类型的错误步骤得到了承认,底层的 pglz 工作被重新改造为 TOAST,这是一个巨大的成功。在从 pglz 迁移到 LZ4 的过程中,Postgres 采用了类似的方法:先测试,再迁移。核心团队谨慎行事,确保压缩算法的改变是正确的路径。
相似文章
展望 Postgres 19
PostgreSQL 19 beta 引入了关键特性,如 REPACK CONCURRENTLY、分区拆分与合并以及增强的逻辑复制,为生产数据库管理提供了实用改进。
Making Postgres 300x faster for analytics: batching, operator fusion, and SIMD
pgrust 0.2 rebuilds the Postgres query engine with batching, operator fusion, and SIMD, achieving 300x faster analytical queries and 30% faster OLTP than Postgres.
TimescaleDB 如何压缩时间序列数据
本文解释了 TimescaleDB 的 hypercore 引擎如何通过列式存储以及 delta 编码和 Gorilla XOR 等专门算法,为时间序列数据实现高达 98% 的压缩率,并将其与 PostgreSQL 的 TOAST 进行了对比。
期待 PostgreSQL 19:是时候了
PostgreSQL 19 终于将引入原生的时态表支持,遵循 SQL:2011 标准,取代过去使用排除约束的手动方法。本文解释了当前方法的局限性以及新功能备受期待的优点。
期待 PostgreSQL 19:查询提示
PostgreSQL 19 通过新的 contrib 模块 pg_plan_advice 和 pg_stash_advice 引入了查询提示功能,结束了长期以来的社区争论,并为 DBA 提供了应对优化器边缘情况的应急方案。