5.2 代价模型与路径生成:规划器如何算账


5.2 代价模型与路径生成:规划器如何算账

本节摘要:规划器用一套统一记账比较候选计划:顺序读一页记 1、随机读一页记 4、处理一行 CPU 记 0.01 量级(seq_page_cost、random_page_cost、cpu_tuple_cost 等参数)。所有代价估算的根是行数估算,而行数来自统计信息。估算一偏,全链条跟着偏——多数慢查询的病根不是模型笨,是统计旧。

记账货币与单位换算

EXPLAIN 里 cost=0.00..158.00 的单位不是秒,是"顺序读一页"的代价倍数:

参数 默认值 含义
seq_page_cost 1.0 顺序读一页,基准货币
random_page_cost 4.0 随机读一页(机械盘经验值)
cpu_tuple_cost 0.01 处理一行的 CPU
cpu_index_tuple_cost 0.005 处理一条索引条目
cpu_operator_cost 0.0025 一次操作符或函数求值

第一课就藏在这里:random_page_cost=4 是机械硬盘时代遗产。全闪存上随机读与顺序读差距很小,调到 1.1 左右,规划器才会大胆选索引扫描——很多"SSD 机器上为什么不用索引"的悬案,答案就在这一行参数。

-- 索引扫描为什么没被选上?对比两种路径的账本 EXPLAIN SELECT * FROM orders WHERE customer_id = 88;
Seq Scan on orders (cost=0.00..1875.00 rows=3 width=64) Filter: (customer_id = 88)

全表一千页,代价记 1875;索引扫描要随机读约 4 页记 16 加少量 CPU——本该赢,除非统计让规划器以为该客户有一万条订单(随机页会到 4 万),于是顺序扫描反而"便宜"。规划器没有错,是它的世界地图过时了

行数估算:一切代价的地基

-- 规划器眼中的这张表 SELECT attname, n_distinct, most_common_vals, histogram_bounds FROM pg_stats WHERE tablename = 'orders' AND attname = 'status';

统计记录四类关键事实:总行数(reltuples)、空值比例、高频值及其频率、等频直方图。等值条件查高频值表拿频率,范围条件查直方图按桶插值,组合条件按独立假设相乘——最后一条正是估算翻车的高发地:status='active' AND deleted_at IS NULL 若两列高度相关,相乘算出的行数可能差几个数量级。

路径枚举:组合爆炸与兜底

表少时,规划器穷举所有连接顺序与算法组合(动态规划);表多于约 12 张,切换到遗传算法——先随机生成一批可行顺序,迭代进化出足够好的一个。所以大 JOIN 的计划"不是最优而是够好",偶尔抽到烂顺序也是已知代价。

图:代价决策流与统计依赖

-- 让地图追上领土 ANALYZE orders; -- 数据分布偏斜大时提高采样精度 ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500; ANALYZE orders;

⚠️ 常见坑:大批量导入后忘跑 ANALYZE,规划器还拿着旧统计做决策;autovacuum 的 ANALYZE 阈值(默认插入更新十分之一加五十行)对"一次性灌入两千万行"反应迟钝,导入后手动执行一次是最稳的习惯。

扩展统计:治相关性估算翻车的药

独立性假设在相关列上失真是估算翻车头号来源,解药是 CREATE STATISTICS 教规划器认识列之间的依赖:

-- 例子:城市与邮编高度相关,两列各自过滤后的组合行数被严重低估 CREATE STATISTICS st_city_zip (dependencies, ndistinct) ON city, zip_code FROM customer; ANALYZE customer;

建好后 pg_stats 查不到它的效果,要查扩展统计自己的视图:

SELECT statistics_name, dependencies FROM pg_stats_ext WHERE statistics_name = 'st_city_zip';
statistics_name | dependencies -----------------+-------------------------------------- st_city_zip | {"city => zip_code": 0.98, ...}

0.98 的依赖度意味着知道城市几乎等于知道邮编——规划器从此对这两列的组合过滤给出接近真实的行数。哪类查询受益最明显:多条件组合过滤(相关性)、多列去重计数(ndistinct)、排序列组合(mcv 扩展)。给报表类慢查询做诊断时,"多个过滤条件"与"估算行数离谱"同时出现,就该想到这一味药。

effective_cache_size:最常被误解的参数

它的名字像在分配内存,实际一个字节也不分配——它只是告诉规划器"操作系统加数据库两层缓存大约能装多少页",供索引扫描的代价估算参考。设小了,规划器低估缓存命中、高估索引随机读代价、偏向全表扫;设大了则盲目乐观选索引。合理值约为机器总内存减去 shared_buffers 与系统预留后的一半到三分之二。它是纯"世界观"参数:改它不影响任何资源占用,只影响规划器对世界的想象,这类参数(同族还有 random_page_cost)调错的症状都是"计划不合常理但机器并不忙"。

案例:一次导入后的集体变慢

运维导入八百万行新数据后,整套报表集体劣化两到十倍,机器负载不高、无锁等待。按链条排查:pg_stat_statements 显示慢的全是带 status 过滤的老查询;EXPLAIN 发现估算行数与实际差百倍——统计还是导入前的。机制完整闭环:autovacuum 的分析阈值按比例触发,八百万新行对统计更新的响应滞后,规划器拿着旧直方图把"几乎全表命中"的条件估成"几百行",选了嵌套循环加索引的精巧计划,实际每行都进索引空转。处置只要两行:导入后手动 ANALYZE 三张主表,劣化即时消失。固化成规范:任何批量导入的最后一步是显式 ANALYZE,这条纪律的价值超过一打事后调参。

本节要点回顾

  • 代价是记账不是时间:货币基准是顺序读一页,EXPLAIN 数字可跨计划比较
  • SSD 请调 random_page_cost:4.0 是机械盘遗产,闪存环境 1.1 更贴近现实
  • 行数估算是命门:一切选择都从过滤后行数推演
  • 组合条件易翻车:独立性假设在相关列上失效,用表达式统计或拆查询补救

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U