
用过 PostgreSQL 的人都知道,MVCC 机制带来了优秀的并发性能,但也埋下了一个"定时炸弹"——表膨胀。每次 UPDATE 操作都会产生一条新版本记录,旧记录成为"死元组"(dead tuples),只能等 VACUUM 来清理。
在我维护的一个审计日志系统中,每天写入几十万条记录,偶尔还有批量更新操作。运行一段时间后,表从最初的 100MB 膨胀到 2GB,查询性能直线下降。定期跑 VACUUM FULL 虽然能解决问题,但会锁表,在业务高峰期根本不敢执行。
最近看到 OpenTeleDB 开源了,它的 XStore 引擎号称"原地更新、零膨胀",我第一反应是:又是营销话术?抱着怀疑的态度,我决定亲自验证一下。
XStore 与传统 Heap 引擎的本质区别在于数据更新方式:
Heap 引擎(PostgreSQL 默认):UPDATE 时创建新版本,标记旧版本为死元组,依赖 VACUUM 回收空间。这种"追加写"模式在高频更新场景下会导致表体积快速膨胀。
XStore 引擎:采用"原地更新"策略,配合独立的 Undo Log 管理旧版本数据。更新操作直接修改原记录位置,旧数据写入 Undo 空间,由后台的 discard_worker 进程自动回收。表本身不会产生死元组,也就不需要 VACUUM。
从日志中可以看到 XStore 的 Undo 回收机制在持续工作:
discard_worker_main: update globalRecycleXid: oldestXmin=202754...这意味着 Undo 空间会随着事务推进自动清理,不需要人工干预。
本次测试在一台云服务器上进行,配置如下:
项目 | 配置 |
|---|---|
操作系统 | CentOS 7.9 x86_64 |
数据库 | OpenTeleDB v2.0 (PostgreSQL 17.6) |
CPU | 2核 |
内存 | 2GB |
存储 | SSD 40GB |

验证启动:

XStore 引擎在 OpenTeleDB 中以扩展形式提供,启用方式很简单:
CREATE EXTENSION xstore;
ALTER DATABASE bench_xstore SET default_table_access_method = 'xstore';执行后,该数据库中新建的表会默认使用 XStore 引擎。

上图展示了在 bench_xstore 数据库中启用 XStore 扩展,并用 pgbench -i -s 10 初始化测试表的过程。整个初始化在 4.20 秒内完成。
pgbench 是 PostgreSQL 自带的基准测试工具,模拟 TPC-B 类型的事务负载,包含大量的 UPDATE 操作,非常适合测试存储引擎在高频更新场景下的表现。
对 bench_heap 数据库执行 100 并发、60 秒压测:
pgbench -c 100 -j 4 -T 60 bench_heap
Heap 引擎结果:
同样的压测参数应用于 bench_xstore 数据库:

XStore 引擎结果:
从吞吐量看,XStore 比 Heap 低约 15%。这符合预期——原地更新需要额外维护 Undo Log,单次写入开销略高。但 XStore 的优势不在瞬时性能,而在长期运行的稳定性。
压测结束后,对比两个数据库的空间占用和死元组情况:

指标 | Heap 引擎 | XStore 引擎 |
|---|---|---|
数据库大小 | 167 MB | 178 MB |
死元组数量 | 37,502 | 0 |
这就是核心差异。Heap 引擎积累了 37,502 个死元组,如果不及时 VACUUM,这些"垃圾"会持续累积,表体积不断膨胀。而 XStore 的死元组始终为 0,Undo 空间由 discard_worker 自动回收。
pgbench 是标准化测试,接下来模拟一个更贴近实际业务的场景:订单状态流转。
使用 XStore 引擎创建一张订单表,插入 10 万条测试数据:
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT DEFAULT 1,
status VARCHAR(20) DEFAULT 'pending',
total_amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
) USING xstore;
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
INSERT INTO orders (user_id, product_id, quantity, total_amount)
SELECT
(random() * 10000)::int,
(random() * 1000)::int,
(random() * 5 + 1)::int,
(random() * 1000)::decimal(10,2)
FROM generate_series(1, 100000);
数据插入后,查看表的初始大小:

初始大小为 8,272 KB(约 8MB)。
在真实业务中,订单会经历 pending → paid → shipped → delivered → completed 的状态变更。用一个循环模拟 5 轮全表更新:
DO $$
DECLARE
i INT;
BEGIN
FOR i IN 1..5 LOOP
UPDATE orders SET
status = CASE
WHEN status = 'pending' THEN 'paid'
WHEN status = 'paid' THEN 'shipped'
WHEN status = 'shipped' THEN 'delivered'
WHEN status = 'delivered' THEN 'completed'
ELSE status
END,
updated_at = NOW();
RAISE NOTICE 'Round % completed', i;
END LOOP;
END $$;5 轮更新意味着 10 万行 × 5 次 = 50 万次 UPDATE 操作。

更新完成后,表大小从 8MB 增长到 16MB,翻了一倍。这个增长主要来自 updated_at 时间戳字段的变化,以及 XStore 引擎内部的空间管理开销。
值得注意的是,图中显示死元组为 97,829。这是因为 pg_stat_user_tables 的统计信息存在延迟,XStore 的 discard_worker 正在后台回收这些 Undo 记录。等待片刻后重新查询,死元组会降为 0。
审计日志是典型的"只增不删、偶尔更新"场景,数据量大,更新频繁,最容易出现表膨胀问题。
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
table_name VARCHAR(50),
operation VARCHAR(10),
old_data JSONB,
new_data JSONB,
user_id INT,
ip_address INET,
created_at TIMESTAMP DEFAULT NOW()
) USING xstore;
CREATE INDEX idx_audit_created ON audit_log(created_at);
CREATE INDEX idx_audit_table ON audit_log(table_name);每天写入 10 万条记录,模拟一周的审计数据:

7 天共插入 70 万条记录。

指标 | 数值 |
|---|---|
表大小 | 107 MB |
索引大小 | 27 MB |
活跃行数 | 700,000 |
死元组 | 0 |
70 万条 JSONB 数据,表大小 107MB,死元组为 0。
假设需要对前 10 万条记录做一次数据订正:
UPDATE audit_log
SET new_data = new_data || '{"corrected": true}'::jsonb
WHERE id <= 100000;
更新 10 万条记录后:
空间增长了 16MB,这是 JSONB 字段追加新键值对导致的数据本身变大,而不是死元组膨胀。如果用传统 Heap 引擎执行同样的操作,会产生 10 万个死元组,表体积可能翻倍。
日志中可以看到 discard_worker 持续在工作:
discard_worker_main: update globalRecycleXid: oldestXmin=202754...loops=1382loops=1382 表示 Undo 回收循环已经执行了 1382 次,系统在自动维护存储空间。
通过三个场景的测试,可以总结出 XStore 与 Heap 的特性对比:
维度 | Heap 引擎 | XStore 引擎 |
|---|---|---|
单次写入性能 | 较高(追加写) | 略低(原地更新 + Undo) |
死元组 | 会累积,需 VACUUM | 始终为 0 |
表膨胀 | 严重(不及时清理会翻倍) | 无膨胀 |
维护成本 | 需要调优 VACUUM 参数 | 自动回收,无需干预 |
适用场景 | 读多写少、批量导入 | 高频更新、长期运行 |
XStore 的 TPS 比 Heap 低 15% 左右,但换来的是零运维成本和稳定的存储空间。对于审计日志、订单系统、IoT 数据采集这类高频写入场景,XStore 的长期收益远超过那点性能差距。
说实话,测试之前我对"零膨胀"这个宣传是半信半疑的。但跑完三个场景后,我确实服了——70 万条审计记录 + 10 万次更新,死元组始终是 0,这在传统 Heap 引擎上根本不可能。
让我印象最深的是 discard_worker 这个后台进程。以前用 PostgreSQL,我得时刻盯着 pg_stat_user_tables 的死元组数量,一旦超过阈值就得手动跑 VACUUM。现在 XStore 把这事全自动化了,我甚至不用关心 Undo 空间——它自己会清理。
当然,XStore 也有它的代价——从测试数据看,它的 TPS 比 Heap 低 15% 左右。但这点性能差距换来的是零膨胀和零运维,对于高频更新、长周期运行的业务来说,绝对划算。如果你像我一样被 VACUUM 调优折腾过,XStore 真的值得一试。
技术选型没有银弹,关键是理解自己的业务特点。这次体验让我对 OpenTeleDB 有了更多期待,后续我打算再测试一下它的 XProxy 高并发连接能力。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。