Skip to content

ANALYZE 统计收集 ​

ANALYZE 是 PostgreSQL 里「看不见但决定一切」的命令。它不改变数据、不返回结果、平时也没人在意,但查询规划器(query planner)为每条 SQL 挑选执行计划时,唯一依据就是 ANALYZE 收集到的统计信息。

统计信息一旦失真,规划器的行数估算(row estimate)就会离谱,进而选中错误的扫描方式或连接算法——一条本来 1 毫秒的查询可能变成 10 秒。这就是为什么 ANALYZE 被归入「维护」而不是「优化」:它是执行计划质量的前置条件,而不是可选项。

本节从配置与运维的角度讲 ANALYZE:有哪些参数控制它的精度与触发时机、采样机制究竟怎么算、在分区/继承/外部表这些特殊场景下的行为差异、怎么观察进度、怎么把「统计是否新鲜」纳入监控。统计信息字段本身的含义与执行计划层面的影响,分别在相邻两节展开。

前置知识

阅读本节前,建议先了解:冻结事务 ID(FREEZE)

基础概念 ​

ANALYZE 在维护体系中的位置 ​

VACUUM 与 ANALYZE 经常一起出现,但它们的职责完全不同,这一点必须先分清:

维度VACUUMANALYZE
目标回收死元组空间、更新 VM、冻结收集列级数据分布统计
是否修改表数据修改(标记空间、冻结标志)不修改
是否写 WAL写(页级修改)几乎不写(只更新系统目录)
主要瓶颈磁盘 I/O(读全表/索引)CPU(排序与统计计算)
触发参数autovacuum_vacuum_*autovacuum_analyze_*
不做会怎样表膨胀、XID 回卷执行计划选错
产出物FSM / VM 状态、冻结标志pg_statistic 中的统计行

因为 autovacuum 守护进程会同时承担这两种工作(按各自的阈值独立触发),很容易让人以为它们是同一件事。实际上它们是两条独立的流水线:一次 VACUUM 不一定带分析,一次 ANALYZE 也完全不需要先清理。

text
autovacuum 守护进程的两条独立流水线

          ┌──────────────────────────────────────────┐
          │           autovacuum launcher             │
          └───────────────┬──────────────────────────┘
                          │ 为每张表分别判断
          ┌───────────────┴──────────────────────────┐
          │                                          │
   ┌──────┴───────┐                          ┌───────┴──────┐
   │  VACUUM 流水线│                          │ ANALYZE 流水线│
   │  阈值:死元组 │                          │ 阈值:被修改  │
   │        插入量 │                          │        元组数 │
   │  产出:FSM/VM │                          │ 产出:pg_     │
   │        /冻结  │                          │   statistic  │
   └───────────────┘                          └──────────────┘
   二者互不依赖,各自独立触发

统计信息存在哪里 ​

ANALYZE 的产出物落在两张系统目录里,理解它们的区别对排查问题很重要:

目录内容可见性大小
pg_statistic真实的统计值(原始形式)仅超级用户可读与 statistics_target 成正比
pg_statspg_statistic 的可读视图所有用户可读(且只显示自己有权读的表)视图,不额外占空间
pg_class.reltuples表的估算行数所有用户一个字段
pg_class.relpages表的估算页数所有用户一个字段
sql
-- 最常用的查看方式: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(*)(但代价是扫全表)。

统计信息的「新鲜度」比「精度」更容易被忽视 ​

统计信息有两个独立的失效维度:

text
统计信息失真的两种原因(要分别监控)

① 精度不足(statistics_target 太低)
   数据分布复杂(高度倾斜、多列强相关),
   但直方图桶数不够 → 估算天然粗糙
   → 靠调高 default_statistics_target 或列级 SET STATISTICS 缓解

② 新鲜度不足(距离上次 ANALYZE 太久)
   表已经变了 500 万行,统计还停留在百万行时
   → 靠调 autovacuum_analyze_* 让它更勤快,或手工 ANALYZE

两者症状相似(都是"估算不准"),但治法完全不同。

新鲜度怎么量化?pg_stat_user_tables 提供了一个直接指标:

sql
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跨列相关性统计,见扩展统计一节
自动分析autoanalyzeautovacuum 触发的 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 线性增长」这个定性结论为准,而不要把它当成规格说明。

采样为什么够用 ​

直觉上「只读一部分行」似乎很不可靠,但对规划器的用途来说并非如此:

text
统计采样的可靠性来自统计学的"大数定律"

目标:估算"WHERE status = 'failed' 命中多少行"
      只需要知道 status 的分布比例,不需要知道每一行

采样 30,000 行即可把占比估计到 1% 以内的误差
(对 1 亿行的表,读 3 万行只占 0.03%)

→ 因此 ANALYZE 对超大表通常也很快
→ 但这也意味着:数据分布"越不均匀",采样越容易漏掉稀有值
   (例如:1 亿行里只有 50 行的极端异常值,可能一次都采不到)

最后那条「漏掉稀有值」正是 statistics_target 与「扩展统计」这两节要解决的问题。

语法与用法 ​

基本语法 ​

PostgreSQL 11 之后的语法:

sql
ANALYZE [ ( option [, ...] ) ] [ table_and_columns [, ...] ]

option 取值:
    VERBOSE [ boolean ]
    SKIP_LOCKED [ boolean ]
    BUFFER_USAGE_LIMIT size

table_and_columns 为:
    [ ONLY ] table_name [ * ] [ ( column_name [, ...] ) ]

旧版(11 之前)语法仍然兼容,但没有括号选项:

sql
-- 旧语法,仍可用
ANALYZE VERBOSE table_name;

各选项含义:

选项作用版本
VERBOSE打印采样过程与结果细节—
SKIP_LOCKED拿不到锁就跳过该表,不等待12+
BUFFER_USAGE_LIMIT限制分析时使用的缓冲区大小16+
ONLY只分析父表自身,不含继承子表18+
*显式表示「父表 + 所有子表」18+

PostgreSQL 18 的行为变更

从 PostgreSQL 18 起,ANALYZE(与 VACUUM 一样)默认处理继承子表。如果只想分析父表本身,需要显式加 ONLY:

sql
ANALYZE ONLY parent_table;   -- 18+,只分析父表自身

在 18 之前,对父表执行 ANALYZE 只分析父表本身;升级后行为发生了变化,依赖旧行为的脚本需要检查。

常见执行形态 ​

形态写法用途
全库分析ANALYZE;日常维护,最快也最省事
指定表ANALYZE orders;批量写入后立即刷新
指定列ANALYZE orders (amount, status);只关心某些热点列
带细节ANALYZE (VERBOSE) orders;排查采样量是否异常
不等待锁ANALYZE (SKIP_LOCKED) orders;避免被长事务挡住
与清理合并VACUUM (ANALYZE) orders;一次扫描做两件事
sql
-- 大批量数据导入后的标准收尾
VACUUM (ANALYZE) orders;
text
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

这一行输出是理解采样机制的关键:

text
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_target1001 ~ 10000全局默认统计目标
autovacuum_analyze_threshold500 ~ 2147483647触发自动分析的基数项
autovacuum_analyze_scale_factor0.10 ~ 100触发自动分析的随行数增长项
vacuum_buffer_usage_limit2MB128kB ~ 16GB,0 禁用ANALYZE / VACUUM 的缓冲区上限
track_countson—是否收集统计计数(关闭则 autovacuum 完全失效)
sql
-- 查看当前值
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 不是「越精确越好」

它同时决定三样东西的规模:

text
default_statistics_target = N 时
├── 直方图(histogram)最多 N 个桶
├── MCV(most common values)列表最多 N 个条目
└── 每组关联统计(扩展统计)的规模随之增长

N 增大 → 估算精度提升
       → ANALYZE 耗时增加(采样行数随之增加,还要排序)
       → pg_statistic 占用空间增加(每列最多增加数 KB)
       → 规划器读统计的 CPU 开销略增

默认 100 对多数场景够用。真正需要提高精度的通常只有少数几列,应当用列级 SET STATISTICS 精确处理,而不是把全局值调大——全局调大等于给所有表的每一列都增加成本。

列级统计目标 ​

sql
-- 只给热点列提高精度,其余列保持默认
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;
text
   attname    | attstattarget
--------------+---------------
 id           |            -1     ← -1 表示"使用全局默认值"
 status       |          1000
 customer_id  |           500
 amount       |            -1
 created_at   |            -1
sql
-- 恢复为全局默认
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 施加了缓冲区限制:

sql
-- 指定本次分析最多使用 64MB 缓冲区
ANALYZE (BUFFER_USAGE_LIMIT '64MB') big_table;

规则:

text
BUFFER_USAGE_LIMIT 的行为

未指定    → 使用 vacuum_buffer_usage_limit(默认 2MB,上限为 shared_buffers 的 1/8)
0         → 禁用限制(可自由使用共享缓冲区,会污染缓存,慎用)
最小 128kB,最大 16GB

详细说明 ​

采样与统计目标的关系 ​

把采样量的决定逻辑画出来:

text
ANALYZE 采样量是怎么定的

① 对每个被分析的列,取它的 statistics target:
     列级 attstattarget(若非 -1) > 否则 default_statistics_target

② 取这些目标值中的【最大值】:T = max(targets)

③ 采样行数与 T 成正比(具体倍数由实现决定,不是官方固定的常数)

④ 采样后按列排序,据此构建:
     MCV 列表(最多 T 条)
     直方图(最多 T 个桶)
     n_distinct / null_frac / correlation 等标量

结论:只要【任一列】的 target 很高,【整表】的采样量都会提高。
      这是列级 STATISTICS 的一个副作用,调优时要知道。
text
示例:一张有 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 用一条「基数 + 比例」的公式决定是否对某表做分析:

text
触发自动 ANALYZE:

    被修改元组数 > autovacuum_analyze_threshold
                  + autovacuum_analyze_scale_factor × 表估算行数

代入默认值(threshold = 50,scale_factor = 0.1):

    被修改元组数 > 50 + 0.1 × reltuples

把这个公式代进具体规模,就能看出它在不同表上的「灵敏度」:

表估算行数触发所需的修改行数占表比例
1,00015015%
10,0001,05010.5%
100,00010,05010%
1,000,000100,05010%
10,000,0001,000,05010%
100,000,00010,000,05010%
text
读法:
- 小表(几百行)由 threshold = 50 主导,变化 50 行左右就会触发
- 大表(上万行以上)由 scale_factor = 0.1 主导,恒定要变化 10% 才触发

问题:对 1 亿行的表,"变化 10%" = 1000 万行。
      这意味着这张表的统计可能长期滞后 1000 万行,
      而 1000 万行的数据变化足以让分布彻底改观。
→ 所以大表通常需要按表调低 scale_factor。
sql
-- 大表的常见做法:降低比例项,并显式给个下限
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 列:

sql
SELECT schemaname, tablename, attname, inherited, n_distinct
FROM pg_stats
WHERE tablename = 'events'
ORDER BY attname, inherited;
text
 schemaname | tablename | attname | inherited | n_distinct
------------+-----------+---------+-----------+------------
 public     | events    | status  | f         |          5    ← 仅父表自身的数据
 public     | events    | status  | t         |          5    ← 父表 + 所有子表汇总

inherited = f 表示「只统计父表本身的数据」,inherited = t 表示「汇总包括所有子表的数据」。规划器在查询父表(包含子表)时用后者,在 SELECT ... FROM ONLY parent 时用前者。

text
分区/继承场景的三条实用结论

① 手工 ANALYZE 父表会同时更新两套统计(inherited = f / t)
② autovacuum 不会因为"子表变了"去分析父表
   → 父表统计长期不更新 → 查父表(跨分区)的计划变差
③ 所以:分区表的父表必须【手工定期 ANALYZE】
sql
-- 分区表的父表必须手工分析(建议纳入日常调度)
ANALYZE parent_partitioned_table;

-- 18+ 可以精确控制范围
ANALYZE ONLY parent_table;      -- 只分析父表自身
ANALYZE parent_table;           -- 分析父表及所有子表(18+ 默认行为)

外部表(Foreign Table) ​

sql
-- 外部表不会自动分析,必须手工执行(且必须显式指定表名)
ANALYZE foreign_table_name;

官方文档明确指出:autovacuum 不会对外部表发起 ANALYZE,因为它无法判断这样做是否有用。如果查询外部表需要统计信息才能得到好计划,就必须自己安排定时分析。

观察分析进度 ​

ANALYZE 有自己的进度视图 pg_stat_progress_analyze:

sql
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;
text
  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 的取值:

text
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)有自己的刷新机制:

text
累积统计的可见性延迟

① 每个后端进程把计数写进共享内存,
   默认最多每 1 秒(PGSTAT_MIN_INTERVAL)刷一次
② 事务内重复读取默认走缓存(stats_fetch_consistency = cache)
③ 若需要"最新值",可在事务内调 pg_stat_clear_snapshot(),
   或把 stats_fetch_consistency 设为 snapshot / none

结论:看到"统计没更新"时,先别急着怀疑 ANALYZE 失败,
      可能只是累积统计还没刷新到共享内存。
sql
-- 强制丢弃缓存快照,读到最新累积统计
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 列查询频繁且分布倾斜,最近多条查询的计划不稳定。

第一步:先确认「不准」是新鲜度问题还是精度问题

sql
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';
text
 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+08

stale_pct 27%,且自动分析在 9 月 27 日刚跑过——说明新鲜度不是主因,精度可能有问题。

第二步:看分布倾斜程度

sql
SELECT attname, n_distinct, null_frac, correlation
FROM pg_stats
WHERE schemaname = 'public' AND tablename = 'orders' AND attname = 'status';
text
 attname | n_distinct | null_frac | correlation
---------+------------+-----------+-------------
 status  |          6 |         0 |    -0.021

只有 6 个不同值,看起来很简单——但问题正在这里:值很少时,直方图起不了作用,统计完全依赖 MCV 列表。如果分布极度倾斜,而 MCV 列表只有 100 条(对 6 个值其实够),真正的风险是规划器把某个值的选择率估错。查 MCV:

sql
SELECT most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status';
text
   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 刷新,再针对这一列适度提高目标以覆盖边界情况。

sql
-- 1) 立即刷新
ANALYZE (VERBOSE) orders;

-- 2) 给这一列一个稍高的目标,保证 MCV 覆盖更完整
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500;
ANALYZE (VERBOSE) orders (status);
text
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 万行)。

sql
ALTER TABLE orders SET (
  autovacuum_analyze_scale_factor = 0.02,
  autovacuum_analyze_threshold = 5000
);

改完触发条件变成 5000 + 0.02 × 行数,即约 6 万行变化就会自动分析,远好于原来的 30 万行。

第五步:验证效果

sql
-- 用实际行数对照估算行数(在 psql 中执行 EXPLAIN ANALYZE)
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM orders WHERE status = 'failed';
text
 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 上有跨分区查询。查询计划越来越差,但每个分区自己看起来都正常。

第一步:确认父表的统计日期

sql
SELECT relname, n_live_tup, n_mod_since_analyze,
       last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'events';
text
 relname | n_live_tup | n_mod_since_analyze | last_analyze | last_autoanalyze
---------+------------+---------------------+--------------+------------------
 events  |          0 |                   0 |              |                 ← 父表自身没有数据,也从未被分析

注意 n_live_tup = 0:分区表的父表通常是空的(数据都在子分区里),所以「父表自身没被修改」,autovacuum 永远不会去分析它。

第二步:确认子分区各自是被分析的

sql
SELECT relname, n_live_tup, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname LIKE 'events_%'
ORDER BY relname DESC LIMIT 6;
text
   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
 ...

子表都在正常自动分析。问题只在于父表统计从未收集。

第三步:手工分析父表

sql
-- 建立父表的统计数据(PG 18 默认会连同子表一起分析)
ANALYZE events;
text
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
sql
-- 确认继承统计已存在(inherited = t)
SELECT attname, inherited, n_distinct
FROM pg_stats WHERE tablename = 'events' AND attname = 'status';
text
 attname | inherited | n_distinct
---------+-----------+------------
 status  | f         |          5     ← 父表自身(空表,无意义)
 status  | t         |          5     ← 父表 + 全部子表(规划器查跨分区时用这个)

第四步:把它纳入日常调度

父表统计不会自动更新,必须靠定时任务。推荐按「每天一次 + 分区结构变化时」执行:

sql
-- 建议加入每日调度(例如 pg_cron 或系统 crontab + psql)
ANALYZE events;
bash
# 用系统 cron 每天凌晨执行
# 0 3 * * *  psql -d mydb -c 'ANALYZE events;' >> /var/log/pg-analyze.log 2>&1

第五步:监控「父表统计陈旧」

sql
-- 找出所有分区/继承父表:自身无数据但子表有数据
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。

sql
-- 错误: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 等互斥,但与普通读写兼容。

sql
-- 不想被别的维护操作挡住,加 SKIP_LOCKED 快速跳过
ANALYZE (SKIP_LOCKED) orders;

SKIP_LOCKED 的含义是「拿不到锁就跳过这张表,不做该表」,不是「降级锁」。批量分析时它能让整体任务不被某张被锁的表拖死。

3. 一次 ANALYZE 只更新被分析列/表的统计,其余保持原值

指定列分析时,未列出的列统计不会更新(保留旧值)。这在「只想刷某一列」时很方便,但也容易造成新旧统计混用。

sql
-- 只刷新 status 列,其他列的统计保持原样
ANALYZE orders (status);

4. 空表不会被写入新统计

官方文档:如果表被判定为空,不会记录新统计,而是保留原有统计。这带来一个反直觉的后果:把表清空后不 ANALYZE,规划器仍按「旧的行数」估算。

sql
DELETE FROM staging_table;
-- 表已空,但规划器仍认为它有很多行
SELECT reltuples FROM pg_class WHERE oid = 'staging_table'::regclass;

需要让规划器知道表空了,得手工分析(或 TRUNCATE,它会更新 reltuples)。

sql
TRUNCATE staging_table;     -- reltuples 立即归零
-- 或者
DELETE FROM staging_table; ANALYZE staging_table;

5. 提高 statistics_target 有副作用,别全局调大

全局调大 default_statistics_target 会让所有表的所有列成本上升:ANALYZE 更慢、pg_statistic 更大、规划开销更高。按列设置才是正解。

sql
-- 不推荐:全局一刀切
ALTER SYSTEM SET default_statistics_target = 1000;

-- 推荐:只给需要的列
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;

6. n_mod_since_analyze 不是「未分析的行数」

它是自上次分析以来被插入/更新/删除的元组数,是一种「变化量」计数,与表中当前有多少行无关。

sql
-- 用它算"陈旧度",不要当成"待分析行数"
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 时它们无法工作。

sql
SHOW track_counts;    -- 必须为 on

8. 不要对超宽表的所有列都设高 target

一张有 200 列、每列都设 target = 1000 的表,ANALYZE 要为 200 列各做一次大规模排序,成本极高,而收益可能为零(大部分列根本没出现在 WHERE 里)。

sql
-- 找出被设了高 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 看作「执行计划的前置条件」,纳入例行维护

统计过时是「计划突然变差」最常见的原因,而它又完全不报错。最低限度要保证:

text
例行分析策略
├── 让 autovacuum 正常工作(不要关闭)
├── 大表按表调低 autovacuum_analyze_scale_factor(0.02 ~ 0.05)
├── 分区/继承父表纳入定时任务手工 ANALYZE
├── 外部表按需手工 ANALYZE
├── 批量导入/迁移后立即手工 ANALYZE
└── 建立 n_mod_since_analyze / n_live_tup 的告警

2. 用「估算误差」而不是「感觉」来决定是否调参

判断某列统计够不够准,标准做法是拿 EXPLAIN 的估算行数和 EXPLAIN ANALYZE 的实际行数对比:

sql
-- 估算与实际差一个数量级,才值得调 target
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE status = 'failed' AND region = 'east';
text
读法:
  估算 rows=1200  实际 rows=118000   → 差 100 倍,需要处理
  估算 rows=118000 实际 rows=120400  → 差 2%,无需处理

3. 优先调列级 SET STATISTICS,不动全局默认

sql
-- 只给真正需要的列提高精度
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. 把「父表统计陈旧」做成一条专门的告警

分区表的父表永远不会被自动分析,这是最容易被漏掉的一类问题。

sql
-- 找出所有从未被手工分析过的分区/继承父表
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;
text
判据:一旦 children > 0 且 last_analyze IS NULL,
就应该立即把这张父表加入每日 ANALYZE 调度。

5. 批量写入后「先分析再查询」,而不是「等自动分析」

自动分析的阈值对大表而言太迟钝(10%)。数据导入、批量更新、ALTER TABLE 加列回填之后,手工分析一次比等 autovacuum 划算得多。

sql
-- 数据加载脚本的收尾三件套
VACUUM (ANALYZE) orders;
VACUUM (ANALYZE) order_items;
ANALYZE customers;

6. 给维护动作加超时保护,避免拖住其他操作

ANALYZE 与 VACUUM、CREATE INDEX CONCURRENTLY 互斥。一个长跑的 ANALYZE 会阻塞后续维护,甚至让 CREATE INDEX CONCURRENTLY 失败。

sql
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 秒)后,超过阈值的维护动作都会被记录,包括「分析花了多久、采样的行数、估算的总行数」。

sql
ALTER SYSTEM SET log_autovacuum_min_duration = 1000;
SELECT pg_reload_conf();
text
日志中会看到形如:
LOG:  automatic analyze of table "mydb.public.orders"
      system usage: CPU: user: 0.42 s, system: 0.11 s, elapsed: 1.83 s

8. 把「统计新鲜度」与「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。

sql
-- 18 之前的语义(升级后需要显式加 ONLY 才能保持)
ANALYZE ONLY parent_table;

10. 每季度复核一遍统计配置

统计配置会随表与查询的演进逐渐失配。

text
季度复核清单
├── 是否有表的 n_mod_since_analyze / n_live_tup 长期 > 20%
├── 是否有分区/继承父表的 last_analyze 为空
├── 是否有外部表从未被手工 ANALYZE
├── 是否有列被设了高 attstattarget 但已不再需要
├── 是否有大表仍在使用默认 scale_factor = 0.1
└── 是否有查询的估算行数与实际行数长期差一个数量级以上

下一节

继续学习:直方图与相关性统计

参考链接 ​