PostgreSQL/openGauss pg_stats 视图从入门到精通:统计信息、执行计划与慢 SQL 优化实战
- [PostgreSQL/openGauss pg_stats 视图从入门到精通:统计信息、执行计划与慢 SQL 优化实战](#PostgreSQL/openGauss pg_stats 视图从入门到精通:统计信息、执行计划与慢 SQL 优化实战)
- [八、pg_stats 核心字段详解](#八、pg_stats 核心字段详解)
-
- [1. schemaname](#1. schemaname)
- [2. tablename](#2. tablename)
- [3. attname](#3. attname)
- [4. null_frac:NULL 值比例](#4. null_frac:NULL 值比例)
-
- [优化器如何使用 null_frac?](#优化器如何使用 null_frac?)
- [5. n_distinct:不同值数量估算](#5. n_distinct:不同值数量估算)
-
- [情况一:n_distinct 为正数](#情况一:n_distinct 为正数)
- [情况二:n_distinct 为负数](#情况二:n_distinct 为负数)
- [6. most_common_vals:最常见的值](#6. most_common_vals:最常见的值)
- [7. most_common_freqs:最常见值的频率](#7. most_common_freqs:最常见值的频率)
- [8. histogram_bounds:直方图边界](#8. histogram_bounds:直方图边界)
-
- [为什么 salary 的直方图很有意义?](#为什么 salary 的直方图很有意义?)
- [9. correlation:相关性](#9. correlation:相关性)
- 九、内容逐列分析
-
- [1. age 字段分析](#1. age 字段分析)
- [2. gender 字段分析](#2. gender 字段分析)
- [3. id 字段分析](#3. id 字段分析)
- [4. created_at 字段分析](#4. created_at 字段分析)
- [5. salary 字段分析](#5. salary 字段分析)
- [6. city 和 status 字段分析](#6. city 和 status 字段分析)
-
- 原因一:客户端字符集显示问题
- [原因二:插入时中文已经被转换成了 ??](#原因二:插入时中文已经被转换成了 ??)
- [原因三:之前做过 UPDATE 测试](#原因三:之前做过 UPDATE 测试)
- [十、通过 EXPLAIN 理解 pg_stats 如何影响执行计划](#十、通过 EXPLAIN 理解 pg_stats 如何影响执行计划)
-
- [1. 创建索引](#1. 创建索引)
- [2. 高频值查询](#2. 高频值查询)
- [3. 相对低频值查询](#3. 相对低频值查询)
- [4. 高选择性查询](#4. 高选择性查询)
- [5. 范围查询](#5. 范围查询)
- [十一、EXPLAIN 和 EXPLAIN ANALYZE 的区别](#十一、EXPLAIN 和 EXPLAIN ANALYZE 的区别)
-
- [1. EXPLAIN](#1. EXPLAIN)
- [2. EXPLAIN ANALYZE](#2. EXPLAIN ANALYZE)
- 十二、复现统计信息过期导致执行计划错误
-
- 第一步:查看原始统计信息
- 第二步:查看执行计划
- [第三步:大量修改数据,但不执行 ANALYZE](#第三步:大量修改数据,但不执行 ANALYZE)
- 第四步:再次查看执行计划
- 第五步:重新收集统计信息
- 第六步:查看新的统计信息
- 第七步:再次查看执行计划
- 十三、如何判断统计信息是否准确?
-
- [1. 看 pg_stats 的估算](#1. 看 pg_stats 的估算)
- [2. 看真实数据分布](#2. 看真实数据分布)
- [3. 对比估算与真实值](#3. 对比估算与真实值)
- 十四、如何提高统计信息精度?
- [十五、pg_stats 排查慢 SQL 的实用流程](#十五、pg_stats 排查慢 SQL 的实用流程)
- [十六、pg_stats 常见误区](#十六、pg_stats 常见误区)
-
- 误区一:有索引就一定走索引
- [误区二:pg_stats 是实时的](#误区二:pg_stats 是实时的)
- [误区三:n_distinct 永远表示具体数量](#误区三:n_distinct 永远表示具体数量)
- [误区四:histogram_bounds 没什么用](#误区四:histogram_bounds 没什么用)
- [误区五:correlation 可以忽略](#误区五:correlation 可以忽略)
- 十七、生产环境建议
-
- [1. 大批量导入后执行 ANALYZE](#1. 大批量导入后执行 ANALYZE)
- [2. 大批量 UPDATE/DELETE 后执行 ANALYZE](#2. 大批量 UPDATE/DELETE 后执行 ANALYZE)
- [3. 慢 SQL 优先看估算行数](#3. 慢 SQL 优先看估算行数)
- [4. 数据倾斜字段提高统计目标](#4. 数据倾斜字段提高统计目标)
- [5. 定期检查核心表统计信息](#5. 定期检查核心表统计信息)
- [十八、完整实验 SQL 汇总](#十八、完整实验 SQL 汇总)
- 十九、总结
PostgreSQL/openGauss pg_stats 视图从入门到精通:统计信息、执行计划与慢 SQL 优化实战
一、前言
在 PostgreSQL 或 openGauss 中,很多人学习 SQL 优化时,第一反应是:
text
SQL 慢了,是不是没建索引?
但实际上,数据库优化器是否选择索引,并不只看有没有索引,还要看它认为这条 SQL 会返回多少数据。
而优化器判断数据量的核心依据之一,就是统计信息。
pg_stats 视图就是我们观察统计信息最常用的入口。
本文从零基础开始,通过一张测试表,完整讲解:
pg_stats是什么- 为什么优化器需要统计信息
null_frac、n_distinct、most_common_vals、histogram_bounds、correlation分别是什么意思- 如何通过 SQL 复现统计信息
- 如何结合
EXPLAIN理解执行计划 - 为什么统计信息过期会导致慢 SQL
- 生产环境中应该如何使用
pg_stats排查问题
二、pg_stats 是什么?
pg_stats 是数据库中的一个系统视图。
它保存的是表中每一列的统计信息。
简单理解:
text
pg_stats = 优化器眼中的数据画像
优化器在执行 SQL 前,并不会真正扫描整张表去计算:
text
这个条件会返回多少行?
这个字段有多少种不同的值?
某个值是不是特别常见?
范围查询会命中多少数据?
如果每次执行 SQL 前都扫一遍全表,那数据库性能会非常差。
所以数据库会通过 ANALYZE 采样表数据,生成统计信息,然后保存在系统统计信息中。我们可以通过 pg_stats 来查看这些统计结果。
三、为什么 pg_stats 很重要?
假设有一张用户表:
sql
SELECT *
FROM users
WHERE gender = 'M';
如果 gender='M' 占全表 90%,那么即使 gender 上有索引,数据库也可能不走索引。
因为通过索引找出 90% 的数据,再回表读取,成本可能比直接全表扫描还高。
但是如果:
sql
SELECT *
FROM users
WHERE gender = 'F';
而 gender='F' 只占 1%,那么索引就很有价值。
问题来了:
数据库怎么知道 M 占 90%,F 占 1%?
答案就是:
sql
pg_stats
四、创建测试表
下面我们创建一张测试表,用来学习 pg_stats。
sql
DROP TABLE IF EXISTS t_pg_stats_demo;
CREATE TABLE t_pg_stats_demo (
id serial PRIMARY KEY,
city text,
gender text,
age int,
salary numeric(10,2),
status text,
created_at date
);
字段说明:
| 字段 | 含义 |
|---|---|
| id | 主键,自增 |
| city | 城市 |
| gender | 性别 |
| age | 年龄 |
| salary | 薪资 |
| status | 状态 |
| created_at | 创建日期 |
五、插入测试数据
sql
INSERT INTO t_pg_stats_demo(city, gender, age, salary, status, created_at)
SELECT
CASE
WHEN i <= 5000 THEN '北京'
WHEN i <= 8000 THEN '上海'
WHEN i <= 9500 THEN '广州'
ELSE '深圳'
END AS city,
CASE
WHEN i % 10 < 7 THEN 'M'
ELSE 'F'
END AS gender,
CASE
WHEN i <= 7000 THEN 25 + (i % 5)
ELSE 40 + (i % 20)
END AS age,
CASE
WHEN i <= 9000 THEN 8000 + (i % 1000)
ELSE 30000 + (i % 5000)
END AS salary,
CASE
WHEN i <= 8500 THEN '正常'
WHEN i <= 9500 THEN '冻结'
ELSE '注销'
END AS status,
current_date - (i % 365)
FROM generate_series(1, 10000) AS s(i);
这批数据的设计是有目的的:
| 字段 | 数据特点 |
|---|---|
| city | 城市分布不均匀 |
| gender | M 占 70%,F 占 30% |
| age | 年轻人集中在 25-29 岁 |
| salary | 大部分是 8000-8999,少量是 30000 以上 |
| created_at | 分布在最近 365 天 |
| id | 每行唯一 |
六、收集统计信息
插入数据之后,需要执行:
sql
ANALYZE t_pg_stats_demo;
ANALYZE 的作用是:
text
采样表数据,生成统计信息,供优化器使用
如果不执行 ANALYZE,pg_stats 中可能没有最新统计信息,优化器也可能无法准确估算 SQL 返回行数。
七、查看 pg_stats
执行:
sql
SELECT
schemaname,
tablename,
attname,
null_frac,
n_distinct,
most_common_vals,
most_common_freqs,
histogram_bounds,
correlation
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
ORDER BY attname;
常见输出字段如下:
| 字段 | 含义 |
|---|---|
| schemaname | schema 名称 |
| tablename | 表名 |
| attname | 字段名 |
| null_frac | NULL 值比例 |
| n_distinct | 不同值数量估算 |
| most_common_vals | 高频值 |
| most_common_freqs | 高频值出现比例 |
| histogram_bounds | 直方图边界 |
| correlation | 字段值与物理存储顺序的相关性 |
八、pg_stats 核心字段详解
1. schemaname
表示表所在的 schema。
例如:
text
schemaname | gilmp
说明测试表位于 gilmp 这个 schema 下。
如果你查询不到结果,可能是 schema 不对。
可以这样查:
sql
SELECT schemaname, tablename, attname
FROM pg_stats
WHERE tablename = 't_pg_stats_demo';
2. tablename
表示表名。
例如:
text
tablename | t_pg_stats_demo
注意,pg_stats 里通常只显示当前用户有权限访问的表统计信息。
3. attname
表示字段名。
例如有:
text
age
city
created_at
gender
id
salary
status
每一行统计信息对应表中的一个字段。
4. null_frac:NULL 值比例
null_frac 表示某一列中 NULL 值所占比例。
例如:
text
null_frac = 0
表示该字段没有 NULL。
如果某列统计信息为:
text
null_frac = 0.3
表示该字段大约 30% 的值是 NULL。
测试 NULL 场景:
sql
UPDATE t_pg_stats_demo
SET city = NULL
WHERE id <= 1000;
ANALYZE t_pg_stats_demo;
SELECT attname, null_frac
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
AND attname = 'city';
你会看到 city 的 null_frac 接近:
text
0.1
因为 10000 行中有 1000 行被更新成了 NULL。
优化器如何使用 null_frac?
例如:
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE city IS NULL;
优化器会根据 null_frac 估算:
text
city IS NULL 大概会返回多少行
如果 NULL 很少,索引可能有价值。
如果 NULL 很多,全表扫描可能更合适。
5. n_distinct:不同值数量估算
n_distinct 是 pg_stats 中非常重要的字段。
它表示某列中不同值数量的估算。
情况一:n_distinct 为正数
示例:
text
age
n_distinct = 25
表示优化器认为:
text
age 这一列大约有 25 个不同值
再比如:
text
gender
n_distinct = 2
说明:
text
gender 这一列大约有 2 个不同值
通常就是:
text
M 和 F
情况二:n_distinct 为负数
id 字段是:
text
id
n_distinct = -1
这是 PostgreSQL/openGauss 中很经典的表示方式。
当 n_distinct 是负数时,含义不是具体数量,而是比例。
计算方式:
text
不同值数量 ≈ 表总行数 × abs(n_distinct)
例如表有 10000 行:
text
n_distinct = -1
则表示:
text
不同值数量 ≈ 10000 × 1 = 10000
也就是说:
text
id 基本每一行都不同
这正符合主键字段的特点。
再例如:
text
n_distinct = -0.2
如果表有 10000 行,表示:
text
不同值数量 ≈ 10000 × 0.2 = 2000
实例 salary :
text
salary
n_distinct = -0.2
表示薪资列的不同值数量大约占总行数的 20%。
6. most_common_vals:最常见的值
most_common_vals 表示该列中最常见的值,也叫 MCV。
MCV 是:
text
Most Common Values
例如 gender 的统计信息类似:
text
most_common_vals | {M,F}
most_common_freqs | {.7,.3}
表示:
| 值 | 频率 |
|---|---|
| M | 70% |
| F | 30% |
这说明:
sql
WHERE gender = 'M'
大概会返回 70% 的数据。
而:
sql
WHERE gender = 'F'
大概会返回 30% 的数据。
7. most_common_freqs:最常见值的频率
most_common_freqs 和 most_common_vals 是一一对应的。
例如:
text
most_common_vals = {M,F}
most_common_freqs = {.7,.3}
对应关系是:
text
M -> 0.7
F -> 0.3
也就是:
text
M 占 70%
F 占 30%
这个字段对等值查询非常重要。
例如:
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'M';
优化器看到 M 是高频值,就会估算返回行数较多。
如果查询:
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
优化器会估算返回行数较少。
8. histogram_bounds:直方图边界
histogram_bounds 用来描述数据分布。
它主要用于范围查询。
例如:
sql
WHERE salary BETWEEN 8000 AND 9000
或者:
sql
WHERE created_at >= '2026-01-01'
优化器会参考 histogram_bounds 判断范围条件大概命中多少数据。
例如从 2025-05-29 到 2026-05-28,说明数据库为日期字段建立了直方图边界。
salary 字段也有直方图边界,例如:
text
8100.00
8110.00
8120.00
...
8999.00
34089.00
...
34999.00
这说明薪资数据不是均匀分布的。
大部分薪资集中在 8000-9000 左右,后面有少量高薪数据在 30000 以上。
为什么 salary 的直方图很有意义?
因为这两个查询命中比例完全不同:
sql
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 8000 AND 9000;
和:
sql
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
第一个范围可能命中大量数据。
第二个范围只命中少量数据。
如果没有直方图,优化器很难准确判断范围查询的选择率。
9. correlation:相关性
correlation 是很多初学者最容易忽略,但非常重要的字段。
它表示:
text
字段值顺序和表中物理存储顺序的相关性
取值范围大致是:
text
-1 到 1
含义如下:
| correlation | 含义 |
|---|---|
| 接近 1 | 字段值递增顺序和物理存储顺序高度一致 |
| 接近 0 | 字段值和物理存储顺序基本无关 |
| 接近 -1 | 字段值递减顺序和物理存储顺序高度一致 |
例如:
text
id
correlation = 1
这非常合理。
因为 id 是自增主键,插入顺序和物理存储顺序基本一致。
所以:
sql
WHERE id BETWEEN 1000 AND 2000
通常读取的数据块比较连续。
示例:
text
created_at
correlation = -0.00705876
接近 0。
说明:
text
created_at 的值和表的物理存储顺序几乎没有关系
如果对 created_at 做范围查询,即使有索引,也可能产生大量随机 IO。
九、内容逐列分析
1. age 字段分析
age 字段:
text
null_frac = 0
n_distinct = 25
most_common_vals = {25,26,27,28,29,40,...,59}
most_common_freqs = {.14,.14,.14,.14,.14,.015,...}
correlation = .700078
说明:
age没有 NULL。age共有大约 25 个不同值。- 25 到 29 岁是高频年龄,每个大约占 14%。
- 40 到 59 岁每个占比约 1.5%。
correlation=0.700078,说明年龄和物理存储顺序有一定正相关。
这和我们造数逻辑一致:
sql
CASE
WHEN i <= 7000 THEN 25 + (i % 5)
ELSE 40 + (i % 20)
END
前 7000 行集中在 25-29 岁,所以这几个年龄是高频值。
验证真实分布
sql
SELECT age, count(*) AS cnt
FROM t_pg_stats_demo
GROUP BY age
ORDER BY cnt DESC, age;
你应该能看到:
text
25-29 岁数量明显更多
40-59 岁数量相对较少
2. gender 字段分析
gender 字段:
text
n_distinct = 2
most_common_vals = {M,F}
most_common_freqs = {.7,.3}
correlation = .57955
说明:
text
gender 只有两个值:M 和 F
并且:
| gender | 占比 |
|---|---|
| M | 70% |
| F | 30% |
这和造数逻辑一致:
sql
CASE
WHEN i % 10 < 7 THEN 'M'
ELSE 'F'
END
因为 0-6 是 7 个数,占 70%。
验证真实分布
sql
SELECT gender, count(*) AS cnt,
round(count(*) * 100.0 / sum(count(*)) over (), 2) AS pct
FROM t_pg_stats_demo
GROUP BY gender
ORDER BY cnt DESC;
3. id 字段分析
id 字段:
text
null_frac = 0
n_distinct = -1
histogram_bounds = {1,100,200,...,10000}
correlation = 1
说明:
id没有 NULL。n_distinct=-1表示基本每行唯一。histogram_bounds从 1 到 10000,说明 id 分布均匀。correlation=1表示 id 顺序和物理存储顺序完全一致。
这是主键字段最理想的统计状态。
适合的查询
sql
SELECT *
FROM t_pg_stats_demo
WHERE id = 100;
或者:
sql
SELECT *
FROM t_pg_stats_demo
WHERE id BETWEEN 100 AND 200;
这类查询通常适合使用主键索引。
4. created_at 字段分析
created_at 字段:
text
n_distinct = 365
histogram_bounds = {"2025-05-29", ..., "2026-05-28"}
correlation = -0.00705876
说明:
created_at大约有 365 个不同值。- 日期分布覆盖一年左右。
correlation接近 0,说明日期和物理存储顺序基本无关。
这和造数逻辑有关:
sql
current_date - (i % 365)
因为 i % 365 循环变化,所以日期不是随着 id 单调递增或递减,而是周期性分布。
适合测试范围查询
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE created_at BETWEEN current_date - 30 AND current_date;
然后创建索引:
sql
CREATE INDEX idx_demo_created_at
ON t_pg_stats_demo(created_at);
ANALYZE t_pg_stats_demo;
再次查看执行计划:
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE created_at BETWEEN current_date - 30 AND current_date;
如果返回数据比例较高,优化器可能仍然选择全表扫描。
这不是索引无效,而是优化器认为:
text
走索引不划算
5. salary 字段分析
salary 字段:
text
n_distinct = -0.2
most_common_vals = {8000.00,8001.00,...}
most_common_freqs = {.0009,.0009,...}
histogram_bounds = {8100.00,8110.00,...,8999.00,34089.00,...,34999.00}
correlation = .352228
说明:
salary的不同值比较多。n_distinct=-0.2表示不同值数量约占总行数 20%。- 大部分常见薪资集中在 8000 附近。
- 直方图显示存在一段明显的高薪区间。
correlation=0.352228,说明薪资和物理存储顺序有一定相关性,但不强。
这类字段很适合观察:
sql
WHERE salary BETWEEN ...
对比两个范围查询
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 8000 AND 9000;
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
第一个范围会命中大量普通薪资。
第二个范围会命中较少高薪数据。
优化器会参考 histogram_bounds 来估算返回行数。
6. city 和 status 字段分析
city 字段显示:
text
n_distinct = 1
most_common_vals = {??}
most_common_freqs = {1}
correlation = 1
status 字段也类似:
text
n_distinct = 1
most_common_vals = {??}
most_common_freqs = {1}
这说明当前统计信息中,优化器认为:
text
city 只有一个不同值
status 也只有一个不同值
这和我们最初设计的数据分布不一致。
因为理论上,city 应该有:
text
北京、上海、广州、深圳
status 应该有:
text
正常、冻结、注销
如果你的实际输出是 {??},常见原因有两个:
原因一:客户端字符集显示问题
中文在 gsql、psql 或终端中显示成了 ??。
可以检查客户端编码:
sql
SHOW client_encoding;
PostgreSQL 中可以设置:
sql
SET client_encoding = 'UTF8';
如果是 openGauss,也需要确认数据库字符集、客户端字符集、终端字符集是否一致。
原因二:插入时中文已经被转换成了 ??
如果插入数据时字符集不正确,可能数据库里真实存储的就是 ??。
验证方法:
sql
SELECT city, count(*)
FROM t_pg_stats_demo
GROUP BY city
ORDER BY count(*) DESC;
如果结果只有:
text
?? | 10000
说明不是显示问题,而是数据本身已经变成了 ??。
原因三:之前做过 UPDATE 测试
例如执行过:
sql
UPDATE t_pg_stats_demo
SET city = '深圳';
然后又执行了:
sql
ANALYZE t_pg_stats_demo;
那么 pg_stats 中就会显示:
text
n_distinct = 1
most_common_freqs = {1}
这是正常的,因为所有行确实都变成了同一个城市。
十、通过 EXPLAIN 理解 pg_stats 如何影响执行计划
1. 创建索引
sql
CREATE INDEX idx_demo_gender ON t_pg_stats_demo(gender);
CREATE INDEX idx_demo_age ON t_pg_stats_demo(age);
CREATE INDEX idx_demo_salary ON t_pg_stats_demo(salary);
CREATE INDEX idx_demo_created_at ON t_pg_stats_demo(created_at);
ANALYZE t_pg_stats_demo;
2. 高频值查询
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'M';
因为 M 占 70%,优化器可能认为:
text
返回数据太多
所以即使有索引,也可能选择:
text
Seq Scan
也就是全表扫描。
3. 相对低频值查询
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
因为 F 占 30%,相比 M 少一些。
如果表足够大,优化器可能更倾向使用索引。
不过在 10000 行的小表中,仍然可能选择全表扫描。
这是因为:
text
小表全表扫描成本很低
所以测试索引效果时,最好把数据量放大到几十万或几百万行。
4. 高选择性查询
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE id = 5000;
由于 id 是唯一值,优化器知道这个条件大概率只返回 1 行。
所以通常会使用主键索引:
text
Index Scan
5. 范围查询
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 8000 AND 9000;
这个范围命中大量数据。
再执行:
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
这个范围命中少量数据。
你可以观察两个执行计划中的:
text
rows=...
这个 rows 就是优化器估算的返回行数。
它不是实际返回行数,而是根据统计信息估算出来的。
十一、EXPLAIN 和 EXPLAIN ANALYZE 的区别
1. EXPLAIN
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
只显示计划,不真正执行 SQL。
适合查看优化器的预估。
2. EXPLAIN ANALYZE
sql
EXPLAIN ANALYZE
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
会真正执行 SQL,并显示实际执行情况。
重点观察:
text
rows=估算行数
actual rows=实际行数
如果两者差距很大,说明统计信息可能不准。
例如:
text
rows=10
actual rows=5000
这就是典型问题。
优化器以为只返回 10 行,结果实际返回 5000 行,执行计划就可能选错。
十二、复现统计信息过期导致执行计划错误
这是生产环境中非常常见的问题。
第一步:查看原始统计信息
sql
ANALYZE t_pg_stats_demo;
SELECT attname, n_distinct, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
AND attname = 'gender';
第二步:查看执行计划
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
第三步:大量修改数据,但不执行 ANALYZE
sql
UPDATE t_pg_stats_demo
SET gender = 'F'
WHERE id <= 9000;
此时真实数据已经变了。
F 已经变成高频值。
但是统计信息还没更新。
第四步:再次查看执行计划
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
这时优化器仍然可能按照旧统计信息估算。
也就是说:
text
真实数据已经变了
但优化器还不知道
第五步:重新收集统计信息
sql
ANALYZE t_pg_stats_demo;
第六步:查看新的统计信息
sql
SELECT attname, n_distinct, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
AND attname = 'gender';
此时你会看到 F 的占比明显上升。
第七步:再次查看执行计划
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
这时执行计划可能发生变化。
这个实验说明:
text
统计信息不是实时更新的
大批量数据变更后,需要及时 ANALYZE
十三、如何判断统计信息是否准确?
1. 看 pg_stats 的估算
sql
SELECT
attname,
null_frac,
n_distinct,
most_common_vals,
most_common_freqs
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
ORDER BY attname;
2. 看真实数据分布
例如查看 gender:
sql
SELECT gender, count(*)
FROM t_pg_stats_demo
GROUP BY gender
ORDER BY count(*) DESC;
查看 city:
sql
SELECT city, count(*)
FROM t_pg_stats_demo
GROUP BY city
ORDER BY count(*) DESC;
查看 age:
sql
SELECT age, count(*)
FROM t_pg_stats_demo
GROUP BY age
ORDER BY count(*) DESC, age;
3. 对比估算与真实值
如果 pg_stats 中:
text
most_common_freqs = {.7,.3}
但真实数据是:
text
M = 10%
F = 90%
说明统计信息过期了。
解决方法:
sql
ANALYZE t_pg_stats_demo;
十四、如何提高统计信息精度?
默认情况下,数据库不会统计所有数据,而是采样。
如果某些列数据分布非常倾斜,默认采样可能不够准确。
可以提高某个字段的统计目标。
sql
ALTER TABLE t_pg_stats_demo
ALTER COLUMN salary SET STATISTICS 1000;
然后重新分析:
sql
ANALYZE t_pg_stats_demo;
查看统计信息:
sql
SELECT attname, most_common_vals, most_common_freqs, histogram_bounds
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
AND attname = 'salary';
SET STATISTICS 的值越大,统计信息越细,但 ANALYZE 成本也越高。
一般不建议全库随便调大。
适合针对:
text
严重数据倾斜的字段
经常作为查询条件的字段
慢 SQL 中估算行数严重错误的字段
十五、pg_stats 排查慢 SQL 的实用流程
当遇到慢 SQL 时,可以按下面流程排查。
第一步:查看执行计划
sql
EXPLAIN ANALYZE
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
重点看:
text
rows
actual rows
如果差距很大,说明优化器估算不准。
第二步:查看相关字段统计信息
sql
SELECT
attname,
null_frac,
n_distinct,
most_common_vals,
most_common_freqs,
histogram_bounds,
correlation
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
AND attname IN ('salary');
第三步:查看真实数据分布
sql
SELECT
min(salary),
max(salary),
count(*)
FROM t_pg_stats_demo;
sql
SELECT salary, count(*)
FROM t_pg_stats_demo
GROUP BY salary
ORDER BY count(*) DESC
LIMIT 20;
第四步:重新收集统计信息
sql
ANALYZE t_pg_stats_demo;
第五步:必要时提高统计精度
sql
ALTER TABLE t_pg_stats_demo
ALTER COLUMN salary SET STATISTICS 1000;
ANALYZE t_pg_stats_demo;
第六步:重新查看执行计划
sql
EXPLAIN ANALYZE
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
如果估算行数和实际行数明显接近,说明统计信息改善有效。
十六、pg_stats 常见误区
误区一:有索引就一定走索引
错误。
优化器是否走索引,要看成本。
如果查询返回大量数据,全表扫描可能更快。
误区二:pg_stats 是实时的
错误。
pg_stats 来自 ANALYZE。
数据大量变化后,如果没有重新分析,统计信息可能是旧的。
误区三:n_distinct 永远表示具体数量
错误。
当 n_distinct 是正数时,表示估算的不同值数量。
当 n_distinct 是负数时,表示不同值数量占总行数的比例。
误区四:histogram_bounds 没什么用
错误。
它对范围查询非常重要。
例如:
sql
WHERE salary BETWEEN 8000 AND 9000
sql
WHERE created_at >= '2026-01-01'
这类条件都需要直方图辅助估算。
误区五:correlation 可以忽略
错误。
correlation 会影响索引扫描成本。
如果字段和物理存储顺序高度相关,索引范围扫描可能很高效。
如果字段随机分布,索引范围扫描可能产生大量随机 IO。
十七、生产环境建议
1. 大批量导入后执行 ANALYZE
sql
ANALYZE table_name;
例如:
sql
ANALYZE orders;
2. 大批量 UPDATE/DELETE 后执行 ANALYZE
sql
ANALYZE table_name;
尤其是状态字段、时间字段、分区字段发生大规模变化后。
3. 慢 SQL 优先看估算行数
sql
EXPLAIN ANALYZE
SELECT ...
重点看:
text
rows
actual rows
如果估算和实际差距巨大,不要急着建索引,先看统计信息。
4. 数据倾斜字段提高统计目标
sql
ALTER TABLE table_name
ALTER COLUMN column_name SET STATISTICS 1000;
ANALYZE table_name;
5. 定期检查核心表统计信息
例如核心业务表:
sql
SELECT
schemaname,
tablename,
attname,
null_frac,
n_distinct,
most_common_vals,
most_common_freqs,
correlation
FROM pg_stats
WHERE tablename IN ('orders', 'users', 'payments')
ORDER BY tablename, attname;
十八、完整实验 SQL 汇总
下面是一套可以直接复现的 SQL。
sql
DROP TABLE IF EXISTS t_pg_stats_demo;
CREATE TABLE t_pg_stats_demo (
id serial PRIMARY KEY,
city text,
gender text,
age int,
salary numeric(10,2),
status text,
created_at date
);
INSERT INTO t_pg_stats_demo(city, gender, age, salary, status, created_at)
SELECT
CASE
WHEN i <= 5000 THEN '北京'
WHEN i <= 8000 THEN '上海'
WHEN i <= 9500 THEN '广州'
ELSE '深圳'
END AS city,
CASE
WHEN i % 10 < 7 THEN 'M'
ELSE 'F'
END AS gender,
CASE
WHEN i <= 7000 THEN 25 + (i % 5)
ELSE 40 + (i % 20)
END AS age,
CASE
WHEN i <= 9000 THEN 8000 + (i % 1000)
ELSE 30000 + (i % 5000)
END AS salary,
CASE
WHEN i <= 8500 THEN '正常'
WHEN i <= 9500 THEN '冻结'
ELSE '注销'
END AS status,
current_date - (i % 365)
FROM generate_series(1, 10000) AS s(i);
ANALYZE t_pg_stats_demo;
SELECT
schemaname,
tablename,
attname,
null_frac,
n_distinct,
most_common_vals,
most_common_freqs,
histogram_bounds,
correlation
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
ORDER BY attname;
创建索引:
sql
CREATE INDEX idx_demo_gender ON t_pg_stats_demo(gender);
CREATE INDEX idx_demo_age ON t_pg_stats_demo(age);
CREATE INDEX idx_demo_salary ON t_pg_stats_demo(salary);
CREATE INDEX idx_demo_created_at ON t_pg_stats_demo(created_at);
ANALYZE t_pg_stats_demo;
查看执行计划:
sql
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'M';
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 8000 AND 9000;
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE salary BETWEEN 30000 AND 35000;
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE created_at BETWEEN current_date - 30 AND current_date;
查看真实分布:
sql
SELECT gender, count(*)
FROM t_pg_stats_demo
GROUP BY gender
ORDER BY count(*) DESC;
SELECT age, count(*)
FROM t_pg_stats_demo
GROUP BY age
ORDER BY count(*) DESC, age;
SELECT city, count(*)
FROM t_pg_stats_demo
GROUP BY city
ORDER BY count(*) DESC;
SELECT status, count(*)
FROM t_pg_stats_demo
GROUP BY status
ORDER BY count(*) DESC;
复现统计信息过期:
sql
ANALYZE t_pg_stats_demo;
SELECT attname, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
AND attname = 'gender';
UPDATE t_pg_stats_demo
SET gender = 'F'
WHERE id <= 9000;
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
ANALYZE t_pg_stats_demo;
SELECT attname, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 't_pg_stats_demo'
AND attname = 'gender';
EXPLAIN
SELECT *
FROM t_pg_stats_demo
WHERE gender = 'F';
十九、总结
pg_stats 是 PostgreSQL/openGauss SQL 优化中非常重要的视图。
它不是业务数据表,而是优化器使用的数据画像。
它告诉优化器:
text
字段有没有 NULL
字段有多少不同值
哪些值最常见
常见值占比是多少
范围数据如何分布
字段顺序和物理存储顺序是否相关
真正理解 pg_stats 后,再看执行计划就不会只停留在:
text
为什么不走索引?
而是会进一步分析:
text
优化器为什么认为不该走索引?
它估算的返回行数是否准确?
统计信息是否过期?
字段数据是否倾斜?
是否需要 ANALYZE?
是否需要提高统计目标?
一句话总结:
text
索引决定数据库有没有可能快;
统计信息决定优化器知不知道该怎么快。
所以,学习 SQL 优化,不能只学索引,也必须学会看 pg_stats。
若有转载,请标明出处:https://blog.csdn.net/CharlesYuangc/article/details/161489067