当前位置: 首页 > news >正文

JOIN、IN、EXISTS谁最快?实测三种写法性能差异与执行计划深度剖析

关键词​:JOIN;IN子查询;EXISTS子查询;半连接;物化;执行计划;EXPLAIN;性能优化


大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

上周那篇关于子查询优化的文章发出来后,评论区炸了。有人说“用JOIN是错的”,有人说“EXISTS才是正解”。今天我不站队,直接用实测数据说话,从执行计划层面彻底拆解这三种写法。

1 问题背景:开发中的常见困惑

在日常开发里,INEXISTSJOIN的争论我听过无数遍:

  • “数据量不大的时候用IN最直观,为什么网上都说要改成EXISTS?”
  • “我的IN子查询明明有索引,为什么EXPLAIN还是显示全表扫描?”
  • NOT INNOT EXISTS结果一样吗?性能差多少?”
  • “子查询改写为JOIN后为什么结果里多了重复行?”

这些困惑的根源在于:不同写法在数据库优化器中的处理逻辑截然不同,而且优化器的选择还受到MySQL版本、数据分布、索引设计和统计信息的影响。今天我们就从底层执行路径开始讲起。

2 核心概念:理解IN、EXISTS、JOIN的执行逻辑

要判断谁更快,必须先理解优化器是如何执行这三种写法的。

2.1IN:物化子查询或半连接

SELECT * FROM users WHERE user_id IN (SELECT user_id FROM orders);

在MySQL 5.5及更早版本中,IN子查询的执行方式是​物化​:先完整执行子查询,将结果集存储在内部临时表中,然后外层查询再与该临时表进行匹配。这种方式在子查询结果集较大时,临时表的构建和磁盘I/O会成为主要瓶颈。

从MySQL 5.6开始,优化器引入了半连接优化。当满足一定条件(子查询无GROUP BY、无聚合、非相关子查询等)时,优化器会将IN子查询转换为类似JOIN的半连接执行路径,性能得到显著提升。

2.2EXISTS:半连接与匹配即停

SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);

EXISTS子查询是​相关子查询​:它会将外层查询的每一行代入子查询,一旦在内层表中找到匹配记录,就会立即停止扫描并返回结果。这使得EXISTS在处理存在性检查时非常高效。但它的开销高度依赖于外层数据量:如果外层表很大,内层索引够快,EXISTS依然高效;如果外层表巨大且内层索引不佳,逐行代入的成本也会很高。

2.3JOIN:笛卡尔积与去重代价

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

JOIN的执行路径是先将两张表按关联条件进行匹配,然后通过索引快速筛选出符合条件的行。如果users表中一个用户有多个订单,JOIN会产生重复行,因此必须使用DISTINCT去重。而DISTINCT在MySQL中通常需要创建临时表进行去重操作,当数据量较大时临时表可能溢出到磁盘,带来额外的性能损耗。

3 实测对比:同一场景下的三种写法

3.1 测试环境

  • 数据库:MySQL 8.0.33
  • users表:10万行,user_id为主键
  • orders表:100万行,user_id有二级索引
  • 目标:查询所有下过单的用户信息

3.2 三种写法的执行时间

写法平均耗时(3次运行)执行计划特点
IN128ms半连接,使用物化或索引
EXISTS95ms半连接,匹配即停
JOIN + DISTINCT236msDISTINCT产生临时表,大表时可能写磁盘

3.3 结果分析

在本测试环境中,EXISTS性能最优,IN次之,JOIN最慢。JOINDISTINCT临时表开销和重复行匹配是其主要性能瓶颈。但需要注意的是,这一结论依赖于特定数据分布和索引设计——如果子查询结果集极小,IN可能更快;如果需要同时返回两表的字段,JOIN则是唯一合理的选择。

4 执行计划深度解码:为什么会有这样的结果?

我们通过EXPLAINEXPLAIN FORMAT=TREE来观察优化器的决策过程。

4.1EXPLAIN输出对比

-- EXPLAIN for IN EXPLAIN SELECT * FROM users WHERE user_id IN (SELECT user_id FROM orders);

输出中关键信息:select_typePRIMARYtype=ALL(外层全表扫描),SUBQUERYtype=index(子查询索引扫描)。这意味着MySQL先执行子查询拿到所有user_id,再逐行匹配外层。

4.2EXPLAIN关键列解读

列名作用本次对比中的表现
select_type标识子查询类型INEXISTS均为半连接优化
type访问类型,ALL=全表扫描,ref=索引查找IN的驱动表为ALLEXISTS的驱动表为ref
Extra附加信息JOIN版本可能显示Using temporary
filtered过滤后剩余比例影响回表代价估算

5 选型决策指南:三条铁律

基于以上分析,我总结出三条实用的选择策略:

  1. ​**存在性检查(业务逻辑为“是否有订单”)**​:优先使用EXISTS。其“匹配即停”的机制和半连接优化,使其在大多数场景下性能最优且语义最清晰。
  2. 子查询结果集非常小(如几十行)且不重复​:IN的可读性最好,由于结果集极小,物化临时表的代价几乎可以忽略。
  3. 需要同时返回A表和B表的字段​:必须使用JOIN。但需通过业务逻辑判断是否需要DISTINCT去重,尽可能避免不必要的去重操作。

6 总结

没有绝对的“最快写法”,只有基于场景和数据特征的“最合适写法”。

  • EXISTS适合存在性检查,匹配即停,通常稳定性最好。
  • IN在子查询结果集极小时可读性最佳,性能也可接受。
  • JOIN在需要两表字段时不可替代,但需关注去重成本。

以后遇到这类问题,先问自己三个问题:业务是要判断存在还是取数据?子查询结果集大不大?能不能接受去重临时表?想清楚再写,比抄网上的“最佳实践”靠谱得多。

小耶在手,SQL不愁。

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

http://www.zskr.cn/news/1325611.html

相关文章:

  • 激光全息防伪标签哪家好?2026二维码防伪标签公司推荐:宏鑫源防伪测评 - 栗子测评
  • 开发过程中如何利用Taotoken的容灾路由保障服务高可用
  • 普通工程师堆起来的人海战术,作用其实很有限
  • 一切命运皆可破,我命由我不由天
  • 做芯片的人,为什么容易看不起管理岗?
  • Textractor:3分钟掌握游戏文本提取,轻松跨越语言障碍!
  • ARM Trace Buffer架构与调试优化实践
  • Win系统安装docker
  • 为ubuntu上的openclaw工具配置taotoken作为模型供应商
  • 不经意传输协议的外包化优化与实践
  • Kubernetes集群能耗监测:RAPL与Prometheus方案对比
  • ARMv8-A架构AArch64异常处理机制详解
  • 告别格式大战!用VSCode的Prettier插件拯救你的代码洁癖(含保存即格式化、快捷键技巧)
  • 源头电主轴厂家推荐!顺源精密专注进口电主轴维修,自研高速精密电主轴,告诉你电主轴哪家好,行业口碑优选 - 栗子测评
  • 别再硬背公式了!用Python手把手带你调参二维卡尔曼滤波(附完整代码与可视化对比)
  • 【公安基础知识】01
  • 手机店还会存在吗
  • 从手机待机到芯片发热:深入聊聊CMOS反相器那点‘电费’是怎么算出来的
  • 2026杭州弱电工程哪家专业?智能照明/监控安防系统/机房施工公司实力盘点 - 栗子测评
  • 2026杭州专业汽车4S店弱电智能化服务公司推荐:车牌识别系统/门禁道闸定制厂家实力解析 - 栗子测评
  • 人脸识别:用数据蒸馏训练高精度人脸识别模型
  • 从沙子到车辙(1.2):计算的梦想与破灭
  • 022、旋转变压器原理与解码
  • C语言嵌入式开发中的软件复位实现方法
  • 蓝桥杯C++选手必看:动态规划从入门到拿分,我用这5道题搞定了(附完整代码)
  • 【Java杂项】为什么 b += 1 可以,但 b = b + 1 会报错?类型提升与复合赋值详解
  • 态是相关,势是因果,感是具身,知是离身
  • Gdev 至 Rust 移植工程(七)
  • Arduino入门教程五|串口通信详解(3个实验+if条件判断,保姆级入门)
  • 2026年选对工作钢格板厂家,这三大核心标准决定你的采购成败