pg_stats:Postgres 内部统计机制详解

概述

本文梳理 PostgreSQL 内部统计的核心原理与pg_stats视图运行机制,内容源自 POSETTE 2026 技术分享。文章系统性讲解pg_stats的定义、生成逻辑,以及其对 Postgres 查询规划器决策的核心影响,帮助读者全面掌握数据库统计数据驱动 SQL 执行计划的底层逻辑。

本文以业务常用的customers数据表作为演示案例:

CREATE TABLE customers ( id bigserial PRIMARY KEY, city text NOT NULL, state text NOT NULL, signup_date date NOT NULL ); -- Insert 1,000,000 rows

以下是日常业务中高频使用的筛选查询语句:

SELECT * FROM customers WHERE state = 'CA';

该表已为statecity字段分别创建独立索引。常规认知中,数据库会优先走state字段的索引扫描,但EXPLAIN ANALYZE的实际执行结果却截然不同:

QUERY PLAN ----------------------------------------------------------------- Seq Scan on customers (cost=0.00..19682.66 rows=173829 width=26) (actual time=0.025..120.574 rows=172001 loops=1) Filter: (state = 'CA'::text) Rows Removed by Filter: 827972 Buffers: shared hit=4601 read=2582 Planning: Buffers: shared hit=139 Planning Time: 0.371 ms Execution Time: 128.136 ms

数据表存在有效索引的前提下,数据库最终选择了顺序扫描。本文将深入解析这一现象背后的统计机制原理。

查询计划由查询规划器生成

Postgres 接收 SQL 查询请求后,会通过内置查询规划器生成执行计划。规划器不会直接读取、解析数据表原始数据,所有执行决策均依托于系统表pg_statistic中存储的数据汇总信息。

pg_statistic存储的汇总数据,包含规划器判定执行计划所需的各类核心维度信息:

  • 数据表中各个字段的唯一值数量
  • 字段的高频取值及其出现频率
  • 字段数值在取值区间内的整体分布规律
  • 数据在磁盘上的物理存储顺序与字段逻辑排序顺序的匹配度

pg_statistic的原始数据为机器优化格式,可读性极差。为方便人工查询与使用,Postgres 封装了 pg_stats 视图,以标准化、易读的形式展示所有统计信息。

ANALYZE:统计数据的生成与更新机制

pg_statistic中的汇总数据无法自动生成,所有统计指标均通过ANALYZE命令生成并刷新。

ANALYZE customers;

ANALYZE会对目标表执行全表扫描或抽样扫描,计算每个字段的多维统计指标,并将结果写入pg_statistic系统表。Postgres 的自动真空清理机制会在后台自动执行ANALYZE,但大批量数据载入、数据迁移等场景下,需手动执行该命令更新实时统计数据。
针对customers表执行统计查询,查看核心字段指标:

SELECT attname, n_distinct, null_frac, correlation FROM pg_stats WHERE tablename = 'customers' ORDER BY attname; attname | n_distinct | null_frac | correlation --------------+------------+-----------+-------------- city | 10106 | 0 | 0.0021338463 id | -1 | 0 | 1 signup_date | 1822 | 0 | 1 state | 50 | 0 | 0.06440461 (4 rows)

核心指标释义如下:

  • n_distinct:字段唯一值的估算数量。state字段恰好有 50 个唯一值,与美国州的数量匹配;city字段存在 10106 个唯一值,符合美国城市数据分布特征。主键id取值为-1,是字段唯一无重复的固定标识。
  • null_frac:字段空值占总行数的比例。示例中所有字段均定义为非空,因此该指标全部为0
  • correlation:取值范围为-1+1,用于衡量数据磁盘物理存储顺序与字段逻辑排序顺序的匹配程度。取值+1代表数据在磁盘上完全有序(自增 ID、增量日期字段均符合该特征);取值趋近于0代表数据存储顺序相对字段值随机;取值趋近于±1时,数据库更倾向于走索引扫描;取值趋近于0时,索引扫描成本更高,更易触发顺序扫描。该指标会作为系数参与规划器的成本计算。

高频值统计(MCV)

ANALYZE会采集字段的高频取值及对应出现频率,两类数据分别存储在 pg_stats 的most_common_valsmost_common_freqs平行数组中。

state字段为例,查询其高频值分布数据:

SELECT unnest(most_common_vals::text::text[]) AS state, unnest(most_common_freqs) AS frequency FROM pg_stats WHERE tablename = 'customers' AND attname = 'state' LIMIT 5; state | frequency -------+------------- CA | 0.17403333 TX | 0.1165 NY | 0.08586667 FL | 0.0666 IL | 0.0474 (5 rows)

统计结果显示,state=CA的数据占比约 17.4%,state=TX占比约 11.6%。高频值的频率数据是规划器估算扫描行数、选择执行扫描方式的核心依据。

成本机制:规划器的计划选择逻辑

查询规划器基于成本计算结果筛选最优执行计划,影响计划选择的三大核心成本参数如下:

  • random_page_cost:随机读取磁盘页的成本,对应索引扫描+回表查询场景
  • seq_page_cost:顺序读取磁盘页的成本,对应全表顺序扫描场景
  • cpu_tuple_cost:处理单行数据的 CPU 计算成本

结合pg_statistic生成的行数估算数据,上述成本参数共同决定规划器的两类核心执行选择:

扫描类型

  • 顺序扫描(Sequential Scan)
  • 索引扫描(Index Scan)
  • 位图堆扫描(Bitmap Heap Scan)

连接类型

  • 嵌套循环连接(Nested Loop)
  • 哈希连接(Hash Join)
  • 合并连接(Merge Join)

规划器会生成多个候选执行计划,计算各计划的总成本,最终选取成本最低的方案执行。失真或过期的统计数据会导致行数估算错误,进而引发执行计划不合理、SQL 性能下降。精准的统计数据是高效查询执行的核心前提。

两类取值的查询计划差异解析

对比查询 state 字段高频值CA与低频值WY的执行计划:

EXPLAIN ANALYZE SELECT * FROM customers WHERE state = 'CA'; -- Seq Scan on customers (cost=0.00..19682.66 rows=174029 ...) -- (actual time=0.042..50.257 rows=172001 loops=1) EXPLAIN ANALYZE SELECT * FROM customers WHERE state = 'WY'; -- Index Scan using customers_state_idx on customers -- (cost=0.42..13116.39 rows=4233 ...) -- (actual time=0.045..21.238 rows=4300 loops=1)

state=CA的数据占全表约 18%,近 18 万行。针对大规模匹配数据,索引扫描需要反复执行索引检索与磁盘页读取,综合成本远高于全表顺序扫描,因此规划器优先选择顺序扫描。

state=WY的匹配数据仅约 4000 行,数据选择性极低,索引扫描的效率优势显著,因此规划器触发索引扫描。

可通过手动关闭顺序扫描功能,验证成本判定逻辑的合理性:

SET enable_seqscan = off; EXPLAIN ANALYZE SELECT * FROM customers WHERE state = 'CA'; -- Index Scan using customers_state_idx on customers -- (cost=0.42..32172.73 rows=170529 width=26) -- (actual time=0.053..75.656 rows=172001 loops=1)

强制走索引扫描后,执行成本从 19682 升至 32172,验证了原顺序扫描计划为最优选择。高频值统计数据精准识别了 CA 的高占比特征,帮助规划器规避低效的索引扫描,充分体现了高频值统计在倾斜数据场景下的核心价值。

直方图:适配非等值范围查询

高频值统计仅适用于等值查询场景。针对时间范围筛选、数值区间对比等范围查询,ANALYZE会生成字段直方图统计数据,辅助规划器完成计划判定。

Postgres 默认生成 100 个等深直方图桶,每个桶承载的数据行数基本一致。查询signup_date字段的直方图边界数据:

SELECT (unnest(histogram_bounds::text::date[]))::date AS bucket_bound FROM pg_stats WHERE tablename = 'customers' AND attname = 'signup_date' LIMIT 8; bucket_bound -------------- 2018-01-01 2018-03-01 2018-04-28 2018-07-04 2018-09-05 2018-10-28 2018-12-27 2019-02-14 (8 rows)

默认直方图桶的粒度覆盖约两个月数据,可满足常规查询估算需求。对于时间、数值分布倾斜的大表,增加直方图桶数量可大幅提升范围查询的行数估算精度。

直方图精度直接影响规划器判定效果:低精度桶仅能判断数据大致分布区间,高精度桶可精准识别数据峰值、谷值及细分分布规律,为精细化范围查询的计划选择提供精准依据。

支持自定义单字段统计精度、增加直方图桶数量:

ALTER TABLE customers ALTER COLUMN signup_date SET STATISTICS 1000; ANALYZE customers;

调整精度并重新采集统计数据后,signup_date 字段的直方图桶粒度缩短至约两天,数据分布识别精度大幅提升:

bucket_bound -------------- 2018-01-01 2018-01-03 2018-01-05 2018-01-07 2018-01-10 2018-01-12 2018-01-14 2018-01-16 (8 rows)

精度与性能需平衡取舍:直方图桶数量越多,估算精度越高,但会增加ANALYZE计算开销与 SQL 规划开销。不建议修改全局默认统计精度,仅针对估算异常的业务字段做定向优化。

字段关联统计

单字段统计可满足绝大多数单条件查询场景,多字段联合筛选场景下,规划器默认的字段独立统计逻辑会产生严重估算偏差。

执行 city 与 state 双字段联合筛选查询:

EXPLAIN ANALYZE SELECT * FROM customers WHERE city = 'Cheyenne' AND state = 'WY'; -- Index Scan on customers (cost=... rows=8 width=...) -- (actual rows=4012)

规划器估算返回 8 行数据,实际查询结果为 4012 行,估算偏差达 500 倍。

问题核心在于,规划器默认所有字段统计相互独立,通过多字段独立概率的乘积计算匹配数据量:

[P(\text{city} = \text{Cheyenne} ;\wedge; \text{state} = \text{WY}) = P(\text{city}) \times P(\text{state})]

实际业务数据中,citystate字段存在强关联关系,city = 'Cheyenne'绝大多数隶属于state = 'WY'。默认的独立统计逻辑无法识别该关联特征,最终导致严重估算偏差。

Postgres 10 及以上版本支持自定义扩展统计,用于识别字段关联关系:

CREATE STATISTICS customers_city_state (dependencies, ndistinct) ON city, state FROM customers; ANALYZE customers;

dependenciesndistinct参数分别用于采集字段间关联关系、唯一值关联信息。重新执行ANALYZE更新统计数据后,查询计划估算精度大幅优化:

EXPLAIN ANALYZE SELECT * FROM customers WHERE city = 'Cheyenne' AND state = 'WY'; -- Index Scan on customers (cost=... rows=4087 width=...) -- (actual rows=4012)

优化后估算行数 4087 与实际行数 4012 基本吻合。精准的底层行数估算,可避免上层关联、聚合逻辑产生连锁错误。底层统计偏差会在多层执行计划中持续放大,最终导致复杂查询整体性能劣化。

注意:扩展统计会轻微增加 ANALYZE 执行开销与查询规划开销,仅建议为确认存在字段关联、且存在估算偏差的字段组合创建扩展统计。

查询性能快速排查清单

当出现异常执行计划、查询效率低下问题时,可按照以下固定流程排查优化:

  1. 对比EXPLAIN ANALYZE执行计划中的估算行数与实际行数,定位偏差最深的执行节点,该节点通常为计划异常的根源。
  2. 查询目标字段的pg_stats统计指标,校验n_distinct唯一值数量、高频值列表是否贴合实际数据特征。
  3. 大批量数据载入、数据迁移、分区切换后,统计数据易过期,需手动执行ANALYZE更新实时统计(自动真空机制无法及时感知瞬时大批量数据变更)。
  4. 单字段查询存在估算误差时,调高字段统计采集精度,重新执行ANALYZE刷新统计数据,提升数据分布识别准确度。
  5. 多字段联合查询因字段关联导致估算异常时,为对应字段组合创建扩展统计,优化跨字段统计精度。
  6. 彻底排查并修复统计异常问题后,再考虑优化 SQL 语句或采用其他调优方案。

总结

Postgres 查询规划器具备优秀的自动优化能力,但所有执行决策均依托系统统计数据,而非真实数据表数据。执行计划的精准度,完全取决于pg_stats统计信息的质量。

pg_stats是洞悉规划器数据判定逻辑的核心窗口,绝大多数异常执行计划、SQL 性能问题,均源于统计数据与实际业务数据的偏差。

遇到不符合预期的EXPLAIN ANALYZE执行结果时,需优先排查优化pg_stats统计问题,而非强行修改计划参数、盲目重写 SQL。

查询规划器的智能程度,完全取决于输入的统计数据质量。

作者:Richard Yen

原文链接:

https://richyen.com/postgres/2026/06/22/pg_stats_how_postgres_internal_stats_work.html