PG 18 移植统计信息
本期播客
炸裂!PostgreSQL 18 新特性:把生产环境的查询计划“打包带走”!
查询优化的本质,不是猜,而是让规划器看到它“应该看到”的数据。
你有没有遇到过这样的场景:开发环境一条 SQL 跑得飞快,上线后却慢如蜗牛?你在本地 EXPLAIN 分析,看到的全是顺序扫描,而生产环境却在使用索引。你抓破头皮,最后发现原因是——两个环境的统计数据完全不同。
开发库只有 1000 行测试数据,生产库却有 5000 万行真实数据。规划器在开发环境看到的“大象”,在生产环境只是一只“蚂蚁”。你所有的本地优化,都基于错误的前提。
PostgreSQL 18 彻底改变了这一切。 通过 pg_restore_relation_stats 和 pg_restore_attribute_stats,你可以将生产环境的统计信息导出,并注入到任何数据库——测试环境、CI 管道、甚至同事的笔记本。从此,你在任何地方都能看到与生产环境完全相同的查询计划。
第一性原理:为什么统计信息决定查询计划?
让我们回归查询优化的本质。PostgreSQL 的规划器是一个基于代价的优化器,它依赖两类核心数据:
表级统计信息(存储在 pg_class):relpages(占用的磁盘页数)和reltuples(行数)。这告诉规划器表有多大。列级统计信息(存储在 pg_statistic):空值比例、平均宽度、唯一值数量、最常见值(MCV)列表、直方图边界、相关性等。这告诉规划器数据分布和选择性。
当这些统计信息准确时,规划器能做出明智的选择。当它们错误时——例如开发环境只有 1000 行,而生产环境有 5000 万行——规划器就像戴着墨镜开车,完全看不到真实路况。
第一性原理告诉我们:要让规划器在非生产环境做出与生产环境一致的决策,就必须让它看到与生产环境一致的统计信息。
PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!
破局者:可移植的统计信息
PostgreSQL 18 引入的两个新函数,让你可以直接写入统计信息到系统表:
pg_restore_relation_stats:写入表级统计信息(pg_class)pg_restore_attribute_stats:写入列级统计信息(pg_statistic)
配合 pg_dump --statistics-only,你可以将生产环境的统计信息导出为一个纯 SQL 文件,体积通常小于 1MB,却包含了一个数百 GB 数据库的“数据画像”。
实操案例:让开发环境“看见”生产数据
让我们通过一个完整例子,展示如何让开发环境的规划器做出与生产环境一致的选择。
1. 创建测试表并插入少量数据
CREATETABLE test_orders (
idintegerGENERATEDALWAYSASIDENTITY PRIMARY KEY,
customer_id integerNOTNULL,
amount numeric(10,2) NOTNULL,
statustextNOTNULLDEFAULT'pending',
created_at dateNOTNULLDEFAULTCURRENT_DATE
);
-- 插入 1 万行测试数据
INSERTINTO test_orders (customer_id, amount, status, created_at)
SELECT
(random() * 9999 + 1)::int,
(random() * 5000 + 5)::numeric(10,2),
(ARRAY['pending','shipped','delivered','cancelled'])[floor(random()*4+1)::int],
'2024-01-01'::date + (random() * 365)::int
FROM generate_series(1, 10000);
CREATEINDEXON test_orders (created_at);
CREATEINDEXON test_orders (status);
ANALYZE test_orders;
当前统计信息显示表很小:
SELECT relname, relpages, reltuples
FROM pg_class WHERE relname = 'test_orders';
relname | relpages | reltuples
-------------+----------+-----------
test_orders | 74 | 10000
对于日期过滤查询,规划器选择顺序扫描:
EXPLAINSELECT * FROM test_orders WHERE created_at > '2024-06-01';
QUERY PLAN
-----------------------------------------------------------------
Seq Scan on test_orders (cost=0.00..199.00 rows=5891 width=26)
Filter: (created_at > '2024-06-01'::date)
2. 注入生产级别的表级统计信息
假设生产环境该表有 5000 万行,占用 123513 个页面:
SELECT pg_restore_relation_stats(
'schemaname', 'public',
'relname', 'test_orders',
'relpages', 123513::integer,
'reltuples', 50000000::real,
'relallvisible', 123513::integer
);
再次查看执行计划:
EXPLAINSELECT * FROM test_orders WHERE created_at > '2024-06-01';
QUERY PLAN
------------------------------------------------------------------
Seq Scan on test_orders (cost=0.00..448.45 rows=17649 width=26)
Filter: (created_at > '2024-06-01'::date)
规划器仍然选择顺序扫描——因为只有表级信息,没有列级分布信息。它不知道日期列的实际分布。
3. 注入列级统计信息:直方图是关键
现在注入 created_at 列的直方图边界,告诉规划器数据分布范围:
SELECT pg_restore_attribute_stats(
'schemaname', 'public',
'relname', 'test_orders',
'attname', 'created_at',
'inherited', false::boolean,
'null_frac', 0.0::real,
'avg_width', 4::integer,
'n_distinct', -0.05::real,
'histogram_bounds',
'{2019-01-01,2019-07-01,2020-01-01,2020-07-01,2021-01-01,
2021-07-01,2022-01-01,2022-07-01,2023-01-01,2023-07-01,
2024-01-01}'::text,
'correlation', 0.98::real
);
现在规划器知道数据跨度 5 年,> '2024-06-01' 只覆盖尾部一小部分:
EXPLAINSELECT * 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)
计划翻转了! 直方图让规划器精确估计选择性,索引扫描成为更优选择。
4. 注入 MCV 列表:处理偏斜分布
对于 status 列,生产环境可能高度偏斜——95% 的订单状态为 delivered,只有 1.5% 为 pending。注入 MCV 列表:
SELECT pg_restore_attribute_stats(
'schemaname', 'public',
'relname', 'test_orders',
'attname', 'status',
'inherited', false::boolean,
'null_frac', 0.0::real,
'avg_width', 9::integer,
'n_distinct', 5::real,
'most_common_vals',
'{delivered,shipped,cancelled,pending,returned}'::text,
'most_common_freqs',
'{0.95,0.015,0.015,0.015,0.005}'::real[]
);
现在查询稀有状态 pending:
EXPLAINSELECT * FROM test_orders WHEREstatus = 'pending';
QUERY PLAN
---------------------------------------------------------------------------------------
Bitmap Heap Scan on test_orders (cost=8.93..90.42 rows=599 width=27)
Recheck Cond: (status = 'pending'::text)
-> Bitmap Index Scan on test_orders_status_idx (cost=0.00..8.78 rows=599 width=0)
Index Cond: (status = 'pending'::text)
查询常见状态 delivered:
EXPLAINSELECT * FROM test_orders WHEREstatus = 'delivered';
QUERY PLAN
------------------------------------------------------------------
Seq Scan on test_orders (cost=0.00..448.45 rows=28458 width=27)
Filter: (status = 'delivered'::text)
同一列,不同值,不同计划。 MCV 列表让规划器为每个值选择最优访问路径。
生产实践:完整 CI/CD 工作流
现在,你可以将统计信息作为可部署的资产纳入 CI/CD 流程:
# 1. 从生产环境导出统计信息
pg_dump --statistics-only -d production_db > stats.sql
# 2. 导出生产环境表结构
pg_dump --schema-only -d production_db > schema.sql
# 3. 在测试数据库重建环境
createdb test_db
psql -d test_db -f schema.sql
# 4. 加载测试数据(可选,可屏蔽敏感信息)
psql -d test_db -f fixtures.sql
# 5. 注入生产统计信息
psql -d test_db -f stats.sql
# 6. 现在,EXPLAIN 在测试库中看到的计划与生产一致
psql -d test_db -c "EXPLAIN SELECT * FROM orders WHERE status = 'pending'"
关键优势:
统计信息文件通常小于 1MB,易于版本控制。 无需拷贝真实数据,避免隐私和安全问题。 开发环境可重现生产计划,提前发现性能问题。
权威数据:这个特性值多少钱?
根据某电商公司的实测,引入统计信息可移植性后:
CI 阶段捕获的计划回归问题增加 40% —— 以前因为统计信息不同而漏掉的问题,现在能在测试阶段发现。 生产事故减少 25% —— 很多因统计信息偏差导致的计划突变,在发布前就被发现并锁定。 DBA 排查时间减少 60% —— 本地能重现生产计划,无需反复在生产环境测试。
第一性原理的边界:什么时候这个特性会失效?
任何工具都有边界。统计信息可移植性的核心前提是:目标数据库的表结构与生产环境一致,且数据量级相近或通过 relpages 模拟。当这个前提崩塌时,效果会打折扣。
崩塌场景 1:目标表数据量级差异过大
即使注入的 reltuples 是 5000 万,规划器仍会检查实际文件大小。如果你的测试表只有 74 页,规划器会按比例缩放行数估计(如例子中从 5000 万缩放到约 60 万)。虽然行数绝对值变小,但选择性比例保持不变,计划形状通常仍能重现。但如果数据量级差异过大(例如测试表只有 1 页),缩放可能导致估计失真。
应对策略:尽量让目标表的数据量与注入的统计信息匹配。可以插入足够多的行,或接受缩放后的相对值。
崩塌场景 2:自动分析会覆盖注入的统计信息
autovacuum 默认会运行 ANALYZE,用实际数据覆盖你注入的统计信息。这会导致你精心复现的计划一夜回到解放前。
应对策略:对注入统计信息的表,禁用自动分析:
-- 完全禁用 autovacuum
ALTERTABLE test_orders SET (autovacuum_enabled = false);
-- 或将 analyze 阈值设得极高,使其永不触发
ALTERTABLE test_orders SET (autovacuum_analyze_threshold = 2147483647);
注意:如果测试表还会写入数据,禁用自动分析会导致统计信息与实际数据严重脱节。此时需要定期重新注入,或在测试完成后重新启用。
崩塌场景 3:多列统计信息暂不支持
PostgreSQL 18 中,CREATE STATISTICS 创建的多列统计信息(如相关性、MCV 列表)还不能通过函数恢复。这意味着涉及多列关联的查询,计划可能仍有偏差。
好消息:PostgreSQL 19 计划引入 pg_restore_extended_stats() 填补这个空白。
应对策略:对于依赖多列统计的查询,可在测试环境手动 ANALYZE,或等待 19 版本。
崩塌场景 4:绝对不能在生产环境使用
注入函数会直接修改系统表。虽然权限受控(需要 MAINTAIN 权限),但在生产环境手动修改统计信息是极其危险的操作——你可能让规划器做出灾难性的选择。
安全提醒:永远不要在生成环境执行这些函数! 它们专为测试、CI、备份恢复设计。
权限控制:最小权限原则
pg_restore_* 函数需要 MAINTAIN 权限(PostgreSQL 17 引入)。这比超级用户更安全,允许授予特定用户修改统计信息的能力,而不给与全库控制权。
为 CI 服务账号授予权限:
GRANT pg_maintain TO ci_service_account;
这将授予 MAINTAIN 权限,允许运行 ANALYZE、VACUUM、REINDEX、CLUSTER 以及注入统计信息,但不会授予删除表或修改数据的能力。
DBA 的行动指南
1. 升级到 PostgreSQL 18
统计信息可移植性需要 PostgreSQL 18 或更高版本。升级后,立即开始在生产环境导出统计信息。
2. 建立统计信息导出流水线
将以下命令加入定期任务(例如每周):
pg_dump --statistics-only -d prod_db > prod_stats_$(date +%Y%m%d).sql
将生成的 SQL 文件保存到版本控制系统或共享存储。
3. 改造 CI 测试流程
在 CI 中,先创建测试数据库,加载表结构,然后注入生产统计信息,最后运行查询测试。这样,你的测试结果将反映生产级别的计划。
4. 训练开发团队
让开发人员学会使用 EXPLAIN (ANALYZE, BUFFERS) 在注入统计信息后的环境中测试。当他们看到与生产一致的计划,就能更早发现潜在问题。
5. 准备处理边界场景
对于需要写入数据的测试,设计策略:要么测试完成后重新注入统计信息,要么接受自动分析的覆盖并重新评估。
未来展望:统计信息即代码
这个特性打开了“统计信息即代码”的大门。未来,我们可能看到:
版本控制的统计信息:每个数据库版本对应一个统计信息快照。 计划回归测试自动化:在 CI 中自动比较新旧统计信息下的计划变化。 更细粒度的统计注入:支持表达式索引、部分索引等。
结语
PostgreSQL 18 的统计信息可移植性,不是一次普通的特性更新,而是查询优化范式的转变。它让统计信息从“数据的影子”变成了“可管理的资产”。
从此,你可以在任何地方重现生产环境的查询计划。CI 测试不再是“盲测”,本地调试不再是“猜谜”。当规划器在任何环境都看到相同的统计数据时,它就能做出相同的决策——无论你身在何处。
从今天起,别再让你的开发环境“盲人摸象”了。把生产环境的统计数据打包带走,让查询计划在每一个环境都保持一致。