ANALYZE 统计收集
ANALYZE 是 PostgreSQL 里「看不见但决定一切」的命令。它不改变数据、不返回结果、平时也没人在意,但查询规划器(query planner)为每条 SQL 挑选执行计划时,唯一依据就是 ANALYZE 收集到的统计信息。
统计信息一旦失真,规划器的行数估算(row estimate)就会离谱,进而选中错误的扫描方式或连接算法——一条本来 1 毫秒的查询可能变成 10 秒。这就是为什么 ANALYZE 被归入「维护」而不是「优化」:它是执行计划质量的前置条件,而不是可选项。
本节从配置与运维的角度讲 ANALYZE:有哪些参数控制它的精度与触发时机、采样机制究竟怎么算、在分区/继承/外部表这些特殊场景下的行为差异、怎么观察进度、怎么把「统计是否新鲜」纳入监控。统计信息字段本身的含义与执行计划层面的影响,分别在相邻两节展开。
前置知识
阅读本节前,建议先了解:冻结事务 ID(FREEZE)
基础概念
ANALYZE 在维护体系中的位置
VACUUM 与 ANALYZE 经常一起出现,但它们的职责完全不同,这一点必须先分清:
| 维度 | VACUUM | ANALYZE |
|---|---|---|
| 目标 | 回收死元组空间、更新 VM、冻结 | 收集列级数据分布统计 |
| 是否修改表数据 | 修改(标记空间、冻结标志) | 不修改 |
| 是否写 WAL | 写(页级修改) | 几乎不写(只更新系统目录) |
| 主要瓶颈 | 磁盘 I/O(读全表/索引) | CPU(排序与统计计算) |
| 触发参数 | autovacuum_vacuum_* | autovacuum_analyze_* |
| 不做会怎样 | 表膨胀、XID 回卷 | 执行计划选错 |
| 产出物 | FSM / VM 状态、冻结标志 | pg_statistic 中的统计行 |
因为 autovacuum 守护进程会同时承担这两种工作(按各自的阈值独立触发),很容易让人以为它们是同一件事。实际上它们是两条独立的流水线:一次 VACUUM 不一定带分析,一次 ANALYZE 也完全不需要先清理。
autovacuum 守护进程的两条独立流水线
┌──────────────────────────────────────────┐
│ autovacuum launcher │
└───────────────┬──────────────────────────┘
│ 为每张表分别判断
┌───────────────┴──────────────────────────┐
│ │
┌──────┴───────┐ ┌───────┴──────┐
│ VACUUM 流水线│ │ ANALYZE 流水线│
│ 阈值:死元组 │ │ 阈值:被修改 │
│ 插入量 │ │ 元组数 │
│ 产出:FSM/VM │ │ 产出:pg_ │
│ /冻结 │ │ statistic │
└───────────────┘ └──────────────┘
二者互不依赖,各自独立触发统计信息存在哪里
ANALYZE 的产出物落在两张系统目录里,理解它们的区别对排查问题很重要:
| 目录 | 内容 | 可见性 | 大小 |
|---|---|---|---|
pg_statistic | 真实的统计值(原始形式) | 仅超级用户可读 | 与 statistics_target 成正比 |
pg_stats | pg_statistic 的可读视图 | 所有用户可读(且只显示自己有权读的表) | 视图,不额外占空间 |
pg_class.reltuples | 表的估算行数 | 所有用户 | 一个字段 |
pg_class.relpages | 表的估算页数 | 所有用户 | 一个字段 |
-- 最常用的查看方式:pg_stats(普通用户也能用)
SELECT attname, n_distinct, null_frac, correlation
FROM pg_stats
WHERE schemaname = 'public' AND tablename = 'orders';reltuples / relpages 不是精确值
pg_class.reltuples 和 relpages 由 VACUUM、ANALYZE、CREATE INDEX 等操作更新,它们不是实时的:刚批量写入 100 万行但还没分析,reltuples 显示的仍是旧值。对超大表,ANALYZE 甚至可能只做部分扫描,reltuples 是按比例外推的估算。
所以:不要用 reltuples 当作「表里到底有多少行」的答案。需要精确值时用 SELECT count(*)(但代价是扫全表)。
统计信息的「新鲜度」比「精度」更容易被忽视
统计信息有两个独立的失效维度:
统计信息失真的两种原因(要分别监控)
① 精度不足(statistics_target 太低)
数据分布复杂(高度倾斜、多列强相关),
但直方图桶数不够 → 估算天然粗糙
→ 靠调高 default_statistics_target 或列级 SET STATISTICS 缓解
② 新鲜度不足(距离上次 ANALYZE 太久)
表已经变了 500 万行,统计还停留在百万行时
→ 靠调 autovacuum_analyze_* 让它更勤快,或手工 ANALYZE
两者症状相似(都是"估算不准"),但治法完全不同。新鲜度怎么量化?pg_stat_user_tables 提供了一个直接指标:
SELECT relname,
n_mod_since_analyze,
n_live_tup,
round(n_mod_since_analyze * 100.0 / greatest(n_live_tup, 1), 1) AS stale_pct,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_mod_since_analyze DESC
LIMIT 10;n_mod_since_analyze 是「自上次分析以来被修改的元组数」。当它相对 n_live_tup 显著变大(比如超过 20%),说明统计已经明显过时。
术语表
| 概念 | 术语 | 说明 |
|---|---|---|
| 统计信息 | statistics | 规划器用于估算行数与选择率的元数据 |
| 统计目标 | statistics target | 控制 MCV 条目数与直方图桶数的精度旋钮 |
| 采样 | sampling | 只读表的一部分行来推算整体分布 |
| 被修改元组数 | modified tuples | 触发自动分析的计数量 |
| 函数依赖统计 | extended statistics | 跨列相关性统计,见扩展统计一节 |
| 自动分析 | autoanalyze | autovacuum 触发的 ANALYZE |
| 统计缓存 | stats cache | 累积统计在内存中的副本 |
关于采样,需要先建立一个正确认识(这是一个高频误区):
ANALYZE不在全表上计算统计,而是读取一个随机采样。官方文档的表述是「采样行数由被分析的各列中最大的统计目标决定」(The largest statistics target among the columns being analyzed determines the number of table rows sampled)。
很多人(包括一些教程)会把采样行数说成一个固定公式,例如「300 × default_statistics_target 行」。官方文档并没有给出这个具体倍数,这个数字是通过实测反推出来的经验值,会随版本、数据类型、ANALYZE 的实现细节而变化。写作与调优时,请以「采样量随 statistics_target 线性增长」这个定性结论为准,而不要把它当成规格说明。
采样为什么够用
直觉上「只读一部分行」似乎很不可靠,但对规划器的用途来说并非如此:
统计采样的可靠性来自统计学的"大数定律"
目标:估算"WHERE status = 'failed' 命中多少行"
只需要知道 status 的分布比例,不需要知道每一行
采样 30,000 行即可把占比估计到 1% 以内的误差
(对 1 亿行的表,读 3 万行只占 0.03%)
→ 因此 ANALYZE 对超大表通常也很快
→ 但这也意味着:数据分布"越不均匀",采样越容易漏掉稀有值
(例如:1 亿行里只有 50 行的极端异常值,可能一次都采不到)最后那条「漏掉稀有值」正是 statistics_target 与「扩展统计」这两节要解决的问题。
语法与用法
基本语法
PostgreSQL 11 之后的语法:
ANALYZE [ ( option [, ...] ) ] [ table_and_columns [, ...] ]
option 取值:
VERBOSE [ boolean ]
SKIP_LOCKED [ boolean ]
BUFFER_USAGE_LIMIT size
table_and_columns 为:
[ ONLY ] table_name [ * ] [ ( column_name [, ...] ) ]旧版(11 之前)语法仍然兼容,但没有括号选项:
-- 旧语法,仍可用
ANALYZE VERBOSE table_name;各选项含义:
| 选项 | 作用 | 版本 |
|---|---|---|
VERBOSE | 打印采样过程与结果细节 | — |
SKIP_LOCKED | 拿不到锁就跳过该表,不等待 | 12+ |
BUFFER_USAGE_LIMIT | 限制分析时使用的缓冲区大小 | 16+ |
ONLY | 只分析父表自身,不含继承子表 | 18+ |
* | 显式表示「父表 + 所有子表」 | 18+ |
PostgreSQL 18 的行为变更
从 PostgreSQL 18 起,ANALYZE(与 VACUUM 一样)默认处理继承子表。如果只想分析父表本身,需要显式加 ONLY:
ANALYZE ONLY parent_table; -- 18+,只分析父表自身在 18 之前,对父表执行 ANALYZE 只分析父表本身;升级后行为发生了变化,依赖旧行为的脚本需要检查。
常见执行形态
| 形态 | 写法 | 用途 |
|---|---|---|
| 全库分析 | ANALYZE; | 日常维护,最快也最省事 |
| 指定表 | ANALYZE orders; | 批量写入后立即刷新 |
| 指定列 | ANALYZE orders (amount, status); | 只关心某些热点列 |
| 带细节 | ANALYZE (VERBOSE) orders; | 排查采样量是否异常 |
| 不等待锁 | ANALYZE (SKIP_LOCKED) orders; | 避免被长事务挡住 |
| 与清理合并 | VACUUM (ANALYZE) orders; | 一次扫描做两件事 |
-- 大批量数据导入后的标准收尾
VACUUM (ANALYZE) orders;INFO: vacuuming "public.orders"
INFO: "orders": found 0 removable, 3004271 nonremovable row versions in 528420 pages
INFO: analyzing "public.orders"
INFO: "orders": scanned 528420 of 528420 pages, containing 3004271 live rows and 0 dead rows; 30000 rows in sample, 3004271 estimated total rows这一行输出是理解采样机制的关键:
INFO: "orders": scanned 528420 of 528420 pages,
containing 3004271 live rows and 0 dead rows;
30000 rows in sample, ← 实际参与统计计算的行数
3004271 estimated total rows ← 由采样外推出的总行数30000 rows in sample 就是采样量。改变 statistics_target 会直接改变这个数字——这正是「采样量随统计目标增长」的证据。
核心配置参数
| 参数 | 默认值 | 范围 | 作用 |
|---|---|---|---|
default_statistics_target | 100 | 1 ~ 10000 | 全局默认统计目标 |
autovacuum_analyze_threshold | 50 | 0 ~ 2147483647 | 触发自动分析的基数项 |
autovacuum_analyze_scale_factor | 0.1 | 0 ~ 100 | 触发自动分析的随行数增长项 |
vacuum_buffer_usage_limit | 2MB | 128kB ~ 16GB,0 禁用 | ANALYZE / VACUUM 的缓冲区上限 |
track_counts | on | — | 是否收集统计计数(关闭则 autovacuum 完全失效) |
-- 查看当前值
SHOW default_statistics_target;
SHOW autovacuum_analyze_scale_factor;
-- 一次性列出全部
SELECT name, setting FROM pg_settings
WHERE name IN ('default_statistics_target',
'autovacuum_analyze_threshold',
'autovacuum_analyze_scale_factor',
'vacuum_buffer_usage_limit');default_statistics_target 不是「越精确越好」
它同时决定三样东西的规模:
default_statistics_target = N 时
├── 直方图(histogram)最多 N 个桶
├── MCV(most common values)列表最多 N 个条目
└── 每组关联统计(扩展统计)的规模随之增长
N 增大 → 估算精度提升
→ ANALYZE 耗时增加(采样行数随之增加,还要排序)
→ pg_statistic 占用空间增加(每列最多增加数 KB)
→ 规划器读统计的 CPU 开销略增默认 100 对多数场景够用。真正需要提高精度的通常只有少数几列,应当用列级 SET STATISTICS 精确处理,而不是把全局值调大——全局调大等于给所有表的每一列都增加成本。
列级统计目标
-- 只给热点列提高精度,其余列保持默认
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;
ALTER TABLE orders ALTER COLUMN customer_id SET STATISTICS 500;
-- 查看列级设置
SELECT attname, attstattarget
FROM pg_attribute
WHERE attrelid = 'orders'::regclass AND attnum > 0 AND NOT attisdropped; attname | attstattarget
--------------+---------------
id | -1 ← -1 表示"使用全局默认值"
status | 1000
customer_id | 500
amount | -1
created_at | -1-- 恢复为全局默认
ALTER TABLE orders ALTER COLUMN status SET STATISTICS -1;
-- 用 0 可以完全禁用该列的统计收集(该列不产生任何 pg_statistic 行)
ALTER TABLE orders ALTER COLUMN notes SET STATISTICS 0;什么时候该提高列级统计目标
判据不是「这一列重要」,而是「这一列的估算明显不准,且原因确实是精度不够」。典型信号:
- 该列的值分布高度倾斜(少数值占了绝大部分行),且查询条件恰好命中这些值;
- 该列在多个高频查询的
WHERE里出现,是过滤的主要入口; - 该列上的选择性估算与
EXPLAIN ANALYZE的实际行数差一个数量级以上,而提高target后确实改善。
如果提高目标后估算没变好,说明问题不在精度,而在新鲜度(统计过时)或列间相关性(需要扩展统计)。
BUFFER_USAGE_LIMIT 与缓冲区策略
ANALYZE 读表会占用共享缓冲区(shared_buffers)。为了不让维护操作把业务热数据挤出缓存,PostgreSQL 对 VACUUM / ANALYZE 施加了缓冲区限制:
-- 指定本次分析最多使用 64MB 缓冲区
ANALYZE (BUFFER_USAGE_LIMIT '64MB') big_table;规则:
BUFFER_USAGE_LIMIT 的行为
未指定 → 使用 vacuum_buffer_usage_limit(默认 2MB,上限为 shared_buffers 的 1/8)
0 → 禁用限制(可自由使用共享缓冲区,会污染缓存,慎用)
最小 128kB,最大 16GB详细说明
采样与统计目标的关系
把采样量的决定逻辑画出来:
ANALYZE 采样量是怎么定的
① 对每个被分析的列,取它的 statistics target:
列级 attstattarget(若非 -1) > 否则 default_statistics_target
② 取这些目标值中的【最大值】:T = max(targets)
③ 采样行数与 T 成正比(具体倍数由实现决定,不是官方固定的常数)
④ 采样后按列排序,据此构建:
MCV 列表(最多 T 条)
直方图(最多 T 个桶)
n_distinct / null_frac / correlation 等标量
结论:只要【任一列】的 target 很高,【整表】的采样量都会提高。
这是列级 STATISTICS 的一个副作用,调优时要知道。示例:一张有 5 列的表
target 设置: col_a=-1 col_b=-1 col_c=1000 col_d=-1 col_e=-1
全局默认 100
实际目标: 100 100 1000 100 100
取最大值 T = 1000
→ 采样量按 T=1000 决定(而不是按单列各自的 target)自动分析的触发条件
autovacuum 用一条「基数 + 比例」的公式决定是否对某表做分析:
触发自动 ANALYZE:
被修改元组数 > autovacuum_analyze_threshold
+ autovacuum_analyze_scale_factor × 表估算行数
代入默认值(threshold = 50,scale_factor = 0.1):
被修改元组数 > 50 + 0.1 × reltuples把这个公式代进具体规模,就能看出它在不同表上的「灵敏度」:
| 表估算行数 | 触发所需的修改行数 | 占表比例 |
|---|---|---|
| 1,000 | 150 | 15% |
| 10,000 | 1,050 | 10.5% |
| 100,000 | 10,050 | 10% |
| 1,000,000 | 100,050 | 10% |
| 10,000,000 | 1,000,050 | 10% |
| 100,000,000 | 10,000,050 | 10% |
读法:
- 小表(几百行)由 threshold = 50 主导,变化 50 行左右就会触发
- 大表(上万行以上)由 scale_factor = 0.1 主导,恒定要变化 10% 才触发
问题:对 1 亿行的表,"变化 10%" = 1000 万行。
这意味着这张表的统计可能长期滞后 1000 万行,
而 1000 万行的数据变化足以让分布彻底改观。
→ 所以大表通常需要按表调低 scale_factor。-- 大表的常见做法:降低比例项,并显式给个下限
ALTER TABLE huge_events SET (
autovacuum_analyze_scale_factor = 0.02,
autovacuum_analyze_threshold = 10000
);autovacuum_vacuum_insert_scale_factor 的一个细节
顺带纠正一个常被混淆的点:触发清理的插入维度参数 autovacuum_vacuum_insert_scale_factor 是按未冻结页数(unfrozen pages)计算的,而不是按 reltuples。这与分析阈值的算法不同。分析阈值(autovacuum_analyze_scale_factor)用的是行数。详见 AutoVacuum 配置。
继承表与分区表的行为差异
这是最容易踩坑的地方,必须分清三个概念:
| 对象类型 | ANALYZE 的行为 | autovacuum 是否自动分析 |
|---|---|---|
| 普通表 | 收集自身统计 | 是(满足阈值时) |
| 继承父表 | 收集两套统计 | 仅当父表自身被修改时 |
| 继承子表 | 收集自身统计 | 是(各子表独立判断) |
| 声明式分区表 | 收集分区表自身的统计 | 否(autovacuum 不分析分区表) |
| 外部表(foreign table) | 仅在显式指定时分析 | 否 |
「继承父表的两套统计」是什么意思?看 pg_stats 的 inherited 列:
SELECT schemaname, tablename, attname, inherited, n_distinct
FROM pg_stats
WHERE tablename = 'events'
ORDER BY attname, inherited; schemaname | tablename | attname | inherited | n_distinct
------------+-----------+---------+-----------+------------
public | events | status | f | 5 ← 仅父表自身的数据
public | events | status | t | 5 ← 父表 + 所有子表汇总inherited = f 表示「只统计父表本身的数据」,inherited = t 表示「汇总包括所有子表的数据」。规划器在查询父表(包含子表)时用后者,在 SELECT ... FROM ONLY parent 时用前者。
分区/继承场景的三条实用结论
① 手工 ANALYZE 父表会同时更新两套统计(inherited = f / t)
② autovacuum 不会因为"子表变了"去分析父表
→ 父表统计长期不更新 → 查父表(跨分区)的计划变差
③ 所以:分区表的父表必须【手工定期 ANALYZE】-- 分区表的父表必须手工分析(建议纳入日常调度)
ANALYZE parent_partitioned_table;
-- 18+ 可以精确控制范围
ANALYZE ONLY parent_table; -- 只分析父表自身
ANALYZE parent_table; -- 分析父表及所有子表(18+ 默认行为)外部表(Foreign Table)
-- 外部表不会自动分析,必须手工执行(且必须显式指定表名)
ANALYZE foreign_table_name;官方文档明确指出:autovacuum 不会对外部表发起 ANALYZE,因为它无法判断这样做是否有用。如果查询外部表需要统计信息才能得到好计划,就必须自己安排定时分析。
观察分析进度
ANALYZE 有自己的进度视图 pg_stat_progress_analyze:
SELECT pid, datname, relid::regclass AS table_name, phase,
sample_blks_total, sample_blks_scanned,
ext_stats_total, ext_stats_computed,
child_tables_total, child_tables_done
FROM pg_stat_progress_analyze; pid | datname | table_name | phase | sample_blks_total | sample_blks_scanned | ext_stats_total | ext_stats_computed | child_tables_total | child_tables_done
-------+---------+-----------------+------------------------+-------------------+---------------------+-----------------+--------------------+--------------------+-------------------
29501 | mydb | public.orders | acquiring sample rows | 3120 | 1840 | 0 | 0 | 0 | 0
29512 | mydb | public.events | computing statistics | 1024 | 1024 | 3 | 1 | 24 | 18各列含义:
| 列 | 含义 |
|---|---|
sample_blks_total | 计划采样的总页数 |
sample_blks_scanned | 已采样页数 |
ext_stats_total / ext_stats_computed | 扩展统计的待计算数 / 已完成数 |
child_tables_total / child_tables_done | 继承子表的待处理数 / 已完成数 |
current_child_table_relid | 当前正在分析的子表 |
delay_time | 因成本延迟累计等待的毫秒数 |
phase 的取值:
pg_stat_progress_analyze.phase 的完整枚举
initializing → 正在准备
acquiring sample rows → 正在读取采样行(主要 I/O 阶段)
acquiring inherited sample rows→ 正在读取继承子表的采样行
computing statistics → 正在计算统计(主要 CPU 阶段)
computing extended statistics → 正在计算扩展统计
finalizing analyze → 正在收尾(写系统目录)用进度视图定位「为什么 ANALYZE 这么慢」
phase长期停在acquiring sample rows且sample_blks_scanned增长缓慢 → I/O 瓶颈,磁盘在忙。phase停在computing statistics很久 → CPU 瓶颈,可能是因为某列statistics_target设得极高,排序量巨大。child_tables_total很大且长时间不见减少 → 子表数量过多,可以考虑给父表单独调优或分批处理。
统计信息与累积统计系统的刷新节奏
一个容易误判的现象:刚执行完 ANALYZE,立刻查 pg_stat_user_tables.last_autoanalyze 却看不到变化,或者 n_mod_since_analyze 没清零。
原因是累积统计系统(cumulative statistics system)有自己的刷新机制:
累积统计的可见性延迟
① 每个后端进程把计数写进共享内存,
默认最多每 1 秒(PGSTAT_MIN_INTERVAL)刷一次
② 事务内重复读取默认走缓存(stats_fetch_consistency = cache)
③ 若需要"最新值",可在事务内调 pg_stat_clear_snapshot(),
或把 stats_fetch_consistency 设为 snapshot / none
结论:看到"统计没更新"时,先别急着怀疑 ANALYZE 失败,
可能只是累积统计还没刷新到共享内存。-- 强制丢弃缓存快照,读到最新累积统计
SELECT pg_stat_clear_snapshot();
SELECT relname, last_analyze, last_autoanalyze, n_mod_since_analyze
FROM pg_stat_user_tables WHERE relname = 'orders';实战示例
场景一:为一张热点表建立统计数据治理
背景:orders 表 3000 万行,status 列查询频繁且分布倾斜,最近多条查询的计划不稳定。
第一步:先确认「不准」是新鲜度问题还是精度问题
SELECT relname,
n_live_tup,
n_mod_since_analyze,
round(n_mod_since_analyze * 100.0 / greatest(n_live_tup, 1), 1) AS stale_pct,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'orders'; relname | n_live_tup | n_mod_since_analyze | stale_pct | last_analyze | last_autoanalyze
---------+------------+---------------------+-----------+-------------------------------+-------------------------------
orders | 3004271 | 812044 | 27.0 | 2026-09-20 02:11:03.412+08 | 2026-09-27 14:02:51.991+08stale_pct 27%,且自动分析在 9 月 27 日刚跑过——说明新鲜度不是主因,精度可能有问题。
第二步:看分布倾斜程度
SELECT attname, n_distinct, null_frac, correlation
FROM pg_stats
WHERE schemaname = 'public' AND tablename = 'orders' AND attname = 'status'; attname | n_distinct | null_frac | correlation
---------+------------+-----------+-------------
status | 6 | 0 | -0.021只有 6 个不同值,看起来很简单——但问题正在这里:值很少时,直方图起不了作用,统计完全依赖 MCV 列表。如果分布极度倾斜,而 MCV 列表只有 100 条(对 6 个值其实够),真正的风险是规划器把某个值的选择率估错。查 MCV:
SELECT most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status'; most_common_vals | most_common_freqs
----------------------+---------------------------------
{paid,shipped,failed} | {0.521,0.310,0.16833333}只有 3 个值进了 MCV 列表,most_common_freqs 合计 0.9993——说明只有 3 个值有实际数据,其余 3 个是「定义了但没用过」的枚举值。这解释了为什么估算会偏:规划器认为 status 只有 3 个有效值。
第三步:对症处理
这不是精度问题,而是统计新鲜度 + MCV 覆盖的组合问题。做法是:先用一次手工 ANALYZE 刷新,再针对这一列适度提高目标以覆盖边界情况。
-- 1) 立即刷新
ANALYZE (VERBOSE) orders;
-- 2) 给这一列一个稍高的目标,保证 MCV 覆盖更完整
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500;
ANALYZE (VERBOSE) orders (status);INFO: analyzing "public.orders"
INFO: "orders": scanned 528420 of 528420 pages, containing 3004271 live rows and 0 dead rows; 50000 rows in sample, 3004271 estimated total rows注意采样量从 30000 提升到了 50000 —— 印证了「采样量与统计目标正相关」。
第四步:让自动分析更及时
对大表降低比例项,避免统计滞后 10%(即 30 万行)。
ALTER TABLE orders SET (
autovacuum_analyze_scale_factor = 0.02,
autovacuum_analyze_threshold = 5000
);改完触发条件变成 5000 + 0.02 × 行数,即约 6 万行变化就会自动分析,远好于原来的 30 万行。
第五步:验证效果
-- 用实际行数对照估算行数(在 psql 中执行 EXPLAIN ANALYZE)
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM orders WHERE status = 'failed'; Aggregate (cost=... rows=1 width=8) (actual time=412.331..412.332 rows=1 loops=1)
-> Index Only Scan using idx_orders_status on orders
(cost=... rows=504118 width=0) (actual time=0.041..355.902 rows=504871 loops=1)
Index Cond: (status = 'failed'::text)
Heap Fetches: 1204估算 rows=504118,实际 rows=504871,误差 0.15% —— 统计已经足够准。
场景二:分区表统计长期不更新
背景:events 按月份分区,父表 events 上有跨分区查询。查询计划越来越差,但每个分区自己看起来都正常。
第一步:确认父表的统计日期
SELECT relname, n_live_tup, n_mod_since_analyze,
last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'events'; relname | n_live_tup | n_mod_since_analyze | last_analyze | last_autoanalyze
---------+------------+---------------------+--------------+------------------
events | 0 | 0 | | ← 父表自身没有数据,也从未被分析注意 n_live_tup = 0:分区表的父表通常是空的(数据都在子分区里),所以「父表自身没被修改」,autovacuum 永远不会去分析它。
第二步:确认子分区各自是被分析的
SELECT relname, n_live_tup, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname LIKE 'events_%'
ORDER BY relname DESC LIMIT 6; relname | n_live_tup | last_autoanalyze
--------------+------------+-------------------------------
events_2026_09 | 8120443 | 2026-09-29 03:41:12.881+08
events_2026_08 | 9018332 | 2026-09-29 03:38:55.102+08
...子表都在正常自动分析。问题只在于父表统计从未收集。
第三步:手工分析父表
-- 建立父表的统计数据(PG 18 默认会连同子表一起分析)
ANALYZE events;INFO: analyzing "public.events"
INFO: "events": scanned 0 of 0 pages, containing 0 live rows and 0 dead rows; 0 rows in sample, 0 estimated total rows-- 确认继承统计已存在(inherited = t)
SELECT attname, inherited, n_distinct
FROM pg_stats WHERE tablename = 'events' AND attname = 'status'; attname | inherited | n_distinct
---------+-----------+------------
status | f | 5 ← 父表自身(空表,无意义)
status | t | 5 ← 父表 + 全部子表(规划器查跨分区时用这个)第四步:把它纳入日常调度
父表统计不会自动更新,必须靠定时任务。推荐按「每天一次 + 分区结构变化时」执行:
-- 建议加入每日调度(例如 pg_cron 或系统 crontab + psql)
ANALYZE events;# 用系统 cron 每天凌晨执行
# 0 3 * * * psql -d mydb -c 'ANALYZE events;' >> /var/log/pg-analyze.log 2>&1第五步:监控「父表统计陈旧」
-- 找出所有分区/继承父表:自身无数据但子表有数据
SELECT p.relname AS parent_table,
(SELECT count(*) FROM pg_inherits WHERE inhparent = p.oid) AS children,
p.last_analyze
FROM pg_stat_user_tables p
WHERE EXISTS (SELECT 1 FROM pg_inherits WHERE inhparent = p.relid)
AND p.last_analyze IS NULL;这类表应当出现在「必须有定时 ANALYZE」的清单里。
注意事项
1. ANALYZE 不能在事务块内执行
与 VACUUM 一样,ANALYZE 不能在 BEGIN ... COMMIT 中运行。因此超时设置必须用会话级 SET,不能用 SET LOCAL。
-- 错误:ANALYZE 不能在显式事务块里
BEGIN;
ANALYZE orders;
COMMIT;
-- 正确:会话级设置超时
SET lock_timeout = '5s';
SET statement_timeout = '30min';
ANALYZE (VERBOSE) orders;
RESET statement_timeout;
RESET lock_timeout;2. ANALYZE 取的是 SHARE UPDATE EXCLUSIVE 锁
它与其他 ANALYZE、VACUUM、CREATE INDEX CONCURRENTLY 等互斥,但与普通读写兼容。
-- 不想被别的维护操作挡住,加 SKIP_LOCKED 快速跳过
ANALYZE (SKIP_LOCKED) orders;SKIP_LOCKED 的含义是「拿不到锁就跳过这张表,不做该表」,不是「降级锁」。批量分析时它能让整体任务不被某张被锁的表拖死。
3. 一次 ANALYZE 只更新被分析列/表的统计,其余保持原值
指定列分析时,未列出的列统计不会更新(保留旧值)。这在「只想刷某一列」时很方便,但也容易造成新旧统计混用。
-- 只刷新 status 列,其他列的统计保持原样
ANALYZE orders (status);4. 空表不会被写入新统计
官方文档:如果表被判定为空,不会记录新统计,而是保留原有统计。这带来一个反直觉的后果:把表清空后不 ANALYZE,规划器仍按「旧的行数」估算。
DELETE FROM staging_table;
-- 表已空,但规划器仍认为它有很多行
SELECT reltuples FROM pg_class WHERE oid = 'staging_table'::regclass;需要让规划器知道表空了,得手工分析(或 TRUNCATE,它会更新 reltuples)。
TRUNCATE staging_table; -- reltuples 立即归零
-- 或者
DELETE FROM staging_table; ANALYZE staging_table;5. 提高 statistics_target 有副作用,别全局调大
全局调大 default_statistics_target 会让所有表的所有列成本上升:ANALYZE 更慢、pg_statistic 更大、规划开销更高。按列设置才是正解。
-- 不推荐:全局一刀切
ALTER SYSTEM SET default_statistics_target = 1000;
-- 推荐:只给需要的列
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;6. n_mod_since_analyze 不是「未分析的行数」
它是自上次分析以来被插入/更新/删除的元组数,是一种「变化量」计数,与表中当前有多少行无关。
-- 用它算"陈旧度",不要当成"待分析行数"
SELECT relname, n_mod_since_analyze,
round(n_mod_since_analyze * 100.0 / greatest(n_live_tup, 1), 1) AS stale_pct
FROM pg_stat_user_tables ORDER BY stale_pct DESC NULLS LAST LIMIT 10;7. 关闭 track_counts 会让 autovacuum 彻底失效
autovacuum 与 autoanalyze 都依赖累积统计的计数来判断阈值。track_counts = off 时它们无法工作。
SHOW track_counts; -- 必须为 on8. 不要对超宽表的所有列都设高 target
一张有 200 列、每列都设 target = 1000 的表,ANALYZE 要为 200 列各做一次大规模排序,成本极高,而收益可能为零(大部分列根本没出现在 WHERE 里)。
-- 找出被设了高 target 的列,复核是否真的需要
SELECT c.relname, a.attname, a.attstattarget
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
WHERE a.attstattarget > 100
AND c.relkind IN ('r', 'p')
ORDER BY a.attstattarget DESC;最佳实践
1. 把 ANALYZE 看作「执行计划的前置条件」,纳入例行维护
统计过时是「计划突然变差」最常见的原因,而它又完全不报错。最低限度要保证:
例行分析策略
├── 让 autovacuum 正常工作(不要关闭)
├── 大表按表调低 autovacuum_analyze_scale_factor(0.02 ~ 0.05)
├── 分区/继承父表纳入定时任务手工 ANALYZE
├── 外部表按需手工 ANALYZE
├── 批量导入/迁移后立即手工 ANALYZE
└── 建立 n_mod_since_analyze / n_live_tup 的告警2. 用「估算误差」而不是「感觉」来决定是否调参
判断某列统计够不够准,标准做法是拿 EXPLAIN 的估算行数和 EXPLAIN ANALYZE 的实际行数对比:
-- 估算与实际差一个数量级,才值得调 target
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE status = 'failed' AND region = 'east';读法:
估算 rows=1200 实际 rows=118000 → 差 100 倍,需要处理
估算 rows=118000 实际 rows=120400 → 差 2%,无需处理3. 优先调列级 SET STATISTICS,不动全局默认
-- 只给真正需要的列提高精度
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;
ALTER TABLE orders ALTER COLUMN customer_id SET STATISTICS 500;
-- 定期复核,把不再需要的改回 -1
ALTER TABLE orders ALTER COLUMN customer_id SET STATISTICS -1;4. 把「父表统计陈旧」做成一条专门的告警
分区表的父表永远不会被自动分析,这是最容易被漏掉的一类问题。
-- 找出所有从未被手工分析过的分区/继承父表
SELECT p.relname AS parent_table,
(SELECT count(*) FROM pg_inherits WHERE inhparent = p.relid) AS children,
p.last_analyze,
pg_size_pretty(pg_total_relation_size(p.relid)) AS total_size
FROM pg_stat_user_tables p
WHERE EXISTS (SELECT 1 FROM pg_inherits WHERE inhparent = p.relid)
ORDER BY p.last_analyze NULLS FIRST;判据:一旦 children > 0 且 last_analyze IS NULL,
就应该立即把这张父表加入每日 ANALYZE 调度。5. 批量写入后「先分析再查询」,而不是「等自动分析」
自动分析的阈值对大表而言太迟钝(10%)。数据导入、批量更新、ALTER TABLE 加列回填之后,手工分析一次比等 autovacuum 划算得多。
-- 数据加载脚本的收尾三件套
VACUUM (ANALYZE) orders;
VACUUM (ANALYZE) order_items;
ANALYZE customers;6. 给维护动作加超时保护,避免拖住其他操作
ANALYZE 与 VACUUM、CREATE INDEX CONCURRENTLY 互斥。一个长跑的 ANALYZE 会阻塞后续维护,甚至让 CREATE INDEX CONCURRENTLY 失败。
SET lock_timeout = '5s';
SET statement_timeout = '30min';
ANALYZE (VERBOSE) orders;
RESET statement_timeout;
RESET lock_timeout;7. 用 log_autovacuum_min_duration 留下痕迹
默认 autovacuum 与 autoanalyze 都不写日志,出问题时无从追溯。设为 0 或 1000(1 秒)后,超过阈值的维护动作都会被记录,包括「分析花了多久、采样的行数、估算的总行数」。
ALTER SYSTEM SET log_autovacuum_min_duration = 1000;
SELECT pg_reload_conf();日志中会看到形如:
LOG: automatic analyze of table "mydb.public.orders"
system usage: CPU: user: 0.42 s, system: 0.11 s, elapsed: 1.83 s8. 把「统计新鲜度」与「VACUUM 时效」分开告警
这两个是独立问题,混在一起告警会导致处理时找错方向。
| 告警项 | 判据 | 处理方向 |
|---|---|---|
| 统计陈旧 | n_mod_since_analyze / n_live_tup > 20% | 调 autovacuum_analyze_* 或手工 ANALYZE |
| 清理滞后 | n_dead_tup / (n_live_tup + n_dead_tup) > 10% | 调 autovacuum_vacuum_* |
| 死元组不降 | n_dead_tup 长期不降 | 查长事务 / 复制槽 |
| 冻结压力 | age(relfrozenxid) > 1.5 亿 | 见 冻结事务 ID(FREEZE) |
9. 升级/迁移到大版本后复核 ANALYZE 相关行为
PostgreSQL 18 改变了 ANALYZE 对继承子表的默认行为(默认处理子表,新增 ONLY)。如果升级前的脚本依赖「ANALYZE parent 只分析父表」,升级后需要显式改写为 ANALYZE ONLY parent。
-- 18 之前的语义(升级后需要显式加 ONLY 才能保持)
ANALYZE ONLY parent_table;10. 每季度复核一遍统计配置
统计配置会随表与查询的演进逐渐失配。
季度复核清单
├── 是否有表的 n_mod_since_analyze / n_live_tup 长期 > 20%
├── 是否有分区/继承父表的 last_analyze 为空
├── 是否有外部表从未被手工 ANALYZE
├── 是否有列被设了高 attstattarget 但已不再需要
├── 是否有大表仍在使用默认 scale_factor = 0.1
└── 是否有查询的估算行数与实际行数长期差一个数量级以上下一节
继续学习:直方图与相关性统计