ORACLE和MySQL的hash join,如果hash table返回0行,则probe table不需要执行,执行次数是0,而PG的还会执行,hash table a返回0行,但是probe table b还要执行,只不过{BANNED}最佳多只扫描满足条件的一行去探测,发现hash table没有数据,后面的不再探测
ORACLE的hash join执行计划显示的顺序和PG,MySQL相反:
PG和MySQL的树形计划,hash table在下面,按照关键字hash标识,probe table在上面,这个是按照探测流程算的,其实probe table不是驱动表
而oracle是按照hash join执行顺序来的,上面是hash table,下面是probe table
对于hash table返回0行,probe table是否要执行,三大库本质上是**数据库引擎在处理“空结果集”时的流水线止损策略(Short-circuiting)**的不同实现。
我们可以把这三种数据库的行为模式总结为:“先确认,再开工”(Oracle/MySQL)与**“边开工,边确认”**(PostgreSQL)。
1. Oracle & MySQL:极致的短路逻辑(Start-Stop)在 Oracle 和 MySQL 的执行引擎中,Hash Join 被视为两个高度解耦的动作:“构建桶”和“流式探测”。
执行行为:引擎会完全运行 Build Table 分支。一旦 Build Table 完成并发现产生的 Hash Table 为空(A-Rows=0 或 Rows=0),Join 控制器会立刻向优化器或上层节点发出信号:
“由于 Hash Table 没有数据,任何 Probe 动作都是徒劳”。
因此,Probe Table 的扫描被完全跳过。
计划表现:Oracle: Starts = 0。这是一个非常清晰的标志,说明底层物理扫描函数根本没被调用。
MySQL: never executed。MySQL 8.0 的树状计划非常诚实地反映了这一跳过行为。
性能优势: 如果 Probe Table 是一个极其庞大的全表扫描(Full Table Scan),这种短路逻辑能节省巨大的 I/O。
2. PostgreSQL:懒加载与首次触达(Lazy Execution)PostgreSQL 的行为比较特殊。
它的执行器(Executor)是基于“拉取模型(Demand-driven pulling)”的。
执行行为:当 Hash Join 节点被调用时,它首先调用右侧的 Hash 节点(Build Table)来填充内存。
即便 Hash 节点返回了 0 行,由于 PG 的架构设计,Hash Join 节点作为父节点,
通常会尝试向左侧(Probe Table)发出至少一次获取数据的请求(ExecProcNode 调用)。
关键点: 如你观察到的,Seq Scan on sub_t b 确实显示了 rows=1 loops=1。
这代表它为了确认“有没有数据可以用来探测”,至少扫描了{BANNED}中国第一行。
后续行为: 当{BANNED}中国第一行数据拿回来准备匹配时,发现内存里的 Hash Buckets 是空的,
PG 此时才会意识到没必要继续,随后停止扫描。计划表现: 你会看到 actual rows=1,
这在统计上可能让 DBA 误以为扫描了很久,但其实它在获取{BANNED}中国第一行后就“梦醒”了。
3.三大库针对hash table返回0行的总结如下:
数据库 判定逻辑位置 当 Build Table 为空时 标志性输出
Oracle HASH JOIN 调度层 彻底不执行 Probe Table 分支 Starts = 0
MySQL Join Controller 层 彻底不执行 Probe Table 分支 never executed
PostgreSQL HashJoinState 迭代器 至少读取 1 行 探测表,随后停止 rows=1 loops=1
示例分析:
oracle 的hash table t1返回0行,T2的starts=0
-
PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 83m3a3jtmx889, child number 0
-------------------------------------
select * from t1,t2 where t1.object_name=t2.object_name and
t1.object_id=0
Plan hash value: 1838229974
-------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
-------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 0 |00:00:00.05 | 1460 | 1413 | | | |
|* 1 | HASH JOIN | | 1 | 1 | 0 |00:00:00.05 | 1460 | 1413 | 789K| 789K| 226K (0)|
|* 2 | TABLE ACCESS FULL| T1 | 1 | 1 | 0 |00:00:00.05 | 1416 | 1412 | | | |
| 3 | TABLE ACCESS FULL| T2 | 0 | 72814 | 0 |00:00:00.01 | 0 | 0 | | | |
-------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("T1"."OBJECT_NAME"="T2"."OBJECT_NAME")
2 - filter("T1"."OBJECT_ID"=0)
MySQL的hash table b表返回0行,probe table a表不执行:never executed
-
explain analyze select/*+no_index(a) no_index(b)*/ * from emp1 a join emp2 b on a.first_name=b.first_name where b.hire_date>date'2026-1-1'\G
*************************** 1. row ***************************
EXPLAIN: -> Inner hash join (a.first_name = b.first_name) (cost=2885080313.73 rows=23061842) (actual time=146.816..146.816 rows=0 loops=1)
-> Table scan on a (cost=0.14 rows=288992) (never executed)
-> Hash
-> Filter: (b.hire_date > DATE'2026-01-01') (cost=30216.55 rows=99831) (actual time=146.807..146.807 rows=0 loops=1)
-> Table scan on b (cost=30216.55 rows=299523) (actual time=0.061..129.105 rows=300014 loops=1)
1 row in set (0.15 sec)
PG的,hash table a返回0行,但是probe table b还要执行,只不过{BANNED}最佳多只扫描满足条件的一行去探测,loops=1,发现hash table没有数据,后面的不再探测
-
explain analyze select/*+hashjoin( a b)*/ * from main_t a,sub_t b where a.id=b.id and a.name='abc';
-
QUERY PLAN
-
---------------------------------------------------------------------------------------------------------------------------------
-
Hash Join (cost=8.30..1718.82 rows=1 width=15) (actual time=0.028..0.030 rows=0 loops=1)
-
Hash Cond: (b.id = a.id)
-
-> Seq Scan on sub_t b (cost=0.00..1448.00 rows=100000 width=7) (actual time=0.011..0.011 rows=1 loops=1)
-
-> Hash (cost=8.29..8.29 rows=1 width=8) (actual time=0.012..0.013 rows=0 loops=1)
-
Buckets: 1024 Batches: 1 Memory Usage: 8kB
-
-> Index Scan using idx_main_t on main_t a (cost=0.28..8.29 rows=1 width=8) (actual time=0.012..0.012 rows=0 loops=1)
-
Index Cond: (name = 'abc'::text)
-
Planning Time: 0.369 ms
-
Execution Time: 0.165 ms