PostgreSQL码农集散地

PG 18 移植统计信息

本期播客

炸裂!PostgreSQL 18 新特性:把生产环境的查询计划“打包带走”!

查询优化的本质,不是猜,而是让规划器看到它“应该看到”的数据。

你有没有遇到过这样的场景:开发环境一条 SQL 跑得飞快,上线后却慢如蜗牛?你在本地 EXPLAIN 分析,看到的全是顺序扫描,而生产环境却在使用索引。你抓破头皮,最后发现原因是——两个环境的统计数据完全不同。

开发库只有 1000 行测试数据,生产库却有 5000 万行真实数据。规划器在开发环境看到的“大象”,在生产环境只是一只“蚂蚁”。你所有的本地优化,都基于错误的前提。

PostgreSQL 18 彻底改变了这一切。 通过 pg_restore_relation_stats 和 pg_restore_attribute_stats,你可以将生产环境的统计信息导出,并注入到任何数据库——测试环境、CI 管道、甚至同事的笔记本。从此,你在任何地方都能看到与生产环境完全相同的查询计划。

第一性原理:为什么统计信息决定查询计划?

让我们回归查询优化的本质。PostgreSQL 的规划器是一个基于代价的优化器,它依赖两类核心数据:

  1. 表级统计信息(存储在 pg_class):relpages(占用的磁盘页数)和 reltuples(行数)。这告诉规划器表有多大。
  2. 列级统计信息(存储在 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 测试不再是“盲测”,本地调试不再是“猜谜”。当规划器在任何环境都看到相同的统计数据时,它就能做出相同的决策——无论你身在何处。

从今天起,别再让你的开发环境“盲人摸象”了。把生产环境的统计数据打包带走,让查询计划在每一个环境都保持一致。