1. 测试背景与问题定义
最近在数据库性能优化工作中,发现一个有趣的现象:Oracle的HASH JOIN半连接(SEMI JOIN)和反连接(ANTI JOIN)操作存在一种"刹车机制"。当驱动表的所有匹配行都找到后,Oracle会提前终止被驱动表的扫描。这种机制能显著减少I/O操作和CPU消耗,尤其在大表关联场景下效果更为明显。
为了验证这一特性,我设计了一组对比测试:
- 测试环境:Oracle 19c vs 崖山23.5.1
- 测试数据:
- TEST02:8万行数据(来自DBA_OBJECTS)
- TEST01:TEST02重复512次,约4500万行
- 关键点:两表均不创建索引,强制走HASH JOIN
2. 半连接刹车机制深度解析
2.1 Oracle的半连接执行计划分析
执行以下SQL并检查执行计划:
sql复制select count(*)
from test02 a
where exists (select null from test01 b where a.object_id = b.object_id);
Oracle的执行计划关键指标:
code复制| Id | Operation | Name | Starts | E-Rows | A-Rows | Buffers |
|-----|---------------------|--------|--------|--------|--------|---------|
| 2 | HASH JOIN SEMI | | 1 | 86987 | 86987 | 4518 |
| 4 | TABLE ACCESS FULL | TEST01 | 1 | 44M| 228K| 3267 |
关键发现:
- TEST01实际扫描行数(A-Rows)仅22.8万行,远小于总行数4500万
- 逻辑读(Buffers)仅4518次,说明没有全表扫描
- 执行时间仅0.02秒
原理说明:Oracle的HASH JOIN SEMI会在内存中构建驱动表(TEST02)的哈希表,当被驱动表(TEST01)的扫描过程中,每找到一行匹配就会标记驱动表中对应行。当所有驱动表行都找到匹配后,立即终止扫描。
2.2 崖山数据库的对比测试
相同SQL在崖山23.5.1的执行计划:
code复制+----+--------------------------------+--------+----------+----------+-------------+
| Id | Operation type | Name | A - Rows | Cost(%CPU)| A - Time |
+----+--------------------------------+--------+----------+----------+-------------+
| 2 | HASH JOIN RIGHT SEMI | | 86987 | 1087669 | 8208166 us |
| 3 | TABLE ACCESS FULL | TEST01 | 44537344 | 1086015 | 2583504 us |
+----+--------------------------------+--------+----------+----------+-------------+
问题暴露:
- TEST01完整扫描了4500万行(A-Rows = 44537344)
- 逻辑读高达651026次
- 执行时间8.2秒,是Oracle的400倍
2.3 刹车机制的触发条件验证
通过删除TEST01中部分数据(OBJECT_ID=2)制造数据缺口:
sql复制create table test01_bak as select * from test01 where object_id>2;
再次执行半连接查询,Oracle的刹车机制失效:
code复制| Id | Operation | Name | A-Rows | Buffers | A-Time |
|-----|---------------------|------------|--------|---------|-----------|
| 4 | TABLE ACCESS FULL | TEST01_BAK | 44M| 635K| 00:00:01.66|
核心结论:
- 刹车机制要求被驱动表的连接列必须完全包含驱动表的连接列值
- 当存在不匹配的驱动表键值时,Oracle会退化为全表扫描
3. 反连接刹车机制测试
3.1 Oracle的反连接执行计划
测试SQL:
sql复制select count(*)
from test02 a
where not exists (select null from test01 b where a.object_id = b.object_id);
执行计划关键指标:
code复制| Id | Operation | Name | A-Rows | Buffers | A-Time |
|-----|---------------------|--------|--------|---------|-----------|
| 2 | HASH JOIN ANTI | | 0 | 4518 | 00:00:00.02|
| 4 | TABLE ACCESS FULL | TEST01 | 228K| 3267 | 00:00:00.01|
验证结果:
- 反连接同样存在刹车机制
- TEST01仅扫描22.8万行即终止
- 执行时间保持0.02秒的高效水平
3.2 崖山反连接推测
基于半连接的测试结果可以合理推测:
- 崖山的HASH JOIN ANTI同样缺乏刹车机制
- 会完整扫描被驱动表的所有数据
- 性能表现应与半连接情况类似
4. 驱动表选择差异分析
4.1 Oracle的驱动表选择
在Oracle中:
sql复制select count(*) from test02 a where exists (select null from test01 b...)
执行计划显示:
code复制| 3 | TABLE ACCESS FULL | TEST02 | --> 驱动表
| 4 | TABLE ACCESS FULL | TEST01 | --> 被驱动表
Oracle规则:
- 对于EXISTS/NOT EXISTS子查询
- 外层表(TEST02)始终作为驱动表
- 子查询中的表(TEST01)作为被驱动表
4.2 崖山的驱动表选择
崖山的执行计划:
code复制| 3 | TABLE ACCESS FULL | TEST01 | --> 被驱动表(实际成为驱动表)
| 4 | TABLE ACCESS FULL | TEST02 | --> 驱动表(实际成为被驱动表)
关键差异:
- 崖山与PostgreSQL行为一致,与Oracle相反
- 驱动表选择策略影响HASH JOIN的内存使用和性能
- 这种差异可能导致SQL在迁移时需要重写优化
5. 性能优化建议
5.1 针对Oracle的优化方案
-
确保数据完整性:
- 被驱动表的连接列必须包含驱动表的所有键值
- 可通过外键约束或应用逻辑保证
-
统计信息准确性:
sql复制exec dbms_stats.gather_table_stats('SCOTT','TEST01'); exec dbms_stats.gather_table_stats('SCOTT','TEST02'); -
HINT强制驱动表选择:
sql复制select /*+ LEADING(a) */ count(*) from test02 a where exists (select null from test01 b...)
5.2 针对崖山的应对策略
-
查询重写:
sql复制-- 将EXISTS改为IN可能获得更好性能 select count(*) from test02 where object_id in (select object_id from test01); -
临时表优化:
sql复制create global temporary table temp_ids as select distinct object_id from test01; select count(*) from test02 a where exists (select null from temp_ids b...); -
应用层分页处理:
- 对于超大结果集,考虑分批处理
- 使用ROWNUM或OFFSET-FETCH分页
6. 深度原理探讨
6.1 HASH JOIN刹车实现机制
Oracle的刹车机制核心逻辑:
-
初始化阶段:
- 为驱动表构建内存哈希表
- 为每行设置"已匹配"标记位
-
探测阶段:
- 扫描被驱动表,查找哈希匹配
- 每找到匹配就标记驱动表对应行
- 当所有驱动表行都被标记,立即终止扫描
-
特殊处理:
- 反连接标记"不匹配"状态
- 处理NULL值的特殊逻辑
6.2 崖山实现差异的根源
通过与Oracle的对比分析,崖山可能:
- 采用经典的HASH JOIN算法
- 没有实现"早期终止"优化
- 驱动表选择策略不同
- 内存管理方式存在差异
7. 生产环境注意事项
-
版本兼容性:
- 不同Oracle版本刹车行为可能微调
- 19c与12c的表现略有差异
-
监控方法:
sql复制-- 实时监控HASH JOIN内存使用 select * from v$sql_workarea_active; -- 检查历史执行统计 select * from v$sql where sql_id = 'axwv6ttb11tjb'; -
参数调优:
sql复制-- 调整HASH JOIN内存区域 alter session set workarea_size_policy = MANUAL; alter session set hash_area_size = 104857600; -- 100MB -
异常处理:
- 当刹车机制失效时(如数据不完整)
- 考虑使用NL提示强制嵌套循环
sql复制select /*+ NL_AJ */ count(*) from test02 a where not exists (select null from test01 b...)
通过本测试可以清晰看到,Oracle的HASH JOIN刹车机制在大表关联时能带来数量级的性能提升。而崖山目前缺乏此优化,在同类场景下性能差距显著。建议开发者在数据库选型和SQL编写时充分考虑这一特性差异。