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

简介: 本文揭秘SQL子查询性能陷阱:IN慢因临时表+全量扫描;推荐JOIN改写——利用索引、避免磁盘IO。实测500万订单下,JOIN比IN快20倍!附三步改写法与NULL避坑指南。

我是小耶,干运营半路出家的野生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不愁。

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

相关文章
|
3月前
|
SQL 关系型数据库 MySQL
一张5000万行的表,加索引从45秒到0.02秒——索引设计你真的会吗
本文实测5000万订单表:无索引查询45秒,加索引后仅0.02秒(提升2250倍)。详解索引原理、建索引时机、联合索引最左前缀、覆盖索引及隐式转换陷阱,干货不啰嗦!
|
3月前
|
SQL 关系型数据库 MySQL
MySQL主从复制实战:从原理到读写分离,新手避坑全指南
数据库小学妹带你轻松入门主从复制!✅基于binlog实现主库写、从库读,支撑读写分离与高可用;🛡️保障数据安全(灾备)、提升并发能力;🔧详解三种复制模式、搭建步骤、延迟优化及避坑指南。运维进阶必备!
|
3月前
|
SQL 缓存 数据库
你还在用LIMIT 1000000,10?献上分页查询优化技巧
本文详解“深分页”陷阱:`LIMIT 1000000,10`为何慢?3种优化方案(游标法、子查询定位、延迟关联)实测提速数十倍,助你零成本提升SQL性能!
|
3月前
|
SQL 运维 关系型数据库
DBA必备技能:MySQL误删恢复完全指南(全量备份+binlog回放)
本文详解误删数据(如`DELETE FROM orders`)后的紧急恢复三步法:查Binlog→临时库回放→差异导回,并附4条血泪预防措施。不讲段子,只教能救命的操作!
|
2月前
|
人工智能 Cloud Native 关系型数据库
MySQL 8.4 LTS来了!从8.0到8.4,DBA必须知道的5个核心变化
MySQL 8.0社区版将于2026年结束生命周期,8.4 LTS作为首个长期支持版本,提供5年超长支持周期(至2031年)。本文从InnoDB并行查询、Redo Log动态容量、默认认证插件变更、参数默认值调整、云原生适配五个维度,梳理DBA升级前必须掌握的核心变化,并提供升级检查清单。
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
3月前
|
SQL 关系型数据库 MySQL
批量操作性能飙升:从30秒到1秒的三种实战方法
业务系统中经常需要批量导入或更新大量数据(如Excel上传、定时同步)。许多开发人员采用循环单条执行的方式,导致1万条数据耗时30秒以上,严重影响用户体验。本文从数据库IO、事务开销、锁竞争三个角度分析单条操作的性能瓶颈,并给出三种优化方案:批量INSERT、LOAD DATA文件导入、批量UPDATE用临时表。每种方案均附实测数据对比与适用场景说明,帮助读者在1万\~100万行级别批量操作中选择最优策略。
|
2月前
|
SQL JavaScript 关系型数据库
SQL改写实战:子查询、CTE、窗口函数性能对比
本文聚焦SQL性能优化,实测对比子查询、CTE与窗口函数在复杂统计、分组排名、递归查询等场景的执行效率。基于MySQL 8.0真实数据(千万级表),揭示窗口函数在“每组取最值”“部门排名”中提速3倍以上,CTE提升可读性与递归能力,而相关子查询易成性能瓶颈。干货满满,避坑必备!
|
3月前
|
关系型数据库 MySQL 测试技术
JOIN、IN、EXISTS谁最快?实测三种写法性能差异与执行计划深度剖析
本文用MySQL 8.0实测拆解`IN`/`EXISTS`/`JOIN`子查询性能:从执行计划、半连接优化、临时表开销等底层原理出发,结合10万+100万数据实测(`EXISTS`最快95ms),给出三条选型铁律——告别盲从“最佳实践”,只选最适配业务与数据的写法!
|
3月前
|
SQL JSON 关系型数据库
EXPLAIN执行计划深度解读:从type到cost,彻底读懂SQL为什么慢
本期深入解析`EXPLAIN`核心字段:用`key_len`判断索引使用列数,借`filtered`评估回表代价,并详解MySQL 8.0的`EXPLAIN ANALYZE`如何以真实执行数据替代估算,让SQL优化更精准、可验证。