首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >让PostgreSQL规划器"失效"pg_regresql 扩展如何拯救回归测试

让PostgreSQL规划器"失效"pg_regresql 扩展如何拯救回归测试

作者头像
小徐
发布2026-05-25 14:15:25
发布2026-05-25 14:15:25
960
举报
文章被收录于专栏:GreenplumGreenplum

让PostgreSQL规划器"失效"pg_regresql 扩展如何拯救回归测试

当你的测试库只有 1 万行数据,而生产环境有 5000 万行时,EXPLAIN 的输出还能信吗?

01 问题的根源

即使你注入了生产环境的 relpages,规划器也会无视它。

为什么呢?因为规划器会直接调用 smgrnblocks() 读取磁盘上的实际页面数,然后按实际文件大小等比缩放 reltuples。 举个具体例子:你的表在磁盘上是 74 页,但 pg_class.relpages 说它是 123,513 页。规划器会怎么做?它会用 74/123513 这个比例把 reltuples 缩小,选择比(selectivity)虽然对了,但绝对成本估算全偏了。 对于日常调试单个查询,这通常没啥问题。但如果你在做自动化回归测试,要跨运行对比 EXPLAIN 成本——那就麻烦了。成本阈值 2× 在基线数据是按假数字缩放过的前提下,意义完全变了。

02 规划器为什么"不信" relpages?

PostgreSQL 规划器调用 get_relation_info()(在 plancat.c 中)时,会委托给 estimate_rel_size(),最终落到 tableam.ctable_block_relation_estimate_size()

关键代码在这里:

代码语言:javascript
复制
curpages = RelationGetNumberOfBlocks(rel);

实际页面数是从存储管理器读出来的。然后函数用 pg_class 中的数据计算元组密度(reltuples / relpages),再乘以 curpages 来估算元组数。 所以 pg_class.reltuples 并没有被忽略——它被缩放到匹配真实文件大小了。 这个设计在日常运行时是合理的:catalog 可能过期,但文件系统永远是最新的。 索引也一样——规划器会从磁盘读取它们的实际大小。

03 pg_regresql 做了什么?

这个扩展 hook 到了 get_relation_info_hook——一个在 PostgreSQL 读取完物理文件统计信息后执行的规划器回调。

hook 做的事情很简单:用 pg_class 中存储的值替换基于文件的数字

代码语言:javascript
复制
rel->pages    ← pg_class.relpages
rel->tuples   ← pg_class.reltuples
rel->allvisfrac ← pg_class.relallvisible / pg_class.relpages

对 rel->indexlist 中的每个索引也做同样处理。每个索引的页面数和元组数都从索引自己的 pg_class 条目覆盖。

守卫条件很简单:

  • • 如果 relpages == 0(空表或从未分析过)→ 跳过
  • • 如果 reltuples == -1(从未分析过)→ 跳过 只有被 ANALYZE 过或注入过统计信息的表才会触发 hook。

04 安装

从源码编译,使用 PGXS:

代码语言:javascript
复制
cd pg_ext
make
make install

零配置:不需要 GUCs、不需要后台 worker、不需要共享内存。

05 使用方法

在你的会话中加载扩展:

代码语言:javascript
复制
LOAD 'pg_regresql';

就这么多。这个会话中的每一个 EXPLAIN 现在都会使用 catalog 统计信息而不是文件大小。没有函数要调用,没有表要配置。

你也可以按数据库加载,在 postgresql.conf 中添加 session_preload_libraries:

代码语言:javascript
复制
ALTER DATABASE test_db SET session_preload_libraries = 'pg_regresql';

06 效果对比

用之前文章的 test_orders 例子:实际 10,000 行,注入了生产环境的统计信息——声称有 5000 万行、123,513 页。

不加载 pg_regresql 时:

代码语言:javascript
复制
EXPLAIN SELECT * FROM test_orders WHERE created_at > '2024-06-01';
代码语言:javascript
复制
                                             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 后:

代码语言:javascript
复制
LOAD 'pg_regresql';
EXPLAIN SELECT * FROM test_orders WHERE created_at > '2024-06-01';
代码语言:javascript
复制
                                                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 万行。行数估算从 6,340 变成了 10,791,836——这才对。

07 什么场景下需要它?

场景一:基于成本的回归测试

如果你在对比不同 schema 版本之间的 EXPLAIN 成本(RegreSQL 做的就是这个),你需要绝对数字稳定且真实。

有缩放行为的情况下,基线成本跟你的测试库大小成正比,而不是生产环境。一个在生产中成本翻倍的迁移,在 CI 中可能只显示 1.3× 增长——因为缩小后的数字压缩了范围。

场景二:在笔记本上复现生产执行计划

有时候执行计划形状本身就会因为绝对数字而改变。有多个 JOIN 的查询,在规划器看到 5000 万行 vs 3 万行时,可能得到完全不同的 JOIN 顺序。因为 hash join 和 nested loop 之间的成本交叉点取决于绝对行数,不只是比例。

场景三:索引仅扫描(Index-Only Scans)

allvisfrac(全可见页面的比例)对索引仅扫描的成本估算很重要。没有 hook 时,allvisfrac 是用真实的 relallvisible catalog 值除以真实页面数计算的。

如果注入的统计信息中 relallvisible 是 120,000,但实际页面数只有 74,比例会被钳位到 1.0,规划器就会高估索引仅扫描有多便宜。hook 用注入的 relpages 作为分母,修正了这个问题。

08 它不做什么?

列级统计信息和 ANALYZE 行为完全不变。扩展只影响规划器如何读取表和索引大小。

有一点值得注意:EXPLAIN ANALYZE 仍然会显示真实(小规模)数据的实际行数。扩展改变的是规划器的成本估算,不是查询执行本身。

09 完整工作流

把 PostgreSQL 18 的可移植统计信息和 pg_regresql 结合起来,完整工作流长这样:

代码语言:javascript
复制
# 1. 从生产环境导出 schema 和统计信息
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. 加载最小 fixture 数据(可选)
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'"

10 兼容性

扩展支持 PostgreSQL 13 到 18

可移植统计信息函数(pg_restore_relation_stats、pg_restore_attribute_stats)需要 PostgreSQL 18,但 pg_regresql 本身适用于任何写入 pg_class 的方法,包括旧版本的直接 catalog 更新。

PostgreSQL 19 需要注意:pg_regresql 使用的 get_relation_info_hook 已经被替换为 build_simple_rel_hook。新 hook 运行时机稍晚、参数不同,但覆盖逻辑不变。

重要提醒

不要在生产环境使用这个扩展!

扩展让规划器"无视现实"——这正是你用注入统计信息做测试时想要的。但在生产环境,你需要规划器看到实际文件大小,这样才能适应数据增长、膨胀和 vacuum 活动。

pg_regresql 只应该出现在 dev/test 和 CI 数据库中。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 河马coding 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 让PostgreSQL规划器"失效"pg_regresql 扩展如何拯救回归测试
    • 01 问题的根源
    • 02 规划器为什么"不信" relpages?
    • 03 pg_regresql 做了什么?
    • 04 安装
    • 05 使用方法
    • 06 效果对比
      • 不加载 pg_regresql 时:
      • 加载 pg_regresql 后:
    • 07 什么场景下需要它?
      • 场景一:基于成本的回归测试
      • 场景二:在笔记本上复现生产执行计划
      • 场景三:索引仅扫描(Index-Only Scans)
    • 08 它不做什么?
    • 09 完整工作流
    • 10 兼容性
    • 重要提醒
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档