跳到主要内容

PostgreSQL 统计信息与查询优化器

查询优化器是 PostgreSQL 的大脑,它依赖统计信息估算每种执行路径的代价,进而选出最优计划。理解统计信息的工作机制,是解决"为何优化器选错计划"的根本。


一、统计信息存储结构

1.1 pg_statistic 与 pg_stats

-- pg_statistic:原始统计数据(底层系统表)
SELECT * FROM pg_statistic
WHERE starelid = 'orders'::regclass
AND staattnum = (SELECT attnum FROM pg_attribute
WHERE attrelid = 'orders'::regclass
AND attname = 'status');

-- pg_stats:可读性更好的视图(推荐使用)
SELECT
attname AS 列名,
n_distinct AS 唯一值估算, -- 正数=绝对数量, 负数=占总行数比例
null_frac AS NULL占比,
avg_width AS 平均字节宽度,
most_common_vals AS 高频值MCV,
most_common_freqs AS 高频值频率,
histogram_bounds AS 直方图边界
FROM pg_stats
WHERE tablename = 'orders'
AND attname = 'status';

1.2 关键统计字段解读

字段含义对优化器的作用
n_distinct唯一值数量(-1 = 全部唯一)估算等值查询的选择率
null_fracNULL 值占比(0.0~1.0)影响 IS NULL 过滤的代价
most_common_vals (MCV)出现最频繁的值列表精确估算高频值的选择率
most_common_freqsMCV 各值的频率配合 MCV 计算选择率
histogram_bounds直方图边界(等频直方图)估算范围查询的选择率
correlation物理顺序与逻辑顺序的相关性(-1~1)影响索引扫描的代价估算
-- correlation 接近 1 → 数据按该列物理有序 → Index Scan 随机 I/O 少,代价低
-- correlation 接近 0 → 数据随机分布 → Index Scan 需要大量随机 I/O,优化器倾向 Seq Scan
SELECT attname, correlation
FROM pg_stats
WHERE tablename = 'orders'
ORDER BY abs(correlation) DESC;

二、ANALYZE 触发机制

-- 手动触发(更新全表统计)
ANALYZE orders;

-- 只更新特定列(减少时间)
ANALYZE orders (user_id, status, created_at);

-- VACUUM + ANALYZE 一起执行
VACUUM ANALYZE orders;

-- 查看统计信息更新时间
SELECT relname, last_analyze, last_autoanalyze, n_mod_since_analyze
FROM pg_stat_user_tables
WHERE relname = 'orders';
-- n_mod_since_analyze 高 → 自上次 ANALYZE 后修改行数多,统计信息可能过时

2.1 Autovacuum 自动触发 ANALYZE

# 触发条件:修改行数 > threshold + scale_factor × reltuples
autovacuum_analyze_threshold = 50 # 最少 50 行变化才触发
autovacuum_analyze_scale_factor = 0.05 # 默认 0.2,大表建议调小(如 0.01)

# 针对高频写入的表单独调整
ALTER TABLE orders SET (
autovacuum_analyze_scale_factor = 0.01,
autovacuum_analyze_threshold = 100
);

2.2 statistics_target — 统计精度

default_statistics_target 控制 MCV 列表和直方图的桶数(默认 100):

-- 提高特定列的统计精度(适合基数高、范围查询多的列)
ALTER TABLE orders ALTER COLUMN created_at SET STATISTICS 500;
-- 然后重新 ANALYZE
ANALYZE orders (created_at);

-- 查看各列当前 statistics target
SELECT attname, attstattarget
FROM pg_attribute
WHERE attrelid = 'orders'::regclass
AND attnum > 0
ORDER BY attnum;
-- attstattarget = -1 → 使用全局 default_statistics_target

三、选择率估算原理

优化器通过统计信息计算选择率(Selectivity),即查询条件过滤后剩余行数的比例。

3.1 等值查询选择率

-- 对于 WHERE status = 'pending'
-- 优化器查 MCV 列表:
-- most_common_vals = {'pending', 'completed', 'cancelled'}
-- most_common_freqs = {0.35, 0.50, 0.15}
-- 选择率 = 0.35,估算行数 = total_rows × 0.35

-- 如果 status 值不在 MCV 中:
-- 选择率 = (1 - sum(MCV频率)) / (n_distinct - len(MCV))

3.2 范围查询选择率(直方图)

-- 对于 WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31'
-- 优化器用直方图边界 histogram_bounds 线性插值估算落在该范围的比例
-- histogram_bounds 桶越多(statistics_target 越大),估算越精准

3.3 多列联合选择率的误差

优化器默认假设各列相互独立,将多个条件的选择率相乘:

-- WHERE status = 'pending' AND region = 'north'
-- 独立假设:selectivity = 0.35 × 0.25 = 0.0875
-- 实际:如果 pending 订单主要来自 north 区,实际选择率可能是 0.30
-- → 严重低估,优化器可能选错计划

四、扩展统计(Extended Statistics)

针对多列相关性问题,PostgreSQL 10+ 提供扩展统计:

-- 1. 创建多列统计(捕获列间相关性)
CREATE STATISTICS orders_status_region ON status, region
FROM orders;

-- 触发 ANALYZE 收集扩展统计
ANALYZE orders;

-- 2. 查看扩展统计
SELECT stxname, stxkeys, stxkind
FROM pg_statistic_ext
WHERE stxrelid = 'orders'::regclass;
-- stxkind: d=ndistinct, f=功能依赖, m=MCV

-- 3. 验证效果:对比创建前后的估算行数
EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND region = 'north';
-- rows 应更接近实际值

五、干预优化器:调试参数

当优化器选错计划时,可通过参数临时关闭某种执行策略来诊断:

-- 关闭 Seq Scan,强制走索引(诊断用,不要长期设置)
SET enable_seqscan = off;
EXPLAIN SELECT * FROM orders WHERE user_id = 42;

-- 关闭 Hash Join,强制 Nested Loop
SET enable_hashjoin = off;
SET enable_mergejoin = off;

-- 关闭并行查询
SET max_parallel_workers_per_gather = 0;

-- 调整 SSD 的随机 I/O 代价(让优化器更积极使用索引)
SET random_page_cost = 1.1;
SET effective_cache_size = '24GB';

-- 恢复所有设置
RESET ALL;

-- 会话级别设置(不影响其他连接)
SET LOCAL enable_seqscan = off; -- 只在当前事务有效

5.1 plan_cache_mode — 参数化查询计划

-- 对于 prepared statement,PostgreSQL 5次后切换为 Generic Plan
-- 如果 Generic Plan 比 Custom Plan 差很多:
SET plan_cache_mode = force_custom_plan; -- 每次都用参数值估算
SET plan_cache_mode = force_generic_plan; -- 始终用通用计划

-- 查看 prepared statement 的计划
SELECT name, statement, prepare_time, calls, generic_plans, custom_plans
FROM pg_prepared_statements;

六、统计信息诊断工作流

-- 第一步:确认统计信息是否过时
SELECT relname,
n_mod_since_analyze,
last_autoanalyze,
pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_stat_user_tables
WHERE n_mod_since_analyze > 10000
ORDER BY n_mod_since_analyze DESC;

-- 第二步:查看估算 vs 实际差距
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE status = 'pending';
-- "rows=XXX" (估算) vs "actual rows=YYY"
-- 差距 > 10x → 统计信息严重失准

-- 第三步:检查直方图桶数是否足够
SELECT attname, attstattarget,
array_length(histogram_bounds, 1) AS histogram_buckets
FROM pg_stats
WHERE tablename = 'orders'
ORDER BY attname;

-- 第四步:更新统计并重新检查
ANALYZE orders;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE status = 'pending';