千万级表做FULL JOIN是反模式,本质需生成全笛卡尔补集致内存爆炸;应改用LEFT+RIGHT JOIN加UNION ALL分三路索引查询,或物化宽表替代实时JOIN。

千万级表做 FULL JOIN 本质是反模式
FULL JOIN 在千万级表上几乎必然失败:它需要生成左表 + 右表的全笛卡尔补集(含 NULL 行),内存和磁盘排序开销爆炸,MySQL 会直接 OOM,PostgreSQL 可能卡住数小时甚至崩溃。这不是性能问题,是语义误用——你真正需要的往往不是 FULL JOIN,而是更轻量、可索引、可分片的等价逻辑。
用 LEFT JOIN + RIGHT JOIN + UNION ALL 替代 FULL JOIN
FULL JOIN 等价于「左独有 + 右独有 + 交集」三部分拼接,而每部分都可走索引、可加 WHERE 过滤、可分页。关键点:
- 左独有:用
LEFT JOIN配合WHERE right_table.id IS NULL - 右独有:用
RIGHT JOIN或改写为LEFT JOIN(交换表序)+WHERE left_table.id IS NULL - 交集:标准
INNER JOIN - 最后用
UNION ALL合并(不用UNION,避免去重开销)
SELECT 'left_only' AS src, l.* , NULL AS r_id, NULL AS r_name FROM orders_large l LEFT JOIN users_small r ON l.user_id = r.id WHERE r.id IS NULL <p>UNION ALL</p><p>SELECT 'right_only', NULL, NULL, r.id, r.name FROM users_small r LEFT JOIN orders_large l ON r.id = l.user_id WHERE l.user_id IS NULL</p><p>UNION ALL</p><p>SELECT 'both', l.*, r.id, r.name FROM orders_large l INNER JOIN users_small r ON l.user_id = r.id;</p>
注意:所有 JOIN 字段必须有索引(如 orders_large(user_id) 和 users_small(id)),否则单个子查询就会慢得无法接受。
如果真要硬跑 FULL JOIN,必须加强制过滤和分块
极少数场景(如离线数据比对)绕不开 FULL JOIN,此时必须打破“全量一次跑完”思维:
- 绝不裸跑:
ON条件外必须加时间/分区字段过滤,例如AND l.created_at >= '2024-01-01' - 按主键或时间范围分块:用
WHERE l.id BETWEEN 1000000 AND 2000000分批执行,结果落临时表再合并 - 关闭排序优化干扰:PostgreSQL 中设
SET enable_hashjoin = off; SET enable_mergejoin = off;强制走 nested loop(小右表时更可控);MySQL 则需确保join_buffer_size足够,但上限受物理内存限制
常见错误现象:ERROR: out of memory for query result(PostgreSQL)、MySQL server has gone away(因超时或内存溢出中断)——这说明没分块也没过滤,已触发系统保护。
用物化中间结果替代实时 JOIN
对高频查询或报表场景,把“FULL JOIN 逻辑”固化为定期更新的宽表,比每次计算强十倍:
- 建一张
orders_with_user_enriched,每日凌晨用INSERT ... SELECT增量刷新(基于updated_at) - 左独有行用
INSERT IGNORE或ON CONFLICT DO NOTHING避免重复 - 右独有行单独跑一个
INSERT补入,标记is_user_only = true - 查询时只查这张宽表,加好复合索引(如
(user_id, is_user_only, created_at))
这个方案把 O(N×M) 的复杂度降为 O(N+M),且支持秒级响应。很多人卡在“必须实时”,但业务上绝大多数所谓“实时”只要分钟级延迟即可接受。
实际中最容易被忽略的一点:FULL JOIN 的 NULL 行在应用层极易引发空指针或聚合偏差,而分拆后的三路结果可以各自定义默认值、打标、甚至跳过处理——可控性远高于黑盒式全连接。

















