EXISTS子查询需同时满足四个条件才触发Semi Join:外层等值关联、被关联字段有索引、子查询无GROUP BY/ORDER BY/LIMIT/UNION/DISTINCT/聚合函数、固定写SELECT 1。

你不能手写 SEMI JOIN —— 它不是 SQL 语法,也不是可执行的关键字,强行写会报错:syntax error at or near "SEMI"。真正能触发半连接优化的,是写对 EXISTS 或合规的 IN 子查询,并满足引擎识别条件。
EXISTS 子查询怎么写才真走 Semi Join
数据库(MySQL 8.0+、PostgreSQL、TiDB)只有在明确判断“这是存在性检查”时,才会启用 Hash Semi Join 或 Nested Loop Semi Join 物理算子。这依赖四个硬性条件同时满足:
-
EXISTS子查询中必须有对外层表字段的等值关联,例如o.customer_id = c.id;写成o.id = c.id就失效 - 被关联字段(如
orders.customer_id)必须有索引;没索引时执行计划会出现type=ALL+Using where - 子查询里不能含
GROUP BY、ORDER BY、LIMIT、UNION、DISTINCT或聚合函数;这些结构会让优化器放弃 unnesting - 子查询里固定写
SELECT 1,而非SELECT *或具体字段——避免优化器误判需投影数据
为什么 IN 很容易踩坑
IN 看似简洁,但语义和执行路径比 EXISTS 更脆弱:
- 子查询返回
NULL时,整行静默过滤(哪怕其他值匹配),逻辑不可控 - 写
WHERE id IN (SELECT DISTINCT customer_id FROM orders)会触发Start temporary/End temporary执行阶段,实际走物化临时表,而非半连接 - 右表字段无索引时,
IN和EXISTS一样慢,但IN更难从执行计划里看出问题(Extra栏可能只显示Using where) - MySQL 8.0+ 默认开启
semijoin=on,但只要子查询里出现窗口函数或 CTE 引用,就会自动禁用
看懂执行计划里的 Semi Join 信号
别只看“有没有 Hash Join”,关键要确认是否为 *半连接*:
- MySQL 的
EXPLAIN中出现FirstMatch:表示内层扫描到第一个匹配就终止,是Nested Loop Semi Join的典型标志 - 出现
Start temporary/End temporary:说明优化器退化为物化 + 去重,没走半连接 - PostgreSQL 的
EXPLAIN (ANALYZE)显示Semi Join节点,且Rows Removed by Filter很低,说明短路生效 - TiDB 若发现子查询结果集远大于外层表(比如查 100 行用户,子查询返回千万订单),可能默认用子表建哈希,此时需加
/*+ SEMI_JOIN_REWRITE() */HINT 改为外层驱动
最易被忽略的一点:即使所有条件都满足,如果外层表经过复杂过滤后仍很大(比如 WHERE created_at > '2025-01-01' 返回 50 万行),而右表索引选择性差(如 status 只有 3 个值),优化器也可能放弃半连接——它优先选代价最低的路径,不是“写了 EXISTS 就一定优化”。

















