订阅更新

子查询 vs JOIN,到底该用哪个?

SQL手册 第 11 题 约 8 分钟读完

本文目录
  1. 先说结论
  2. 场景一:取关联数据——JOIN更直观
  3. 场景二:存在性判断——EXISTS更优
  4. 场景三:NOT IN 的NULL大坑
  5. 场景四:关联子查询 vs 窗口函数
  6. 场景五:大数据场景下的特殊考虑
  7. 面试总结:怎么答这道题

先说结论

大部分场景下,JOIN和子查询性能差距不大,因为现代优化器(Spark/Hive/PostgreSQL)会把子查询改写成JOIN来执行。但在某些特定场景下,两者有明显区别。

记住一个原则:能用JOIN就用JOIN,遇到"存在性判断"优先考虑EXISTS。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

"子查询能实现的,JOIN也能实现,那到底用哪个?"——这个问题我被面试官问过不下5次,自己面别人也爱问。因为答案不是非黑即白,能把各种场景分析清楚的人,SQL功底基本不差。

场景一:取关联数据——JOIN更直观

需求:查询每个订单的用户名和手机号。

-- JOIN写法(推荐)
SELECT
  o.order_id,
  o.order_amount,
  u.user_name,
  u.phone
FROM user_order o
JOIN dim_user u ON o.user_id = u.user_id
 
-- 子查询写法(不推荐)
SELECT
  order_id,
  order_amount,
  (SELECT user_name FROM dim_user WHERE user_id = o.user_id) AS user_name,
  (SELECT phone FROM dim_user WHERE user_id = o.user_id) AS phone
FROM user_order o

子查询写法叫标量子查询(Scalar Subquery),每一行都要执行一次子查询。虽然优化器可能会把它改写成JOIN,但不是所有引擎都能优化,而且可读性也差。

结论:需要关联取字段的时候,直接用JOIN。

场景二:存在性判断——EXISTS更优

需求:找出下过单的用户。

-- EXISTS写法(推荐)
SELECT u.user_id, u.user_name
FROM dim_user u
WHERE EXISTS (
  SELECT 1 FROM user_order o
  WHERE o.user_id = u.user_id
)
 
-- IN写法
SELECT user_id, user_name
FROM dim_user
WHERE user_id IN (
  SELECT user_id FROM user_order
)
 
-- JOIN写法
SELECT DISTINCT u.user_id, u.user_name
FROM dim_user u
JOIN user_order o ON u.user_id = o.user_id

三种都能实现,区别在哪?

EXISTS的优势: 它是"找到一条就停"的短路逻辑。一个用户有100个订单,EXISTS找到第一个就返回TRUE,不需要继续扫描剩下99条。

IN的问题: 子查询会先把所有user_id查出来放到一个集合里,如果user_order有10亿行,这个集合会非常大。而且IN对NULL的处理有坑(后面会讲)。

JOIN的问题: 一个用户有100个订单,JOIN后会产生100行,必须用DISTINCT去重。数据量大的时候DISTINCT本身就很耗资源。

结论:判断"存在/不存在"的时候,首选EXISTS/NOT EXISTS。

场景三:NOT IN 的NULL大坑

这是面试高频陷阱题。

需求:找出没有下过单的用户。

-- ❌ NOT IN遇到NULL会出问题
SELECT user_id, user_name
FROM dim_user
WHERE user_id NOT IN (
  SELECT user_id FROM user_order  -- 如果这里有NULL...
)

如果user_order表中有一条记录的user_id是NULL,那么NOT IN的结果会变成空集——一个用户都查不出来。

为什么?SQL的三值逻辑。x NOT IN (1, 2, NULL) 等价于 x != 1 AND x != 2 AND x != NULL。而x != NULL的结果是UNKNOWN(不是TRUE也不是FALSE),AND上一个UNKNOWN,整个条件就永远不会是TRUE。

-- ✅ 用NOT EXISTS替代NOT IN
SELECT u.user_id, u.user_name
FROM dim_user u
WHERE NOT EXISTS (
  SELECT 1 FROM user_order o
  WHERE o.user_id = u.user_id
)
 
-- ✅ 或者用LEFT JOIN + IS NULL
SELECT u.user_id, u.user_name
FROM dim_user u
LEFT JOIN user_order o ON u.user_id = o.user_id
WHERE o.user_id IS NULL

我之前做用户增长分析的时候就踩过这个坑。需求是"找出注册了但从未下单的用户",用NOT IN写的,跑出来结果是0。我还以为所有用户都下过单,兴高采烈地跟运营汇报"转化率100%"。结果运营一脸问号,排查了一个下午才发现是订单表里有几条user_id为NULL的脏数据。从那以后我再也不用NOT IN了。

场景四:关联子查询 vs 窗口函数

需求:查每个部门工资最高的员工。

-- 关联子查询写法
SELECT *
FROM employee e1
WHERE salary = (
  SELECT MAX(salary)
  FROM employee e2
  WHERE e2.dept_id = e1.dept_id
)
 
-- 窗口函数写法(推荐)
SELECT * FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
  FROM employee
) t
WHERE rn = 1

关联子查询在外表每一行都要执行一次内部查询,逻辑上是O(N×M)的复杂度。窗口函数只需要一次全表扫描+分组排序,效率高得多。

而且关联子查询还有一个问题:如果最高工资有并列,它会返回多条记录。窗口函数用ROW_NUMBER可以精确控制只返回一条。

结论:涉及"分组内的TopN/最值"需求,优先用窗口函数。

场景五:大数据场景下的特殊考虑

在Hive/Spark里,子查询和JOIN的差异更大:

1. IN子查询的大小限制

Hive执行IN子查询时,会把子查询结果收集到Driver端,如果结果集太大(比如几百万行),Driver会OOM。而JOIN走的是MapReduce/Spark的分布式Shuffle,不会有这个问题。

-- ❌ 在Hive中,如果子查询返回几百万行,可能OOM
SELECT * FROM big_table
WHERE user_id IN (SELECT user_id FROM another_big_table)
 
-- ✅ 改用JOIN
SELECT t.*
FROM big_table t
JOIN (SELECT DISTINCT user_id FROM another_big_table) u
  ON t.user_id = u.user_id

2. EXISTS在Hive中的支持

早期的Hive版本对EXISTS支持不好,有些写法会报错或者性能很差。Hive 2.x以后好多了,但如果你还在用老版本,LEFT SEMI JOIN是替代方案:

-- Hive的LEFT SEMI JOIN = EXISTS
SELECT u.*
FROM dim_user u
LEFT SEMI JOIN user_order o ON u.user_id = o.user_id

LEFT SEMI JOIN只返回左表中能匹配上右表的行,效果等同于EXISTS,而且不会产生笛卡尔积。

面试总结:怎么答这道题

面试官问"子查询和JOIN怎么选"的时候,分场景回答:

场景推荐写法原因
---------------------
取关联字段JOIN直观,性能好
存在性判断EXISTS短路逻辑,不怕NULL
不存在判断NOT EXISTS 或 LEFT JOIN + IS NULLNOT IN有NULL陷阱
分组内TopN窗口函数比关联子查询快
大数据量过滤JOIN子查询结果集可能撑爆Driver

最后补一句:可读性也是选择标准。 团队里10个人看你的SQL,能一眼看懂的写法就是最好的写法。我在做SQL代码规范治理的时候,明确规定了"禁止使用NOT IN""减少标量子查询"这两条规则,从源头避免踩坑。

明天 DAY 12 聊NULL的各种坑,SQL三值逻辑、NULL的算术运算、聚合函数对NULL的处理——这些细节决定了你的SQL是"能跑"还是"跑对"。

你在工作中是偏好子查询还是JOIN?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器