pg_regresql:真正可移植的 PostgreSQL 统计信息
2026-03-21 · 5分钟阅读 · Radim Marek
目录
上一篇文章展示了 PostgreSQL 18 使得优化器统计信息变得可移植,但仍留下了一个缺口:
尝试注入 relpages 是没用的,因为规划器会检查实际文件大小并按比例进行缩放。
规划器不信任 pg_class.relpages。它会调用 smgrnblocks() 从磁盘读取实际的 8KB 页面数。你的表在磁盘上有 74 个页面,但 pg_class.relpages 显示为 123,513?规划器会使用这个比例来缩放 reltuples,使其与实际文件大小匹配。选择性比例保持正确,计划形态大多得以保留,但绝对成本估计值会偏离。
对于调试单个查询,这通常没问题。但对于跨运行比较 EXPLAIN 成本的自动化回归测试,这就出问题了。当基线是从虚假缩放的数字计算出来时,2 倍的成本阈值就有了不同的含义。
作为我在 RegreSQL 上工作的一部分,我很高兴地宣布推出 pg_regresql 扩展,它通过直接钩入规划器来修复这个问题。
为什么规划器忽略 relpages
当 PostgreSQL 规划器在 plancat.c 中调用 get_relation_info() 时,它会委托给 estimate_rel_size(),最终到达 tableam.c 中的 table_block_relation_estimate_size()。在那里,实际的页面数来自存储管理器:
c
curpages = RelationGetNumberOfBlocks(rel);
然后,该函数从 pg_class 计算元组密度(reltuples / relpages),并将其乘以 curpages 来估计元组数。所以 pg_class.reltuples 并没有被忽略,而是被缩放以匹配实际文件大小。这种推理在正常操作中是合理的:目录可能过时,但文件系统始终是最新的。
索引也是如此。规划器也会从磁盘读取它们的实际大小。
pg_regresql 的作用
该扩展钩入 get_relation_info_hook,这是一个在 PostgreSQL 读取物理文件统计信息后运行的规划器回调。该钩子用存储在 pg_class 中的值替换基于文件大小的数字:
rel->pages←pg_class.relpagesrel->tuples←pg_class.reltuplesrel->allvisfrac←pg_class.relallvisible / pg_class.relpages
它对于 rel->indexlist 中的每个索引也执行相同的操作。每个索引的页数和元组数都会从其自身的 pg_class 条目中被覆盖。
保护条件很简单:如果 relpages == 0(表为空或从未分析过)或 reltuples == -1(从未分析过),则跳过覆盖。该钩子仅对已执行 ANALYZE 或注入了统计信息的表激活。
安装
使用 PGXS 从源码构建:
bash
cd pg_ext
make
make install
它不需要 GUC、后台工作进程或共享内存。
使用方法
在你的会话中加载扩展:
sql
LOAD 'pg_regresql';
就这样。此会话中的每个 EXPLAIN 现在都将使用目录统计信息而不是文件大小。无需调用任何函数,也无需配置任何表。
你也可以通过将其添加到 postgresql.conf 中的 session_preload_libraries 或通过 ALTER DATABASE 来按数据库加载:
sql
ALTER DATABASE test_db SET session_preload_libraries = 'pg_regresql';
它带来的改变
使用上一篇文章中相同的 test_orders 示例:10,000 条实际行,注入了声称有 5000 万行分布在 123,513 个页面上的生产统计信息。
不使用 pg_regresql:
sql
EXPLAIN SELECT * FROM test_orders WHERE created_at > '2024-06-01';
QUERY PLAN
----------------------------------------------------------------------------------------------------
Index Scan using test_orders_created_at_idx on test_orders (cost=0.29..153.21 rows=6340 width=26)
Index Cond: (created_at > '2024-06-01'::date)
计划形态是正确的(得益于直方图,使用了索引扫描),但行估计是 6,340。对于一个 5000 万行的表,且过滤条件覆盖了大约 10% 的直方图范围,预期的估计值应该在数百万。规划器看到磁盘上有 74 个实际页面,将 reltuples 缩放至约 30,000,然后应用选择性。比例保留了,但绝对值是错误的。
使用 pg_regresql:
sql
LOAD 'pg_regresql';
EXPLAIN SELECT * FROM test_orders WHERE created_at > '2024-06-01';
QUERY PLAN
-----------------------------------------------------------------------------------------------------------
Index Scan using test_orders_created_at_idx on test_orders (cost=0.29..153212.27 rows=10791836 width=27)
Index Cond: (created_at > '2024-06-01'::date)
(2 rows)
成本数字现在反映了完整的 5000 万行。
这为什么重要
-
基于成本的回归测试 。如果你在比较不同模式版本之间的
EXPLAIN成本(这正是 RegreSQL 所做的),你需要绝对数字稳定且真实。由于存在缩放行为,你的基线成本与你的测试数据库大小成比例,而不是生产环境。生产中使成本翻倍的迁移,在 CI 中可能只显示 1.3 倍的增长,因为缩放后的数字压缩了变化范围。 -
在笔记本电脑上重现生产计划。有时计划形态本身会根据绝对数字而变化。当规划器看到 5000 万行对比 30,000 行时,涉及多个连接的查询可能会得到不同的连接顺序,因为哈希连接和嵌套循环之间的成本交叉点取决于绝对行数,而不仅仅是比例。
-
仅索引扫描 。
allvisfrac(全可见页面的比例)对于仅索引扫描的成本计算很重要。没有这个钩子时,allvisfrac是通过真实的relallvisible目录值除以实际页面数来计算的。对于注入的统计信息,relallvisible可能是 120,000,但实际页面数是 74,所以该分数被钳位到 1.0,规划器会低估仅索引扫描的成本。该钩子通过使用注入的relpages作为分母来修复此问题。
它不能做什么
列级统计信息和 ANALYZE 行为保持不变。该扩展仅影响规划器读取表和索引大小。有一点需要注意:EXPLAIN ANALYZE 仍然会显示来自实际(小规模)数据的实际行数。该扩展改变的是规划器的成本估计,而不是查询执行。
完整工作流程
结合 PostgreSQL 18 的可移植统计信息和 pg_regresql,完整的工作流程如下所示:
bash
# 1. 从生产环境转储模式和统计信息
pg_dump --schema-only -d production_db > schema.sql
pg_dump --statistics-only -d production_db > stats.sql
# 2. 创建测试数据库
createdb test_db
psql -d test_db -f schema.sql
# 3. 加载最小的测试数据(可选)
psql -d test_db -f fixtures.sql
# 4. 注入生产统计信息
psql -d test_db -f stats.sql
# 5. 安装 pg_regresql 并防止统计信息被覆盖
psql -d test_db <<SQL
ALTER DATABASE test_db SET session_preload_libraries = 'pg_regresql';
ALTER TABLE orders SET (autovacuum_enabled = false);
-- 对其他表重复此操作
SQL
# 6. 重新连接并验证(计划现在与生产环境匹配)
psql -d test_db -c "EXPLAIN SELECT * FROM orders WHERE status = 'pending'"
兼容性
该扩展适用于 PostgreSQL 13 到 18。可移植统计信息函数(pg_restore_relation_stats、pg_restore_attribute_stats)需要 PostgreSQL 18,但 pg_regresql 适用于任何写入 pg_class 的方法,包括在旧版本上直接更新目录。
PostgreSQL 19 将需要一个小的更新:pg_regresql 使用的 get_relation_info_hook 已被 build_simple_rel_hook 替换。新的钩子运行时间稍晚,参数也不同,但覆盖逻辑保持不变。
请勿在生产环境中使用
该扩展使规划器忽略现实。这正是你在使用注入统计信息进行测试时想要的。在生产环境中,你希望规划器看到实际的文件大小,以便它能够适应数据增长、膨胀和 vacuum 活动。请将 pg_regresql 留在你的开发/测试和 CI 数据库中。