别再滥用IN子查询了!用JOIN改写,从8秒到0.4秒(附优化步骤)

8 阅读2分钟

我是小耶,干运营半路出家的野生DBA——写功课只是为了我踩过的坑,你们别再踩了!

刚学SQL那会儿,遇到“在A表里查B表也有的数据”,我总喜欢写 IN 子查询,因为好理解,像英语一样:user_id IN (SELECT user_id FROM orders)。后来有一次,我写了一个这样的查询,跑了快十分钟都没出结果,这才认真去研究它为什么慢。

子查询慢的原因,可以这样理解:就像你打电话给餐厅,让服务员把所有菜名念一遍(生成一个大列表),然后你拿着这个列表一样一样去找你想吃的。如果餐厅有几百道菜,这个过程会非常慢。

在数据库里,子查询会先产生一个临时结果集,可能放在内存或磁盘里,然后外层查询逐行去匹配。如果子查询返回几百万行,临时表巨大,内存放不下就会写磁盘,IO飙升,速度自然快不起来。

怎么改?能JOIN就别子查询。

举个例子:查询“下过单的用户”中的VIP用户。

❌ 较慢的写法(子查询):

SELECT * FROM users 
WHERE vip_level = 3 
  AND user_id IN (SELECT DISTINCT user_id FROM orders);

✅ 快得多的写法(JOIN):

SELECT DISTINCT u.* 
FROM users u 
JOIN orders o ON u.user_id = o.user_id 
WHERE u.vip_level = 3;

注意加了 DISTINCT,因为一个用户可能下多个订单,JOIN会产生重复,要去重。

为什么JOIN快?

  • 可以利用 orders.user_id 上的索引
  • 优化器会选择小表驱动大表(通常VIP用户数量较少)
  • 不会生成巨大的中间临时表

子查询什么时候还凑合?

  • 子查询的结果集非常小(比如只返回几十行),写起来简单,性能差别不大
  • EXISTS 在某些场景下比 IN 好,尤其是子查询大但外层能快速匹配时

特别提醒:NOT IN 要小心 NULL 值——如果子查询结果中包含 NULLNOT IN 会返回空结果,所以更推荐用 NOT EXISTS

实测数据 我拿一张500万行的订单表、50万行的用户表做了对比:

  • IN 子查询:8.3秒
  • JOIN 写法:0.4秒 差距超过20倍。

改写三步骤

  1. 把子查询中的表放到 FROM 里,用 JOIN 连接
  2. 如果原SQL用了 DISTINCT 或担心重复,加上 DISTINCT 或用 GROUP BY
  3. 确保 JOIN 的关联字段有索引(例如 orders.user_id 要有索引)

学会用JOIN改写子查询,是SQL优化的进阶门槛。以后看到 INEXISTS,先问问自己:子查询结果集大不大?大就改JOIN。这个习惯能帮你省下很多加班时间。

小耶在手,SQL不愁。

你遇到过子查询跑崩的情况吗?评论区分享一下。