
Burnt TOAST:PostgreSQL 大字段值的性能优化实践
来源标题:Burnt TOAST: performance considerations for large values in PostgreSQL
文章通过拆分 event_log 表,将 JSON 大字段从主表移至独立表 event_log_data,主表体积从约 46MB 降至 5096kB。拆分后主应用表不再需要携带 TOAST 指针,且原混有 TOAST 与非 TOAST 值的列数据均被重新安置。作者称此调整后相关操作性能将显著提升。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
CREATE TABLE event_new
(
event_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
ticker VARCHAR(4) NOT NULL,
strike INT NOT NULL,
shares INT NOT NULL
--- 这里省略了更多列
);
CREATE TABLE event_log_data
(
event_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
event_data JSON NOT NULL
);
INSERT INTO event_new (event_id, ticker, strike, shares)
SELECT event_id, ticker, strike, shares
FROM event_log;
INSERT INTO event_log_data (event_id, event_data)
SELECT event_id, event_data
FROM event_log;
ALTER TABLE event_log RENAME TO event_log_old;
ALTER TABLE event_new RENAME TO event_log;
这样做有什么好处?首先,我们不再需要在主应用表里携带 TOAST 指针。其次,由于 toast_tuple_target 的阈值截断,这一列之前是 TOAST 值和未 TOAST 值的混合,现在连未 TOAST 的值也一并迁移了。从新表的堆大小就能看出来:
SELECT pg_size_pretty(pg_relation_size('event_log')) AS event_log, pg_size_pretty(pg_relation_size('event_log_data')) AS non_toast; event_log | non_toast -----------+----------- 5096 kB | 46 MB 现在所有操作都会大幅加快,因为页面密度得到了保证,会更高。每页能放下更多行,表示表主要核心所需的页数也更少。整个堆现在只有 5MB,而不是 47MB,节省了 9 倍多。在高性能系统中,只要能想办法把大列迁移到一个可选的 JOIN 中,这件事就值得再考虑一下。
如果你以为把 toast_tuple_target 随便设得很低就能达到类似效果,那就再想想。如果行值短于 TOAST_TUPLE_THRESHOLD,TOAST 系统根本不会启动——这是一个编译时的硬性下限。低于这个值的值甚至不会被考虑 TOAST。怎么说呢?Postgres 是很挑食的。
最明显的反对意见是,应用现在需要做一次 join。不用担心,Postgres 查询规划器已经替你处理好了:
CREATE OR REPLACE VIEW event_log_all AS SELECT e.*, d.event_data FROM event_log e LEFT JOIN event_log_data d USING (event_id); EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, SUMMARY OFF) SELECT event_id, strike, shares FROM event_log_all WHERE event_id = 42; Index Scan using event_new_pkey on event_log e (actual time=0.032..0.034 rows=1.00 loops=1) Index Cond: (event_id = 42) Index Searches: 1 Buffers: shared hit=3 关键在于这个 LEFT JOIN,它让 event data 变成可选的,尽管我们知道每一行在那里都有值。不指定该列的查询会自动剪掉这个 JOIN,皆大欢喜。
从 Postgres 14 开始,甚至可以选压缩类型。你既可以继续用久经考验的内置 pglz,也可以在二进制中编译了 lz4 的情况下使用它。整个服务器的默认值来自 default_toast_compression:
SHOW default_toast_compression; default_toast_compression
pglz
也可以在 CREATE TABLE 或 ALTER TABLE 时用 COMPRESSION 装饰器指定。
关于压缩类型,有一点很有意思:pglz 有点古怪。它会跳过小于 32 字节的值,要求至少节省 25%,而且如果在前 1KB 里找不到可压缩的内容就直接放弃。最后这条规则实际跑起来看着挺奇怪的:
CREATE EXTENSION pgcrypto; CREATE TABLE squeeze (method TEXT, payload TEXT); ALTER TABLE squeeze ALTER payload SET COMPRESSION pglz; INSERT INTO squeeze VALUES ('pglz', encode(gen_random_bytes(1024), 'base64') || repeat('Postgres! ', 1000)); ALTER TABLE squeeze ALTER payload SET COMPRESSION lz4; INSERT INTO squeeze VALUES ('lz4', encode(gen_random_bytes(1024), 'base64') || repeat('Postgres! ', 1000)); SELECT method, length(payload) AS chars, pg_column_size(payload) AS stored, pg_column_compression(payload) AS compression, pg_column_toast_chunk_id(payload) AS toasted_chunk FROM squeeze; method | chars | stored | compression | toasted_chunk --------+-------+--------+-------------+--------------- pglz | 11385 | 11385 | | 415076 lz4 | 11385 | 1517 | lz4 | 这也会带来一些相关的性能影响。看下面:
-- 先用 pglz 测试 TRUNCATE TABLE squeeze; ALTER TABLE squeeze ALTER payload SET COMPRESSION pglz; \timing on INSERT INTO squeeze SELECT 'pglz', repeat('Postgres!', 10000) FROM generate_series(1, 100000); Time: 52179.203 ms (00:52.179) SELECT sum(length(payload)) FROM squeeze; sum
9000000000 Time: 17108.284 ms (00:17.108) -- 再用 lz4 测试 TRUNCATE TABLE squeeze; ALTER TABLE squeeze ALTER payload SET COMPRESSION lz4; \timing on INSERT INTO squeeze SELECT 'lz4', repeat('Postgres!', 10000) FROM generate_series(1, 100000); Time: 1774.048 ms (00:01.774) SELECT sum(length(payload)) FROM squeeze; sum
9000000000 (1 row) Time: 20929.714 ms (00:20.930) 处理 10 万行、每行 1 万个压缩值时,在这个例子里 lz4 解压大约慢 20%,但压缩速度却快了 30 多倍。这实在惊人。
你看,我们都想玩甩锅游戏。我一直把应用 event_log 表性能糟糕的责任归咎于 TOAST 系统。每次有人抱怨的时候。每当一个写得很烂的查询穿过应用、以某种显然惹恼了它的方式碰到这张表,导致每次页面加载耗时翻两番甚至完全超时的时候。在我心里,TOAST 就是罪魁祸首。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。