Postgres 19 压缩:从 pglz 到 LZ4

Lobsters Hottest 工具

摘要

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

Hacker News Top

PostgreSQL 19 beta 引入了关键特性,如 REPACK CONCURRENTLY、分区拆分与合并以及增强的逻辑复制,为生产数据库管理提供了实用改进。

TimescaleDB 如何压缩时间序列数据

Hacker News Top

本文解释了 TimescaleDB 的 hypercore 引擎如何通过列式存储以及 delta 编码和 Gorilla XOR 等专门算法,为时间序列数据实现高达 98% 的压缩率,并将其与 PostgreSQL 的 TOAST 进行了对比。

期待 PostgreSQL 19:是时候了

Hacker News Top

PostgreSQL 19 终于将引入原生的时态表支持,遵循 SQL:2011 标准,取代过去使用排除约束的手动方法。本文解释了当前方法的局限性以及新功能备受期待的优点。

期待 PostgreSQL 19:查询提示

Hacker News Top

PostgreSQL 19 通过新的 contrib 模块 pg_plan_advice 和 pg_stash_advice 引入了查询提示功能,结束了长期以来的社区争论,并为 DBA 提供了应对优化器边缘情况的应急方案。