优化器架构对比:Oracle vs PostgreSQL vs MySQL

为什么同一句 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)**的动态规划算法搜索最优计划:

  1. 从单表访问路径开始,评估所有的索引扫描和全表扫描;
  2. 两两组合评估 JOIN 顺序和 JOIN 方法(Nested Loop / Hash Join / Sort Merge);
  3. 逐步扩展到三表、四表......直到覆盖所有表;
  4. 剪枝:如果某个中间计划的部分代价已经超过已知最优计划,直接丢弃。

这个过程的时间复杂度是 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 EXPLAINEXPLAIN 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 优化器代表了未来方向。适合「正在追赶场景中」的项目,期待后续版本。

理解优化器的设计哲学,比背诵优化器行为重要十倍。因为行为可以查文档,哲学决定了它会往哪个方向演进。


相关推荐
浪子明X1 小时前
从 MongoDB 文档到关系模型:构建可重跑、可对账的数据迁移流水线
数据库·mongodb·oracle
隔窗听雨眠1 小时前
GBase 8s并发控制深度解析:封锁机制、隔离级别与死锁处理全攻略
服务器·数据库·oracle
阿坤带你走近大数据3 小时前
SQL的执行顺序和书写顺序的介绍
数据库·sql·oracle
码农颜5 小时前
5.6.2 ⾏级锁死锁
数据库·sql·oracle
ChaHae-In6 小时前
MyBatis动态SQL与MyBatis-Plus高效开发指南
数据库·oracle·mybatis
冰暮流星6 小时前
mysql之分组查询
数据库·mysql
倒流时光三十年7 小时前
PostgreSQL Semi-Join(半连接)通俗讲解
数据库·postgresql
NiceCloud喜云7 小时前
腾讯云国际版云数据库选型:MySQL、Redis 怎么按业务架构来判断
数据库·mysql·腾讯云
ew452188 小时前
MySQL JOIN 关联语法、差异、标准用法、经典坑点汇总
数据库·mysql
ciqingloveless8 小时前
Oracle打开数据库加密
数据库·oracle