在 PostgreSQL 的查询优化器(Planner)设计中,所有路径决策(Path Selection)都建立在一个抽象的数学模型之上------Cost(代价模型)。
优化器在决定走全表扫描(Seq Scan)、索引点查(Index Scan)、位图扫描(Bitmap Index Scan)还是哈希连接(Hash Join)时,并不运行代码,也不预测硬件当前的物理毫秒数,而是单纯通过公式累加各个环节的 Cost,选择算出来总 Cost 最低的那条路径。
在所有代价因子中,random_page_cost 是对执行路径形态影响最深远、也最容易被误读的参数之一。
一、 优化器的代价度量衡:它如何计算 Cost?
PostgreSQL 的代价模型定义了两个基础的 I/O 计量原点:
seq_page_cost(默认值:1.0) :
顺序从磁盘或缓存中读取一个 8KB 页面的抽象开销基准。所有其他 I/O 开销都以它作为对比单位。random_page_cost(默认值:4.0) :
非连续寻址随机读取一个 8KB 页面的抽象开销倍数。
除了 I/O,优化器还会加上 CPU 代价(如每处理一个元组的 cpu_tuple_cost = 0.01,评估一个操作符的 cpu_operator_cost = 0.0025)。
当优化器面对一条带有索引的简单过滤查询(如 SELECT * FROM records WHERE domain_id = 12345)时,核心竞争主要发生在两种扫描方式之间:
1. 全表顺序扫描(Seq Scan)的代价公式
优化器认为全表扫描是单向连续读:
Costseq≈(relpages×seq_page_cost)+(reltuples×cpu_tuple_cost)+CPU Filter Cost\text{Cost}_{\text{seq}} \approx (\text{relpages} \times \text{seq\_page\_cost}) + (\text{reltuples} \times \text{cpu\_tuple\_cost}) + \text{CPU Filter Cost}Costseq≈(relpages×seq_page_cost)+(reltuples×cpu_tuple_cost)+CPU Filter Cost
此时 random_page_cost 完全不参与计算。
2. 索引点查(Index Scan)的代价公式
走索引点查分为两段跳跃:
- 索引树本身的检索:自顶向下遍历 B-Tree 节点页面(主要计为少量页面的随机读与比较开销)。
- 回表读取真实数据行(Heap Fetch) :根据索引拿到的
ctid(物理行指针)去表的数据块中取数据。
- 若表数据完全未聚集(Unclustered):行物理分布是散落在各个页面的,每抓取一行,优化器就默认需要发起一次随机页面访问。
- 代价核心项:
Costindex≈Index Tree Cost+(估算匹配的页面数×random_page_cost)+CPU Cost\text{Cost}_{\text{index}} \approx \text{Index Tree Cost} + (\text{估算匹配的页面数} \times \mathbf{random\_page\_cost}) + \text{CPU Cost}Costindex≈Index Tree Cost+(估算匹配的页面数×random_page_cost)+CPU Cost
二、 现场复盘:4.0 与 1.1 下的 Cost 演变推导
以一张包含 1690 万行数据的表、索引点查查询为例:
sql
SELECT * FROM records WHERE domain_id = 12345 LIMIT 10;
优化器通过统计信息预估 domain_id = 12345 全表约有 23 行(rows=23)。
1. 当 random_page_cost = 4.0 时
优化器认为:每去堆表拿一个页面,代价都要按照顺序读的 4 倍来算。
text
-> Index Scan using domain_id on records (cost=0.44..96.56 rows=23 width=591)
- 启动代价(Startup Cost =
0.44):进入 B-Tree 索引根节点并定位到首个叶子节点的起始代价。 - 总代价(Total Cost =
96.56):抓取完 23 行需要读取的索引页与回表页面乘上 4.0 权重后的综合累加。 - Limit 剪枝(Cost =
0.44..42.23) :因为只取 10 行,优化器将总成本按行数比例折算为42.23。
2. 当 random_page_cost = 1.1 时
优化器被告知:随机读取一个页面的惩罚极低(仅比顺序读多 10%)。
text
-> Index Scan using domain_id on records (cost=0.44..27.16 rows=23 width=591)
- **总代价由
96.56骤降到27.16**,降幅达 71.8%。 - **Limit 后的成本由
42.23骤降至12.06**。
在这个简单的场景下,由于 Index Scan 的代价即使被放大到 42.23,依然远低于全表扫描 1690 万行所需的数十万 Cost,因此执行路径的表象形态看似没有改变(都选择了 Index Scan)。
但这只是表象。
三、 random_page_cost 如何在幕后左右执行路径生成
优化器构建执行树的过程是一个自底向上的动态规划比价体系 。random_page_cost 的高低直接决定了以下关键路径决策的临界点(Tipping Point):
[ 相同查询条件: domain_id IN (...) ]
│
┌────────────┴────────────┐
▼ ▼
random_page_cost = 4.0 random_page_cost = 1.1
高估随机寻址成本 抹平随机寻址惩罚
│ │
┌────────┴────────┐ │
▼ ▼ ▼
[Bitmap Heap Scan] [Seq Scan] [Index Scan]
收集TID物理排序 放弃索引暴力扫 直接走B-Tree指针点查
(兼顾顺序I/O) (总Cost更低) (极低Cost直接胜出)
1. Index Scan 与 Seq Scan 的临界点漂移
假设某个查询需要检索的记录数占全表的比例从 0.1% 逐渐上升到 5%:
- 在
4.0下 :回表随机读代价迅速膨胀。当匹配行数达到几千行时,Costindex\text{Cost}_{\text{index}}Costindex 会迅速越过全表顺序扫描的代价线。优化器会认定:"与其在盘上到处乱跳随机找这几千行,不如直接把全表整块顺次扫一遍更划算 "。执行器被迫退化为 Seq Scan(全表扫描)。 - 在
1.1下 :随机读惩罚被拉平,索引点查的 Cost 增长曲线极为平缓。即便匹配行数占比进一步提升,优化器依然坚定地维持 Index Scan。
2. 诱发 Bitmap Index Scan(位图索引扫描)
当 random_page_cost = 4.0 时,优化器为了规避纯粹的随机回表,经常会生成第三种路径:Bitmap Index Scan + Bitmap Heap Scan。
- 内部机制 :优化器先在索引中把所有满足条件的物理行地址(
TID)提取到内存位图中,按物理页面位置进行升序排序,然后再去读取堆表。 - 代价权衡 :排序把原本的离散随机读(乘数 4.0)转化为类似批量的局部顺序读。但构建位图和排序需要额外的 CPU 和内存开销(
work_mem)。 - 参数影响 :若设置
random_page_cost = 1.1,优化器计算得出直接Index Scan已经极其便宜,便不再需要付出构建位图的代价,直接生成更简洁直接的Index Scan。
3. 多表关联算法的重构(Nested Loop vs Hash/Merge Join)
在多表连接场景(如 domains JOIN records ON domains.id = records.domain_id)下,优化器有以下选择:
- Nested Loop(嵌套循环):从驱动表拿一条记录,立即去被驱动表的索引树点查一次。
- **高度依赖
random_page_cost**:点查次数极其频繁。 - 当设为
4.0时,嵌套循环的总代价随外层行数呈倍数急剧放大,优化器会倾向于放弃索引点查,转而选择 Hash Join (在内存构建整张大表的哈希桶)或 Merge Join(双表按键排序后归并)。
- 当设为
1.1时,嵌套循环内部的Index Scan单次成本被压低,Nested Loop 会成为全阶段代价极低的首选方案。
四、 总结:如何理性校准该参数
PostgreSQL 源码将默认值设为 4.0,是历史遗留下来的保守设定,它严格基于机械硬盘时代磁头物理寻道的物理现实。
在当前的存储与架构选型中,调优该参数的核心逻辑不是盲从硬件规格,而是让优化器的代价模型精准贴合系统真实的访问链路:
- 工作集远小于可用内存时(全内存缓存) :
无论底层挂载的是高性能 NVMe SSD 还是普通云盘,数据的提取绝大多数在内存中完成,物理层面的"随机开销"几乎不存在。将random_page_cost下调至 **1.1~1.2**,能精确抹平优化器对索引点查的虚假恐慌,锁定高频索引路径。 - 数据量远超内存且读 I/O 频繁穿透至存储介质时 :
必须承认固态存储(如标准云盘 gp3)的随机 8KB IOPS 依然存在配额和寻道延迟,此时盲目使用1.1会导致优化器在范围查询中拒绝合理的批量预读与位图合并。设置在 **1.4~2.0**才是更符合物理实际的折中均衡点。
random_page_cost 绝非调节运行速度的"物理油门",它是优化器在决策天平上衡量随机与顺序之间倾斜程度的核心砝码。