semi join本质是exists的执行语义,数据库优化器常自动将其转为物理semi join,但sql中不可直接书写;可用inner join+distinct或in(需处理null)等价改写,spark sql和trino支持left semi join语法,postgresql等不支持。

SEMI JOIN 本质就是 EXISTS 的执行语义
数据库优化器(如 PostgreSQL、Spark SQL、Trino)在底层常把 EXISTS 自动转成 SEMI JOIN 执行,但你不能直接写 SEMI JOIN 关键字——它不是标准 SQL 语法,而是物理执行计划里的概念。想“改写”,实际是用等价的 JOIN + DISTINCT 或 IN 替代,同时注意语义是否严格等价。
用 INNER JOIN + DISTINCT 模拟 SEMI JOIN(适用多数场景)
当 EXISTS 子查询只检查存在性、不依赖外部列计算时,可用 INNER JOIN 加去重替代,避免重复行膨胀:
-
EXISTS原写法:SELECT id FROM orders o WHERE EXISTS (SELECT 1 FROM customers c WHERE c.id = o.customer_id AND c.status = 'active');
- 等价改写:
SELECT DISTINCT o.id FROM orders o INNER JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active';
- 注意:若
orders.customer_id有 NULL,INNER JOIN会自动过滤掉,和EXISTS行为一致;但若customers.id允许 NULL,需额外加c.id IS NOT NULL条件,否则可能漏数据 - 性能上,
DISTINCT可能引入排序或哈希去重开销,大表慎用;索引建议在customers(id, status)上建联合索引
用 IN 替代 EXISTS 要小心 NULL 和重复值
IN 看似简洁,但语义不同:当子查询结果含 NULL,整个 IN 表达式返回 UNKNOWN,导致整行被过滤,而 EXISTS 不受子查询列是否为 NULL 影响:
- 危险写法(结果可能不等价):
SELECT id FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE status = 'active');
- 安全写法(显式排除 NULL):
SELECT id FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE status = 'active' AND id IS NOT NULL);
-
IN对子查询结果自动去重,无需DISTINCT,但子查询若返回大量值,某些数据库(如 MySQL 5.7)可能触发IN列表长度限制或全表扫描
各数据库对 SEMI JOIN 的显式支持差异很大
真正能写 SEMI JOIN 关键字的极少,仅限部分引擎内部 DSL 或扩展语法:
- Spark SQL 支持
LEFT SEMI JOIN,但只能用于JOIN语法,不能替代所有EXISTS场景:SELECT o.* FROM orders o LEFT SEMI JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active';
- PostgreSQL、MySQL、SQL Server **不支持**
SEMI JOIN关键字,强行写会报错syntax error at or near "SEMI" - Trino 用
LEFT SEMI JOIN,但要求右表只能在ON条件中引用,不能出现在SELECT或WHERE其他位置 - 改写前务必确认执行计划:用
EXPLAIN对比原EXISTS和改写后的语句,看是否真走 hash semi-join 或 nested loop semi-join,而不是退化成普通 join + filter
最易被忽略的一点:EXISTS 子查询里如果有相关列参与聚合或复杂表达式(比如 (SELECT COUNT(*) FROM ...)),就无法用任何 JOIN 形式安全改写——SEMI JOIN 语义只覆盖“是否存在”,不覆盖“存在几个”或“满足某聚合条件”。这时候老老实实用 EXISTS,别硬套。











