PostgreSQL 14 多表 JOIN 行数估计严重偏差源于统计信息不足:默认 STATISTICS_TARGET=100 无法捕获倾斜值和列相关性,需对倾斜列提高目标值、为组合条件建多列统计,并为函数表达式单独收集统计,最后用 EXPLAIN (ANALYZE, BUFFERS) 验证效果。

PostgreSQL 14 中多表 JOIN 估计行数严重偏差,基本可以断定是统计信息没覆盖真实分布——不是优化器坏了,是它“看错了数据”。
为什么 ANALYZE 后 Planner 还乱估?关键在统计目标和列相关性
PostgreSQL 默认对每列只收集有限统计(STATISTICS_TARGET = 100),对倾斜值(比如 status = 'pending' 占 98%)或组合条件(WHERE region = 'CN' AND category = 'A')完全无感。更麻烦的是:它默认不记录列间相关性,两个独立估算相乘(0.01 × 0.05 = 0.0005),实际联合选择率可能是 0.03,误差达 60 倍。
- 用
SELECT attname, n_distinct, most_common_vals FROM pg_stats WHERE tablename = 'orders' AND attname IN ('region', 'category');查是否真有高频值被忽略 - 若
most_common_vals为空或长度极短,说明采样不足,STATISTICS_TARGET太低 - 检查是否建了多列统计:
\dS+ orders看有没有pg_statistic_ext条目;没有就需手动创建
如何为倾斜列和组合条件补统计?
不能只跑 ANALYZE table_name —— 它按默认采样率扫,对长尾无效。必须精准干预:
- 对单列倾斜(如
status):执行ANALYZE orders (status) WITH (STATISTICS_TARGET = 1000);,把目标提至 1000 强制抓更多频次分布 - 对高频组合(如
region+category):先建扩展统计CREATE STATISTICS s_orders_region_cat ON region, category FROM orders;,再ANALYZE orders; - 避免过度:单列
STATISTICS_TARGET > 2000会显著拖慢ANALYZE时间;组合统计只建真正高频过滤的 2–3 列,别全量覆盖
表达式和函数条件怎么让 Planner “看得见”?
像 WHERE date_trunc('month', created_at) = '2024-01-01' 这种,Planner 根本不会用 created_at 的原始统计,直接按 0.1%~1% 猜——因为它不知道这个表达式的分布。
- 建函数索引:
CREATE INDEX idx_orders_month ON orders (date_trunc('month', created_at)); - 再为该表达式单独收集统计:
ANALYZE orders ((date_trunc('month', created_at)));(注意括号写法) - 验证是否生效:查
pg_stats里attname是否出现date_trunc开头的虚拟列名 - 别依赖
EXPLAIN的预估行数判断——重点对比actual rows和rows=xxx,比值超 5 就得调
JOIN 节点本身放大误差?盯紧 Nested Loop 和 Rows Removed by Join Filter
上游估算偏差 3 倍,到 Nested Loop Join 可能变成 30 倍以上。PostgreSQL 14 的执行计划里,Nested Loop (Join Filter: true) 或 Rows Removed by Join Filter: 0 是危险信号:说明 Planner 没找到高效连接路径,被迫全量匹配。
- 确认驱动表是否合理:小表做外层,大表做内层;如果
EXPLAIN显示大表在左、小表在右,且内层扫描次数极高,说明顺序被误判 - 临时禁用重排:设
SET join_collapse_limit = 1;,强制按你写的顺序执行,有时反而更稳(尤其当右表已加好索引) - 检查连接字段类型是否一致:
orders.user_id是BIGINT,但users.id是TEXT?隐式转换会让索引失效,Planner 只能退化成嵌套循环 - 别信
EXPLAIN的rows字段——它只是估算;真正要看的是actual rows和Buffers: shared read=xxx,读盘高 + 实际行远大于预估,基本坐实统计失准
最常被跳过的一步:改完统计后,一定要用 EXPLAIN (ANALYZE, BUFFERS) 重跑原查询,对比每个节点的 actual rows 是否收敛到合理范围。统计不是一劳永逸,数据持续写入后,定期触发 ANALYZE(或开 autovacuum 的 stats target 自适应)才真正闭环。

















