为什么同一句 SQL 在 Oracle 里走 Nested Loop,到了 PostgreSQL 变成 Hash Join?这不是语法差异,而是优化器的设计哲学从根本上不同。本文从架构层面拆解三种数据库优化器的来龙去脉。
前言:优化器才是 SQL 引擎的真正灵魂
很多人学习 SQL 调优时,直接从 EXPLAIN 入手,盯着执行计划琢磨为什么用了全表扫描。但很少有人追问一个更根本的问题:这个执行计划是怎么被选出来的?
SQL 是一种声明式语言------你只告诉数据库「要什么」,不告诉它「怎么做」。把声明式的 SQL 转换成高效执行计划的任务,落在优化器(Optimizer)身上。同一个查询,可能有上百种等价的执行方式(不同的 JOIN 顺序、不同的访问路径、不同的 JOIN 算法),优化器的职责是从中选出代价最低的那个。
Oracle、PostgreSQL、MySQL(InnoDB)虽然都采用基于代价的优化(Cost-Based Optimization, CBO),但它们的架构设计、代价模型、计划搜索策略截然不同。理解这些差异,你才能解释那些「同样的 SQL、同样的数据分布,为什么三库选了不同的执行计划」的诡异现象。
一、优化器简史:从 RBO 到 CBO
1.1 规则优化器时代(RBO)
在 CBO 之前,数据库都使用基于规则的优化器(Rule-Based Optimizer, RBO)。RBO 的核心思想很简单:根据预定义的规则等级选择执行路径,不看数据的实际分布。
比如 Oracle 的 RBO 有 15 个访问路径等级:
| 等级 | 访问路径 |
|---|---|
| 1 | 按 ROWID 单行访问 |
| 4 | 按唯一索引单行访问 |
| 8 | 按组合唯一索引访问 |
| 9 | 按唯一索引范围扫描 |
| 15 | 全表扫描 |
RBO 的逻辑是:能用唯一索引就不用全表扫描,等级越低越好。这个规则在数据量小、数据分布均匀时还算合理。但当一张表有 1 亿行、过滤条件只能命中 10 行时,RBO 可能仍然选择全表扫描------因为它不知道数据长什么样。
1.2 CBO 的诞生
CBO 的核心理念是:让优化器「看见」数据。通过收集统计信息(行数、数据分布、列相关性),CBO 可以估算每种执行计划的 I/O 和 CPU 代价,从中选出最优方案。
三条产品线的 CBO 演进时间线:
| 数据库 | CBO 引入时间 | 替代 RBO 时间 | 关键事件 |
|---|---|---|---|
| Oracle | 1992(Oracle 7) | 2003(Oracle 10g,默认 CBO) | 行业先驱,1988 年提交专利 |
| PostgreSQL | 1996(PostgreSQL 诞生起) | 无(从无 RBO 阶段) | 纯 CBO,继承学术界查询优化研究成果 |
| MySQL | 2005(MySQL 5.0 雏形) | 2008(MySQL 5.1+ 逐渐替代) | 历史包袱重,早期大量 RBO 逻辑 |
这里有两点很值得玩味:
第一,Oracle 是 CBO 的先行者。 1988 年就申请了 CBO 专利,1992 年 Oracle 7 首次实现,这个时间比大多数读者开始编程的时间还早。经过 30 多年的迭代,Oracle CBO 的成熟度在业界无出其右。
第二,PostgreSQL 从一出生就是纯 CBO。 这一点很多人不知道。PG 的学术背景(UC Berkeley 的 Ingres/Postgres 项目)让它在设计之初就采用了代价模型,没有 RBO 的历史包袱。这也是为什么 PG 社区至今拒绝 Hint 机制------他们的哲学是「如果优化器选错了,应该修优化器,而不是让用户手动干预」。
第三,MySQL 的优化器历史包袱最重。 MySQL 早期大量依赖 RBO 逻辑(比如 IN 子查询的固定优化策略、特定情况下的全表扫描偏好),直到 5.6/5.7 才逐步用 CBO 替代。甚至在 MySQL 8.0 中,你还能找到一些 RBO 时代的遗迹。
1.3 为什么说 RBO 的幽灵还在游荡
即使在今天,三种数据库中仍有 RBO 的影子:
- Oracle :
/*+ RULE */Hint 在 Oracle 10g 之后就被官方声明不再支持,但SELECT /*+ RULE */ ... FROM dba_tables这种写法在某些老项目中仍然能看到; - PostgreSQL:最纯粹的 CBO,没有 RBO 痕迹;
- MySQL :某些场景下优化器仍会跳过代价评估直接选计划。比如
SELECT ... FROM t FORCE INDEX(idx)强制索引就是 RBO 思维的延续;早期版本对 IN 子查询强制使用半连接转换也是硬编码规则。
理解这段历史,你才能理解为什么三库优化器的设计哲学差异如此之大。
二、Oracle CBO:工业级优化引擎
Oracle CBO 的架构可以用「三段式流水线」来概括:
SQL 文本 → 查询转换器 → 代价估算器 → 计划生成器 → 执行计划
2.1 查询转换(Query Transformation)
这是 Oracle 优化器最强大的部分。在估算代价之前,CBO 会先对 SQL 做逻辑等价变换,把原始查询重写为等价但可能更高效的查询形式。常见的转换包括:
- 视图合并(View Merging):将视图定义展开到主查询中;
- 谓词推进(Predicate Pushing):将外层 WHERE 条件推到子查询或视图中;
- 子查询反嵌套(Subquery Unnesting) :将
WHERE col IN (SELECT ...)转换为 JOIN; - 星形转换(Star Transformation):针对星形模型的数据仓库查询,将大表与多个维表的 JOIN 转换为对位图索引的合并操作;
- OR 扩展(OR Expansion) :将
WHERE a=1 OR b=2转换为UNION ALL两个分支各自走索引。
sql
-- 原始查询
SELECT * FROM orders o
WHERE o.customer_id IN (SELECT id FROM customers WHERE level = 'VIP');
-- 查询转换后的等价形式(反嵌套)
SELECT o.* FROM orders o, customers c
WHERE o.customer_id = c.id AND c.level = 'VIP';
查询转换在 Oracle 中非常激进------它甚至会重写你写的部分 SQL 逻辑,前提是保证等价性。这也是 Oracle CBO 能在复杂查询(尤其是报表/OLAP 场景)中脱颖而出的关键。
2.2 代价估算(Cost Estimation)
代价估算是 CBO 的核心。Oracle 使用以下公式计算单次操作的代价:
Cost = I/O Cost + CPU Cost
其中:
- I/O Cost :单块读(index scan)和多块读(full table scan)的次数,由
db_file_multiblock_read_count等参数影响; - CPU Cost :估算的 CPU 周期消耗,由系统统计信息(
DBMS_STATS.GATHER_SYSTEM_STATS)提供每操作耗时基准; - 基数估算(Cardinality Estimation):最重要也最容易出错的环节------优化器需要估算每个中间结果集有多少行。如果基数算错了,后续的 JOIN 顺序、JOIN 方法都会跟着错。
Oracle 的基数估算依赖一套复杂的统计信息体系,包括列级统计、直方图、多列扩展统计、动态采样等。
2.3 计划生成(Plan Generation)
Oracle CBO 使用**自底向上(Bottom-Up)**的动态规划算法搜索最优计划:
- 从单表访问路径开始,评估所有的索引扫描和全表扫描;
- 两两组合评估 JOIN 顺序和 JOIN 方法(Nested Loop / Hash Join / Sort Merge);
- 逐步扩展到三表、四表......直到覆盖所有表;
- 剪枝:如果某个中间计划的部分代价已经超过已知最优计划,直接丢弃。
这个过程的时间复杂度是 O(3^N)(N 为表数量),但因为剪枝策略,实际运行远小于理论值。当 JOIN 表数量较多时(实践中通常在 6-7 表以上),Oracle 不再穷举所有排列,而是改用启发式搜索,由 _optimizer_max_permutations(默认 2000)控制排列数上限。
2.4 自适应优化(Adaptive Optimization)
Oracle 12c 引入的自适应优化是 CBO 的一个重要进化:
- 自适应计划(Adaptive Plans):优化器在计划中嵌入「检测点」,运行时根据实际行数与估算的偏差,动态切换计划分支(如从 Nested Loop 切换到 Hash Join);
- SQL 计划指令(SQL Plan Directives):当某个列的实际基数与估算严重偏差时,自动记录偏差信息,下次生成计划时自动修正;
- 自动重优化(Automatic Reoptimization):首次执行后如果发现估算偏差太大,下次执行时自动用修正后的统计重新优化。
这套机制的核心思路是:承认优化器不可能永远猜对,但可以让它在运行时自我纠正。
三、PostgreSQL CBO:学院派的代价之舞
PostgreSQL 的优化器同样是 CBO,但设计思路和 Oracle 有明显差异:
3.1 代价模型
PG 的代价模型比 Oracle 更「透明」------它把代价计算的关键参数直接暴露给 DBA:
sql
-- 查看 PostgreSQL 的代价常量(默认值)
SELECT name, setting, context FROM pg_settings
WHERE name LIKE 'cpu%' OR name LIKE '%cost%' OR name = 'random_page_cost'
ORDER BY name;
关键常量:
| 参数 | 默认值 | 含义 |
|---|---|---|
seq_page_cost |
1.0 | 顺序扫描一个页的代价 |
random_page_cost |
4.0 | 随机访问一个页的代价 |
cpu_tuple_cost |
0.01 | 处理一行的 CPU 代价 |
cpu_index_tuple_cost |
0.005 | 处理一个索引行的 CPU 代价 |
cpu_operator_cost |
0.0025 | 执行一次算子操作的 CPU 代价 |
PG 的代价是一个没有单位的抽象值,仅用于不同计划之间的相对比较。你不能说「Cost=1000 的执行需要 1 秒」,但规则是清晰的:总代价越低越好。
sql
-- 一个简单查询的代价计算
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
-- 输出示例:
-- Index Scan using idx_orders_customer on orders
-- (cost=0.29..8.31 rows=1 width=40)
-- Index Cond: (customer_id = 42)
--
-- 代价分解:
-- 启动代价 0.29 = 读索引根页到找到第一个匹配叶子页
-- 总代价 8.31 = 启动代价 + 从叶子页读数据页
random_page_cost 的默认值 4.0 是一个历史设定,基于机械硬盘的寻道延迟。如果你使用 SSD,应该把它调为 1.1-2.0。这个参数直接决定了优化器对索引扫描 vs 全表扫描的偏好------如果设得太高,优化器会过度偏好全表扫描。
3.2 遗传算法查询优化(GEQO)
当涉及 12 张以上表 JOIN 时(由 geqo_threshold 控制),PG 不再使用穷举搜索,而是切换到遗传算法(GEQO):
1. 初始种群:随机生成若干 JOIN 顺序
2. 适应度评估:用代价模型计算每个顺序的代价
3. 交叉/变异:好的顺序互相「繁殖」+ 随机扰动
4. 迭代:重复 N 代
5. 输出最优个体
GEQO 的优点是能处理非常复杂的查询(上百张表),代价是结果不确定------同一个 SQL 在不同时刻可能生成不同的执行计划。对于 OLTP 系统,这个特性需要特别注意。
3.3 查询转换能力
相比 Oracle,PG 的查询转换能力相对有限:
- 子查询反嵌套:支持,但不如 Oracle 激进;
- 视图合并:简单视图可以合并,但复杂视图(含 GROUP BY / DISTINCT)不合并;
- 谓词推进 :支持,分区表场景下依赖
enable_partition_pruning(默认 on)。
PG 社区的原则是「如果优化器做不了,DBA 可以通过改写 SQL 来做」。这也是为什么 PG 生态中「手工 SQL 改写」比 Oracle 生态中更常见。
四、MySQL CBO:从简单到复杂的追赶之路
MySQL 的优化器有三个关键版本节点:
4.1 MySQL 5.6 及之前:RBO 为主、CBO 为辅
很多人不知道,MySQL 早期优化器中有大量 RBO 逻辑,比如:
IN (SELECT ...)强制使用半连接转换(在 5.5 中效率极低);ORDER BY ... LIMIT的索引选择优先级固定;- 派生表(Derived Table)一定物化,不尝试合并到外层查询。
4.2 MySQL 5.7:CBO 全面接管
MySQL 5.7 是优化器的重要分水岭:
- 代价模型重构:从硬编码的「页读取次数」改为可配置的「I/O + CPU」代价模型;
- 优化器跟踪(Optimizer Trace):终于可以像 Oracle 10053 事件一样查看优化器内部决策过程;
- JSON EXPLAIN :
EXPLAIN FORMAT=JSON提供结构化的执行计划输出。
4.3 MySQL 8.0:功能大爆发
MySQL 8.0 的优化器进步最快:
| 特性 | 版本 | 意义 |
|---|---|---|
| Hash Join | 8.0.18 | 替代 Block Nested Loop,大表 JOIN 性能大幅提升 |
| 直方图 | 8.0 | 解决数据倾斜时的基数估算问题 |
| CTE 优化 | 8.0 | WITH 子句不再只是语法糖,优化器会按需物化 |
| 不可见索引 | 8.0 | 先设为不可见→观察执行计划→再决定是否删除 |
| Hypergraph 优化器 | 8.0.21 (实验) | 全新的 JOIN 枚举引擎,用超图替代动态规划 |
sql
-- MySQL 8.0 直方图示例
ANALYZE TABLE orders
UPDATE HISTOGRAM ON customer_id WITH 100 BUCKETS;
-- MySQL 优化器跟踪(Opt Trace)
SET optimizer_trace='enabled=on';
SELECT * FROM orders WHERE customer_id = 42;
SELECT * FROM information_schema.OPTIMIZER_TRACE\G
4.4 Hypergraph 优化器:MySQL 的未来方向
MySQL 8.0.21 引入的实验性 Hypergraph 优化器(SET optimizer_switch='hypergraph_optimizer=on')是一次根本性的架构变革:
- 传统方法:动态规划 + 启发式剪枝,按「左深树」(Left-Deep Tree)枚举 JOIN 顺序;
- Hypergraph 方法:将 JOIN 图建模为超图(Hypergraph),用自顶向下(Top-Down)的 DPccp 算法搜索,同时支持「浓密树」(Bushy Tree),可能发现传统方法找不到的更优计划。
这个方向代表了 MySQL 优化器从「够用」到「先进」的野心。
五、三库优化器设计哲学对比
| 维度 | Oracle | PostgreSQL | MySQL |
|---|---|---|---|
| 设计哲学 | 自动纠错,减少人工干预 | 透明可控,DBA 掌握方向盘 | 简单够用→逐步追赶 |
| 查询转换 | 激进(15+ 种转换) | 保守(手工改写可弥补) | 中等(8.0 后大幅提升) |
| 代价模型 | 单位化的 I/O + CPU | 抽象无单位代价常量 | 可配置的 I/O + CPU |
| 自适应能力 | 自适应计划 + 自动重优化 | 不依赖运行时反馈 | 不依赖运行时反馈 |
| JOIN 枚举 | 动态规划(≤7表)+ 启发式 | 穷举(≤12表)+ GEQO | 贪婪算法 + Hypergraph(实验) |
| Hint 机制 | 完善(100+ Hint) | 社区拒绝,缺省 | 有(但不如 Oracle 丰富) |
| 统计信息 | 细粒度(直方图+多列+表达式) | 中等(pg_stats,pg_statistic_ext) | 中等但进步快(8.0 直方图) |
| 透明度 | 需 10053 Trace 才能看到内部 | EXPLAIN 直接输出 | Opt Trace + JSON EXPLAIN |
当你在三库之间切换时,理解这些哲学差异,你不会再疑惑「为什么同样是 CBO,行为却完全不同」。
六、总结
三种数据库虽然都叫「基于代价的优化器」,但内核差异极大:
- Oracle:30 年迭代的工业级优化引擎,激进查询转换 + 自适应纠错 + 完善 Hint 体系。适合「不想管优化器」的场景------它会尽可能地自动搞定;
- PostgreSQL :学院派的设计哲学,代价模型参数完全透明,拒绝 Hint 但提供
random_page_cost等关键调节旋钮。适合「想掌控优化器行为」的 DBA; - MySQL:从 RBO 起步,8.0 之后进步最快,Hypergraph 优化器代表了未来方向。适合「正在追赶场景中」的项目,期待后续版本。
理解优化器的设计哲学,比背诵优化器行为重要十倍。因为行为可以查文档,哲学决定了它会往哪个方向演进。