内容提要
文章剖析PostgreSQL的TOAST机制:大文本列被拆分存入影子表,读取需额外索引与重组,拖慢查询。实验对比发现,TOAST表虽小、多数操作更快,但访问大列代价高。最终方案是将大列拆到独立表,使主表堆从47MB降至5MB,性能提升约9倍,并建议用lz4压缩替代pglz。
延伸解读
TOAST 的代价:读取大列时的额外开销
文章通过实验表明,TOAST 并非总是性能负担。对于不涉及大列的查询(如 count、offset 或更新非 TOAST 列),TOAST 表因堆更小反而更快。但一旦访问被 TOAST 的列,就需要读取 TOAST 索引和表、重组数据块并解压,导致缓冲区读取次数激增。例如,提取 JSON 字段时,TOAST 表需要 288127 次缓冲区读取,而非 TOAST 表仅需 40754 次。因此,问题不在于 TOAST 本身,而在于频繁访问大列。
拆分大列:显著提升主表性能的实用方案
作者将大列拆分到独立表后,主表堆从 47MB 降至 5MB,性能提升约 9 倍。拆分后,主表页面密度更高,所有不涉及大列的操作都更快。对于需要大列的查询,可以通过 LEFT JOIN 视图按需关联,Postgres 规划器能自动剪枝未使用的 JOIN。但文章也提醒,并非所有大列都适合拆分,需权衡额外 JOIN 的复杂性和模式整洁性。
压缩算法选择:lz4 与 pglz 的权衡
文章对比了 pglz 和 lz4 两种 TOAST 压缩算法。在插入 10 万行 10KB 压缩值的测试中,lz4 压缩速度比 pglz 快 30 倍以上(1.7 秒 vs 52 秒),但解压速度慢约 20%。pglz 还有特殊规则:跳过小于 32 字节的值、要求至少 25% 的压缩节省、若前 1KB 无可压缩内容则放弃。因此,写入密集的场景更适合 lz4,而读取密集且压缩率要求高的场景可考虑 pglz。
Q&A
PostgreSQL的TOAST机制是什么?它如何存储大字段?
TOAST(The Oversized-Attribute Storage Technique)是PostgreSQL用于存储超大字段的机制。当一行数据超过8KB页面限制时,PostgreSQL会为变长字段创建一张影子表(TOAST表),将大内容拆分成约1996字节的块存储,并在原行中保留一个指针。这样主表堆可以保持较小,但读取时需要额外访问TOAST表及其索引,并重组数据。
为什么含有大文本列的表查询会变慢?
因为大文本列被TOAST化后,读取该列需要访问TOAST索引和TOAST表,每个块都要单独读取,然后重组和解压缩,带来额外的I/O、内存和CPU开销。如果查询使用SELECT *,即使不需要大列也会触发这些操作,导致性能下降。
TOAST表一定比非TOAST表性能差吗?
不一定。实验表明,对于不涉及大列的查询(如count(*)、按偏移量取行、更新非大列),TOAST表因为堆更小、页面密度更高,反而更快。例如count(event_data)在TOAST表上耗时26ms,而非TOAST表需91ms。但访问大列本身时,TOAST表因需额外读取TOAST表而更慢。
如何优化含有大文本列的表结构以提升性能?
可以将大列拆分到独立的表中,通过主键关联。这样主表堆不再包含TOAST指针,页面密度提高,堆大小显著减小(例如从47MB降至5MB,提升约9倍)。对于不需要大列的查询,可以使用LEFT JOIN视图,规划器会自动剪枝,避免额外开销。
PostgreSQL中lz4压缩和pglz压缩有什么区别?哪个更好?
从PostgreSQL 14开始支持lz4压缩。pglz是内置默认压缩,但压缩率较低且速度慢;lz4压缩速度极快(测试中插入快30倍以上),但解压稍慢(约20%)。对于大文本字段,推荐使用lz4以大幅提升写入性能。可通过default_toast_compression或列级COMPRESSION选项设置。
TOAST的存储策略有哪些?EXTENDED和EXTERNAL有什么区别?
PostgreSQL有四种存储策略:PLAIN(不压缩不TOAST)、MAIN(允许压缩,尽量不TOAST)、EXTERNAL(允许TOAST但不压缩)、EXTENDED(允许压缩和TOAST,是TEXT、JSONB等类型的默认)。EXTERNAL适合中等大小但不想压缩的列,可减小堆大小;EXTENDED则优先压缩,压缩后仍超限才TOAST。