FULL OUTER JOIN 性能差源于必须全表扫描且无法剪枝,优化器只能选Hash Match或Merge Join,二者均需双表全扫+合并;索引仅在提供排序或缩小哈希构建端时有效;重写为UNION+LEFT JOIN更高效。

FULL OUTER JOIN 在大型数据集上慢,不是因为写法错,而是它天然无法跳过任何一行——左右表所有数据都得扫一遍,再合并补 NULL,没有剪枝空间。
为什么执行计划里总出现 Hash Match (Full Outer Join) 或 Merge Join + Sort?
数据库引擎没太多选择:INNER JOIN 可双向过滤,LEFT JOIN 至少能固定左表驱动,但 FULL OUTER JOIN 必须同时保留两表全部行。优化器通常只能选以下两种物理算子:
- Hash Match:先为小表建哈希表,再扫描大表探测;但若两表都大,哈希表可能放不进内存,触发
Spill to TempDB(SQL Server)或磁盘哈希(PostgreSQL),I/O 暴增 - Merge Join:要求两表连接列已排序,否则得先各自
Sort—— 百万级数据排序本身就很重,且Sort算子常出现在执行计划顶部,说明开销前置
无论哪种,都逃不开“双表全扫描 + 内存/磁盘合并”这个本质。你看到的 Estimated Rows 和 Actual Rows 差距越大,越说明统计信息不准,进一步误导优化器选错算法。
索引真的有用吗?哪些索引才管用
有索引不一定加速 FULL OUTER JOIN,关键看怎么用:
- 仅在连接列(如
id)建单列索引,对 Hash/Merge 无直接帮助——哈希连接不依赖索引查找,归并连接只认排序,不认 B-Tree 结构 - 但如果连接列上有 聚集索引(SQL Server)或 主键顺序存储(PostgreSQL),Merge Join 才可能免去
Sort步骤 - 复合索引无效:比如
(id, status)对ON t1.id = t2.id没额外收益,status字段在 FULL OUTER JOIN 中不参与匹配逻辑 - 真正起作用的是「让优化器相信某表更小」:更新统计信息后,它更可能把有索引的小表当哈希构建端,减少内存压力
分区交换(Partition Switching)根本不能加速 FULL OUTER JOIN
这是常见误解。分区交换是 DDL 操作,用于快速替换整个分区(比如按月切换历史表),但它:
- 不改变查询执行计划,也不影响连接算法
- 不减少扫描行数——即使你把左右表都按
date分区,FULL OUTER JOIN 仍需跨所有分区匹配,除非你手动加WHERE date BETWEEN ...裁剪范围 - 若强行用
SWITCH PARTITION后再 JOIN,反而引入额外元数据操作和锁竞争
分区只在你能把问题限制在单个分区时才有意义,而 FULL OUTER JOIN 的语义决定了它大概率要跨分区。
替代方案比硬调 FULL OUTER JOIN 更有效
90% 的业务场景其实不需要真正的 FULL OUTER JOIN,而是“所有键的并集 + 各自值”。这时重写为:
SELECT keys.id,
t1.val AS val1,
t2.val AS val2
FROM (SELECT id FROM t1 UNION SELECT id FROM t2) keys
LEFT JOIN t1 ON keys.id = t1.id
LEFT JOIN t2 ON keys.id = t2.id;
好处很实在:
-
UNION去重后结果集通常远小于两表行数之和,LEFT JOIN 驱动表变小 - 两个 LEFT JOIN 可分别利用
t1(id)和t2(id)索引,避免全表扫描 - 执行计划里不再出现
Hash Match (Full Outer Join),而是两个独立的Index Seek+Nested Loops,内存占用低、响应快
真正难的不是写对 SQL,而是判断业务是否真需要“两边全保留”。一旦误判,所有索引、并行、参数调优都是在给错误前提打补丁。

















