跳到内容

8.2 基数估算、成本模型与执行计划诊断

一条查询昨天只跑几十毫秒,今天却因为换了连接顺序超时;执行计划里最先失真的,是中间结果行数。

优化器最难的不是知道有哪些算法,而是在执行前估算每个中间 relation 有多少 rows。一次 100 倍的 cardinality error 会沿 join tree 放大,让原本适合小 outer 的 nested loop 被用于百万 rows,或让 hash table 严重低估 memory。

Optimizer 选择什么

physical alternatives 包括:

  • access path:sequential/index/bitmap/partition scan;
  • join order 与 join algorithm;
  • predicate placement;
  • aggregation/sort implementation;
  • parallelism、partition exchange、materialization;
  • required/provided order;
  • remote pushdown;
  • JIT/compiled execution thresholds。

optimizer 不一定枚举所有可能 plans。search space 随 joins 增长迅速,系统会用 dynamic programming、memo/cascades、heuristics、join-order restrictions 或 genetic/randomized search 剪枝。

Cost 不是毫秒

PostgreSQL plan 中:

text
cost=startup_cost..total_cost
rows=estimated_output_rows
width=estimated_average_row_bytes

cost 是按配置参数加权的抽象单位,用来比较 alternatives,不直接等于 wall-clock milliseconds。常见组成是 page I/O、tuple/operator CPU、parallel setup/transfer、sort/hash 等模型。

seq_page_costrandom_page_cost 等默认与意义会随版本/配置变化。不能写“SSD 就把 random_page_cost 固定设成 1.1”。这些参数还隐含 cache 与整个 workload 的相对代价,需要 benchmark 和 plan regression 验证。

Selection cardinality

optimizer 可能使用:

  • table row/page estimate;
  • NULL fraction;
  • number of distinct values;
  • most-common values/frequencies;
  • histogram bounds;
  • physical correlation;
  • expression/index statistics;
  • constraints/partition bounds。

等值 predicate 对 MCV 可用直接频率;不在 MCV 的 value 常按 remaining distinct distribution 估算。range predicate 用 histogram interpolation。它们都是 sample/model estimates,不是运行 query 得到的真值。

Correlated columns

sql
WHERE country = 'CN'
  AND city = 'Shanghai'

若优化器假设 independence:

text
selectivity(country='CN') × selectivity(city='Shanghai')

会低估相关组合。PostgreSQL extended statistics 可记录 dependencies、MCV combinations 或 ndistinct:

sql
CREATE STATISTICS stats_country_city
    (dependencies, mcv, ndistinct)
ON country, city
FROM addresses;

ANALYZE addresses;

support 与适用 predicate 要查当前版本。extended stats 不会神奇解决跨表 correlation 或所有 expression。

Join cardinality

简化 equijoin estimate 可能依赖两侧 rows、distinct counts、MCV 与 uniqueness:

text
|R ⋈ S| ≈ |R| × |S| / max(ndistinct(R.k), ndistinct(S.k))

这是假设均匀/containment 的粗略公式。skew、partial overlap、NULL、composite key correlation 和 filters 会破坏它。PK/FK/UNIQUE constraints 能提供强证据,所以 constraints 也帮助 optimization,不只负责 integrity。

Statistics 为什么过时

  • bulk load 后未 analyze;
  • rapidly changing/partitioned table;
  • sampling 没捕获 rare skew;
  • prepared statement parameters 分布不同;
  • expression/correlation 未建 statistics;
  • temporary table lifecycle;
  • remote source 没提供准确 estimates。

“突然慢的头号原因就是没 ANALYZE”过度武断。I/O、lock、plan cache、data growth、bloat、network 与 dependency latency 都可能是根因。

inner joins 常可重排;outer/semi/anti/lateral joins 和 volatile semantics 会限制 legal orders。即使三张表有多种 parenthesization,commutativity 还会产生重复表示,optimizer 用 equivalence classes/memo 避免盲目全排列。

PostgreSQL 对较小 join problem 使用 exhaustive/dynamic-programming style search,对超过 geqo_threshold 的情况可启用 GEQO。GEQO 不是 PostgreSQL 12 才出现,具体 threshold/default 以当前配置为准。

把 15-table query 手工拆成 temp tables 可能降低搜索空间,也可能丢失 pushdown/reordering、增加 I/O 和改变 transaction semantics。先用 plan evidence 判断。

Prepared statements:custom 与 generic plan

同一 SQL 的最佳 plan 可能依赖 parameter:

sql
SELECT * FROM events WHERE tenant_id = $1;

小 tenant 适合 index scan,大 tenant 可能适合 sequential/bitmap scan。系统可在每次根据 parameter 生成 custom plan,或复用 generic plan 节省 planning time。

parameter sniffing/generic-plan regression 的表现是某些 values 快、某些慢。诊断要记录 parameter distribution 与 plan choice,不要把 literal 拼进 SQL 规避 plan cache并引入 injection。

阅读 EXPLAIN ANALYZE

PostgreSQL 安全的 read query 示例:

sql
EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS, TIMING OFF, SUMMARY ON)
SELECT a.adventurer_id, COUNT(*)
FROM adventurers AS a
JOIN quests AS q
  ON q.adventurer_id = a.adventurer_id
WHERE q.status = 'active'
GROUP BY a.adventurer_id;

ANALYZE 实际执行 statement。对 INSERT/UPDATE/DELETE 使用前应在 transaction 中 ROLLBACK 或在复制环境分析,并考虑 triggers/external side effects。

首先比较 estimated 与 actual rows

找最早出现数量级偏差的 node,而不是只看 root。上游错误会传递到 parent。

loops

PostgreSQL 的 actual timerows 通常是 per-loop averages;总 rows/work 需结合 loops。parallel plan 中 worker details 和 gathered rows 也要一起看。

Buffers

  • shared hit/read/dirtied/written;
  • temp read/written;
  • local buffers;
  • I/O timing(若配置收集)。

buffer hit 不等于零成本,read 不一定等于 device miss;OS cache 与 async I/O 仍在下层。

Rows Removed

大量 rows removed by filter 表明 access path 读了许多后过滤,但不自动意味着“加一个单列 index”。需要评估 composite/partial index、selectivity、write cost 与 query importance。

Sort/Hash

查看 sort method、memory、disk;hash buckets/batches/memory。batches > 1 常表示 spill/repartition,但版本输出字段不同。

一套诊断顺序

  1. 保存 SQL、parameters、schema/index、数据库版本与 plan settings;
  2. 确认等待时间:CPU、I/O、lock、client/network;
  3. 获取带 actual rows/buffers 的安全 plan;
  4. 找第一个 cardinality 大偏差;
  5. 检查 predicate semantics、stats、correlation、constraints;
  6. 检查 operator resource:spill、loops、heap fetch、parallel skew;
  7. 形成最小 change:stats/index/query/schema/config;
  8. 用代表 parameters 和冷/热 cache对照;
  9. 观察写入成本、其他 queries 与并发 guardrails。

Query rewrite 的边界

  • EXISTS 可表达 semijoin,避免 join duplicate;
  • NOT EXISTS 更安全表达 nullable anti-join;
  • predicate pushdown 要尊重 outer join/NULL;
  • function/cast 包住 indexed column 可能阻止普通 index condition,可用 expression index 或改写;
  • CTE 是否 inline/materialize 取决于产品版本与语法选项;
  • OR 可用 bitmap OR、union 或 scan,手改 UNION ALL 要处理 duplicate semantics。

任何 rewrite 都先证明结果等价,再比较 plan。

Remote/FDW

remote source optimization 受限于 connector 能下推的 filter/join/aggregate、remote statistics、network latency 和 transaction semantics。optimizer 看到的 local cost 可能缺少 remote queue/cache/skew。

检查 plan 中哪些操作在 remote 执行、传回多少 rows/bytes,而不是笼统说“军师管不到远程表”。

验收清单

  • [ ] 不把 cost 当毫秒;
  • [ ] 找最早 cardinality 偏差,不只看 root;
  • [ ] actual rows/time 结合 loops;
  • [ ] 统计能描述单列/扩展相关性,但不是实时真值;
  • [ ] prepared statement 同时测试代表 parameters;
  • [ ] EXPLAIN ANALYZE 的副作用已控制;
  • [ ] 优化后检查写成本、并发和其他 plans。

本章小结

执行器决定“怎样做”,优化器依据不完整 statistics 估计“哪种做法更便宜”。最重要的诊断信号通常是 intermediate cardinality 与实际资源,而不是某个 node 名字看起来是否高级。下一章进入 query compilation:如何减少解释器/iterator overhead,并把表达式变成更贴近 CPU 的代码。

官方资料入口

Built with VitePress | Software Systems Atlas