MySQL的 JOIN 优化终极指南
目录
- 前言
- 序章:为何要有JOIN?——“一个好汉三个帮”的数据库哲学 🤝
- 第一章:JOIN的“七十二变”——常见JOIN类型速览 🎭
- 第二章:MySQL的“红娘秘籍”——JOIN执行原理大揭秘 🕵️♀️📖
- 2.1 简单嵌套循环连接 (Simple Nested Loop Join, SNLJ) - “老实人的笨办法” 🐢
- 2.2 索引嵌套循环连接 (Index Nested Loop Join, INLJ) - “聪明人的快捷方式” 🚀
- 2.3 块嵌套循环连接 (Block Nested Loop Join, BNL) - “批量相亲,减少跑腿” 🚌
- 2.4 MySQL 8.0+ 的新贵:哈希连接 (Hash Join) 🧙♂️
- 第三章:JOIN优化的“葵花宝典”——核心法则与实战技巧 🚀📖
- 1. 索引!索引!还是TMD索引!(最重要的事说三遍) 🔑🔑🔑
- 2. 驱动表的选择:“谁先动筷子”的艺术 🥢
- 3. 过滤条件要“给力”:尽可能早地减少结果集 📉
- 4. join_buffer_size:不是万能丹,合理使用才有效 💊
- 5. EXPLAIN:你的JOIN优化“导航仪” 🗺️
- 6. MySQL 8.0+ 的其他JOIN优化特性 (锦上添花) 🌸
- 7. JOIN查询的“七宗罪” (常见避坑指南) 🚫
- 第四章:实战演练——看个例子压压惊 👨🏫
- 总结:JOIN优化🧘♂️
🌟我的其他文章也讲解的比较有趣😁,如果喜欢博主的讲解方式,可以多多支持一下,感谢🤗!
其他优质专栏: 【🎇SpringBoot】【🎉多线程】【🎨Redis】【✨设计模式专栏(已完结)】…等
如果喜欢作者的讲解方式,可以点赞收藏加关注,你的支持就是我的动力
✨更多文章请看个人主页: 码熔burning
前言
大家好!又双叒叕是我,你们的老朋友,一个幽默的程序员。
今天,咱们来点更刺激的,聊聊那个让无数英雄竞折腰的——JOIN
查询优化!
你是不是也写过那种“九九八十一难”般的JOIN语句,一执行,MySQL就跟便秘似的,半天憋不出一个P(结果)?或者,你看着EXPLAIN
那一堆眼花缭乱的Nested Loop
、Using join buffer
,感觉智商被按在地上摩擦?
别慌!JOIN虽然复杂,但它不是“爱情魔咒”,只要你摸清了它的“脾气秉性”,掌握了正确的“撩妹技巧”(优化方法),它也能从“世纪大难题”变成你SQL工具箱里的“瑞士军刀”!
准备好了吗?系好安全带,咱们的“JOIN优化探索号”飞船,马上起航!目的地——高效JOIN的“幸福彼岸”!🚀💑
序章:为何要有JOIN?——“一个好汉三个帮”的数据库哲学 🤝
想象一下,你的数据被精心设计,分门别类地存放在不同的“小抽屉”(表)里。比如:
students
表:存放学生的基本信息(学号、姓名、班级ID)。classes
表:存放班级信息(班级ID、班主任、教室)。scores
表:存放学生的考试成绩(学号、科目、分数)。
现在,你想知道“火箭班所有学生的姓名及其各科成绩”,单靠一个“抽屉”肯定搞不定吧?你得把students
和scores
这两个抽屉打开,根据“学号”这个共同的线索,把它们关联起来。
JOIN
,就是数据库世界里的“联谊会主持人”! 它的核心任务,就是根据你指定的“共同话题”(连接条件),把来自不同表(抽屉)的相关数据行“拉郎配”,组合成一个更完整、更有意义的结果集。
没有JOIN
,数据就是一座座孤岛;有了JOIN
,数据才能汇聚成汪洋大海,展现出真正的价值!
第一章:JOIN的“七十二变”——常见JOIN类型速览 🎭
在MySQL的“联谊会”上,主持人(JOIN)会根据你的要求,采用不同的“配对策略”。咱们先来快速认识一下几位常见的“联谊会司仪”:
-
INNER JOIN
(内连接):最严格的“司仪”,只介绍那些在两个表里都能找到“共同话题”(匹配连接条件)的行。如果A表的某行在B表找不到伴儿,或者B表的某行在A表找不到伴儿,对不起,它俩都不能参加这场“内涵派对”。- 口头禅:“宁缺毋滥,非诚勿扰!”
- 写法:
SELECT ... FROM tableA INNER JOIN tableB ON tableA.col = tableB.col;
(或者直接SELECT ... FROM tableA, tableB WHERE tableA.col = tableB.col;
,效果类似,但推荐显式JOIN
)
-
LEFT JOIN
(左连接,也叫LEFT OUTER JOIN
):偏心眼的“司仪”,以左边的表(FROM
子句中先出现的表)为准。左表的每一行都会出现在结果中。- 如果右表有匹配的行,就正常配对。
- 如果右表没有匹配的行,右表相关的列会用
NULL
来填充,“强行配对,找不到对象就给你个空气伴侣”。 - 口头禅:“左边的都是爷,一个都不能少!右边的?能配就配,配不上拉倒(用NULL)!”
- 写法:
SELECT ... FROM tableA LEFT JOIN tableB ON tableA.col = tableB.col;
-
RIGHT JOIN
(右连接,也叫RIGHT OUTER JOIN
):跟LEFT JOIN
反过来,以右边的表为准。- 口头禅:“右边的都是姑奶奶,全都得伺候好!左边的?随缘吧!”
- 小技巧:很多时候,
A RIGHT JOIN B
都可以改写成B LEFT JOIN A
,效果一样,但LEFT JOIN
更常用,可读性可能更好。
-
FULL JOIN
(全连接,也叫FULL OUTER JOIN
):最大方的“司仪”,左边右边的客人一个都不落下!- 左表有匹配,右表有匹配:正常配对。
- 左表有,右表没有:左表数据显示,右表数据为
NULL
。 - 左表没有,右表有:右表数据显示,左表数据为
NULL
。 - 口头禅:“来者都是客,一个都不能少!找不到伴儿的,我给你们发‘安慰奖’(NULL)!”
- MySQL的“小遗憾”:MySQL本身不直接支持
FULL OUTER JOIN
关键字。但别灰心,你可以通过LEFT JOIN ... UNION ... RIGHT JOIN
(或者LEFT JOIN ... UNION ALL ... RIGHT JOIN WHERE A.key IS NULL
等变体) 来模拟实现全连接的效果。-- 模拟FULL JOIN (注意,对于匹配上的行会显示两次,如果想去重用UNION) SELECT * FROM tableA LEFT JOIN tableB ON tableA.id = tableB.id UNION ALL -- 或者 UNION 去重 SELECT * FROM tableA RIGHT JOIN tableB ON tableA.id = tableB.id WHERE tableA.id IS NULL; -- 只取右表有而左表没有的部分
-
CROSS JOIN
(交叉连接,也叫笛卡尔积):最“疯狂”的“司仪”,不做任何筛选,把A表的每一行和B表的每一行都强行“拉郎配”一次。- 如果A表有M行,B表有N行,结果集就会有 M * N 行!数据量一大,分分钟把你的数据库搞“爆炸”!🤯
- 口头禅:“管他三七二十一,全都给我配一遍!宁可错杀一千,不可放过一个(潜在组合)!”
- 写法:
SELECT ... FROM tableA CROSS JOIN tableB;
或者SELECT ... FROM tableA, tableB;
(不加任何WHERE
连接条件时) - 用途:正常业务中用得极少,除非你真的需要所有可能的组合(比如生成测试数据、某些特定的数学运算)。大多数情况下,如果你不小心写出了笛卡尔积,那很可能是你忘了加
ON
或WHERE
连接条件了!
了解了这些“司仪”的性格,我们才能更好地指挥它们干活。
第二章:MySQL的“红娘秘籍”——JOIN执行原理大揭秘 🕵️♀️📖
当MySQL收到一个JOIN请求后,它内部是怎么运作的呢?难道真的是挨个比较吗?不完全是,它也有一套自己的“相亲算法”。
在MySQL的早期版本以及很多情况下,JOIN操作的核心算法是嵌套循环连接 (Nested Loop Join, NLJ) 及其变种。
2.1 简单嵌套循环连接 (Simple Nested Loop Join, SNLJ) - “老实人的笨办法” 🐢
这是最原始、最容易理解,但也通常是最低效的一种。
-
算法描述:
- 选择一个表作为“外层表”(也叫驱动表,Driving Table)。
- 遍历外层表的每一行。
- 对于外层表的每一行,都去遍历“内层表”(也叫被驱动表,Driven Table)的所有行,找到匹配的行,然后组合输出。
-
伪代码示意:
FOR each row R1 in OuterTable:FOR each row R2 in InnerTable:IF R1 joins with R2 ON join_condition:Output (R1, R2)
-
性能噩梦:如果外层表有M行,内层表有N行,那么总的比较次数大约是 M * N!如果内外层表都没有索引,那每次在内层表查找都是全表扫描,I/O次数大约是
M + M*N
(外层扫一遍,内层扫M遍)。数据量一大,简直是“龟速行驶”。 -
MySQL的“嫌弃”:由于SNLJ效率太低,现代MySQL优化器会极力避免使用它,除非万不得已(比如连接条件极其复杂,没有任何索引可用)。
2.2 索引嵌套循环连接 (Index Nested Loop Join, INLJ) - “聪明人的快捷方式” 🚀
当被驱动表(内层表)的连接字段上有索引时,情况就大不一样了!INLJ闪亮登场!
-
算法描述:
- 选择一个表作为“外层表”(驱动表)。
- 遍历外层表的每一行。
- 对于外层表的每一行,不再全表扫描内层表,而是拿着外层表的连接字段值,通过内层表连接字段上的索引,直接“精准定位”到内层表中匹配的行。
-
伪代码示意:
FOR each row R1 in OuterTable:LOOKUP R2 in InnerTable USING INDEX ON InnerTable.join_column WHERE InnerTable.join_column = R1.join_column_value:IF R2 is found:Output (R1, R2)
-
性能飞跃:
- I/O次数:如果外层表M行,内层表通过索引查找(假设是B+树索引,理想情况是
logN
或常数级别),总的I/O次数大约是M + M * (索引查找成本)
。相比SNLJ的M + M*N
,效率提升是数量级的! EXPLAIN
中的信号:当你用EXPLAIN
分析JOIN语句时,如果看到被驱动表的type
是eq_ref
(对于唯一索引/主键连接) 或ref
(对于普通二级索引连接),通常就意味着用上了INLJ,这是个好兆头!
-- 假设 students.class_id 和 classes.id 都有索引,且 classes.id 是主键 EXPLAIN SELECT s.name, c.class_name FROM students s INNER JOIN classes c ON s.class_id = c.id;-- 可能的EXPLAIN结果:
id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE s ALL idx_class_id NULL NULL NULL 1000 1 SIMPLE c eq_ref PRIMARY PRIMARY 4 test.s.class_id 1 – (这里students是驱动表,classes是被驱动表,classes.id用了主键索引,type=eq_ref,完美!)
- I/O次数:如果外层表M行,内层表通过索引查找(假设是B+树索引,理想情况是
2.3 块嵌套循环连接 (Block Nested Loop Join, BNL) - “批量相亲,减少跑腿” 🚌
当被驱动表(内层表)的连接字段上没有可用索引时,SNLJ太慢,MySQL又不想坐以待毙,于是就有了BNL。这通常是MySQL在无法使用INLJ时的“无奈之举”。
-
算法描述:
- 选择一个表作为“外层表”(驱动表)。
- 开辟一块内存区域,叫做Join Buffer(大小由
join_buffer_size
参数控制)。 - 从外层表一次性读取一批行(比如10行、100行,取决于Join Buffer能装多少),把这些行的连接字段值和需要查询的列都放到Join Buffer里。
- 然后,全表扫描一次内层表。
- 对于内层表的每一行,都跟Join Buffer中缓存的所有外层表行进行匹配。
- 重复步骤3-5,直到外层表的所有行都处理完毕。
-
伪代码示意:
Initialize JoinBuffer FOR each row R1 in OuterTable:IF JoinBuffer is full:FOR each row R2 in InnerTable: // Scan InnerTable onceFOR each buffered_R1 in JoinBuffer:IF buffered_R1 joins with R2 ON join_condition:Output (buffered_R1, R2)Clear JoinBufferStore R1's relevant columns in JoinBuffer// Process any remaining rows in JoinBuffer (last batch) IF JoinBuffer is not empty:FOR each row R2 in InnerTable: // Scan InnerTable again (potentially)FOR each buffered_R1 in JoinBuffer:IF buffered_R1 joins with R2 ON join_condition:Output (buffered_R1, R2)
-
性能特点:
- 减少了内层表的扫描次数:相比SNLJ(内层表扫M遍),BNL中内层表被扫描的次数是
外层表总行数 / Join Buffer能容纳的外层表行数
。如果Join Buffer足够大,甚至可能只扫描内层表一次(理想情况,很少见)。 - 依然是磁盘I/O大户:因为内层表还是全表扫描。
EXPLAIN
中的信号:Extra
列出现Using join buffer (Block Nested Loop)
。
- 减少了内层表的扫描次数:相比SNLJ(内层表扫M遍),BNL中内层表被扫描的次数是
-
join_buffer_size
的关键:这个参数的大小直接影响BNL的效率。Buffer越大,一次能缓存的外层表行越多,内层表的扫描次数就越少。但要注意,这个Buffer是每个连接独享的,设置过大可能导致内存问题(详见上一篇《全局参数优化》)。
2.4 MySQL 8.0+ 的新贵:哈希连接 (Hash Join) 🧙♂️
从MySQL 8.0.18版本开始,当JOIN操作无法使用索引(即BNL的适用场景)时,MySQL引入了更高效的哈希连接 (Hash Join) 算法,并在后续版本中逐渐用它来替代BNL。
-
算法描述 (简化版):
- 选择一个表作为“构建表”(通常是较小的那个表)。
- 读取构建表的所有行,根据连接字段计算哈希值,并在内存中构建一个哈希表 (Hash Table)。哈希表的Key是哈希值,Value是指向原始行的指针或行数据本身。
- 然后,选择另一个表作为“探测表”(通常是较大的那个表)。
- 逐行读取探测表,对于每一行,同样根据连接字段计算哈希值。
- 用这个哈希值去哈希表中“探测”是否存在匹配的行。如果哈希值匹配,再进一步比较原始连接字段的值是否精确相等。
- 如果匹配成功,则组合输出。
-
性能优势:
- 通常比BNL效率更高,尤其是在连接的表都比较大,且Join Buffer无法完全容纳构建表时。
- 构建哈希表和探测哈希表的操作,平均时间复杂度接近O(1)。
-
EXPLAIN
中的信号:在MySQL 8.0.18+,如果JOIN无法使用索引,你可能会在Extra
列看到类似Using hash join
或在EXPLAIN FORMAT=JSON
的输出中看到hash_join
的执行计划。 -
内存依赖:哈希连接也需要内存来构建哈希表。如果内存不足以容纳整个构建表的哈希表,MySQL可能会采用更复杂的“分块哈希连接”或“溢出到磁盘的哈希连接”,性能会有所下降,但通常仍优于BNL。
-
替代BNL:在MySQL 8.0.20及更高版本中,BNL被哈希连接完全取代。也就是说,即使你看到
Using join buffer
,其底层实现也可能是哈希连接了。
小结JOIN执行算法:
MySQL优化器会“绞尽脑汁”地选择最高效的JOIN算法。它的首选永远是INLJ(用上索引),因为这通常是最快的。如果实在没办法用索引,它在8.0之前会退而求其次用BNL,在8.0.18之后则更倾向于用更强大的Hash Join。
第三章:JOIN优化的“葵花宝典”——核心法则与实战技巧 🚀📖
知道了MySQL是如何“相亲”的,我们就能对症下药,写出让它“一见钟情”的JOIN语句了!
1. 索引!索引!还是TMD索引!(最重要的事说三遍) 🔑🔑🔑
这是JOIN优化的第一金科玉律,没有之一!
- 给谁加索引?
- 连接条件中的列:
ON tableA.col1 = tableB.col2
,那么tableA.col1
和tableB.col2
都应该是索引候选者。尤其是被驱动表(内层表)的连接列,必须要有索引! WHERE
子句中用于筛选的列。ORDER BY
和GROUP BY
中用到的列。
- 连接条件中的列:
- 索引类型:B-Tree索引是JOIN的好朋友。
- 数据类型要一致:确保连接字段的数据类型、字符集、排序规则都完全一致。如果类型不匹配(比如一个
INT
,一个VARCHAR
),MySQL可能需要进行隐式类型转换,这会导致索引失效!- 坏例子:
ON users.user_id (INT) = orders.user_id_str (VARCHAR)
-> 索引可能失效! - 好例子:
ON users.user_id (INT) = orders.user_id (INT)
- 坏例子:
- 避免在连接字段上使用函数:和普通查询一样,
ON FUNC(tableA.col) = tableB.col
也会让tableA.col
上的索引失效(除非你用了MySQL 8.0的函数索引)。
段子手吐槽:
不给JOIN列加索引,就像派了一个近视800度的士兵去战场上肉眼索敌,然后你还怪他打不准?!给他配个“八倍镜”(索引)啊,大哥!
2. 驱动表的选择:“谁先动筷子”的艺术 🥢
在嵌套循环类的JOIN中(SNLJ, INLJ, BNL),驱动表(外层表)的选择对性能有很大影响。
- MySQL优化器的选择:
- 通常,MySQL优化器会尝试选择结果集行数较少的那个表作为驱动表(在应用了
WHERE
条件过滤之后)。因为驱动表会被完整扫描(或部分扫描),它的行数越少,外层循环的次数就越少。 - 对于
INNER JOIN
,优化器有权调整表的连接顺序。 - 对于
LEFT JOIN
,左表固定为驱动表。 - 对于
RIGHT JOIN
,右表固定为驱动表(或者MySQL可能将其改写为等价的LEFT JOIN
再处理)。
- 通常,MySQL优化器会尝试选择结果集行数较少的那个表作为驱动表(在应用了
- 人工干预:
STRAIGHT_JOIN
:
如果你觉得MySQL优化器选的驱动表“不够明智”(比如统计信息不准导致误判),你可以用STRAIGHT_JOIN
关键字来“强制”指定连接顺序。SELECT ... FROM tableA STRAIGHT_JOIN tableB ...
会强制tableA
作为驱动表。- 何时使用? 仅当你有充分的理由和测试数据证明优化器选错了,并且
STRAIGHT_JOIN
能带来明显性能提升时才考虑。大多数情况下,相信优化器。 - 风险:如果你的判断是错的,
STRAIGHT_JOIN
反而可能让性能更差。
- 何时使用? 仅当你有充分的理由和测试数据证明优化器选错了,并且
驱动表选择原则(通用思路):
- 小表驱动大表:尽量让行数较少的表(经过
WHERE
过滤后)作为驱动表。 - 被驱动表连接列有索引是前提:无论谁驱动谁,保证被驱动表的连接列上有高效索引是必须的。
3. 过滤条件要“给力”:尽可能早地减少结果集 📉
-
WHERE
子句 VSON
子句:- 对于
INNER JOIN
:WHERE
和ON
中的条件在逻辑上是等价的,MySQL优化器可能会重新安排它们的执行顺序。但通常建议连接相关的条件写在ON
中,单表筛选条件写在WHERE
中,更清晰。 - 对于
LEFT JOIN
/RIGHT JOIN
(OUTER JOIN):ON
和WHERE
的条件位置非常重要!ON
条件:是在生成临时连接结果集之前就用来筛选被驱动表(对于LEFT JOIN是右表,对于RIGHT JOIN是左表)的记录的。如果被驱动表的记录不满足ON
条件,它就不会参与连接,其对应的驱动表行在结果中相关列为NULL。WHERE
条件:是在临时连接结果集(驱动表所有行 + 匹配上的被驱动表行或NULL)生成之后,再对这个结果集进行最终的筛选。- 关键区别:如果把针对被驱动表的筛选条件错放到
WHERE
子句中,对于OUTER JOIN,可能会导致本应保留的驱动表行(因为OUTER JOIN的特性)因为被驱动表部分为NULL而不满足WHERE
条件,从而被错误地过滤掉,使得OUTER JOIN的行为退化成类似INNER JOIN。 - 最佳实践:对于OUTER JOIN,如果想根据被驱动表的列来限制哪些行可以参与连接,务必把这些条件写在
ON
子句里!-- 需求:查询所有学生及其数学课的成绩(没有数学成绩的也显示学生,成绩为NULL) -- 正确写法 (筛选数学课的条件在ON里) SELECT s.name, sc.score FROM students s LEFT JOIN scores sc ON s.student_id = sc.student_id AND sc.subject = '数学';-- 错误写法 (筛选数学课的条件在WHERE里,会导致没有数学成绩的学生整行被过滤掉) SELECT s.name, sc.score FROM students s LEFT JOIN scores sc ON s.student_id = sc.student_id WHERE sc.subject = '数学'; -- 这实际上变成了INNER JOIN的效果
- 对于
-
尽早过滤:通过在
WHERE
子句或ON
子句中添加有效的筛选条件,尽早地把不需要的数据行给“咔嚓”掉,这样参与JOIN运算的行数就少了,性能自然提升。
4. join_buffer_size:不是万能丹,合理使用才有效 💊
- 何时起作用:只有当JOIN操作无法使用索引,MySQL被迫使用BNL或Hash Join时,
join_buffer_size
才派得上用场。 - 不是越大越好:它是个“连接独享”的内存区域。如果设太大,并发连接一多,内存就爆了。
- 优先解决索引问题:调大
join_buffer_size
是“治标不治本”的。首要任务永远是检查并优化JOIN列的索引! - 文档建议:除非你确定有很多无法避免的、需要大量内存的无索引JOIN,否则不建议将此值设得过大。几MB到十几MB通常是上限。
5. EXPLAIN:你的JOIN优化“导航仪” 🗺️
不厌其烦地再次强调EXPLAIN
的重要性!对于任何你觉得慢的JOIN查询,第一件事就是把它扔给EXPLAIN
“体检”一下。
- 重点关注的列:
table
: 表的读取顺序(大致反映了驱动表和被驱动表的顺序)。type
: 连接类型!这是判断JOIN效率的核心。- 最佳:
system
>const
>eq_ref
(唯一索引/主键JOIN) >ref
(普通二级索引JOIN) - 较差:
ref_or_null
>index_merge
>unique_subquery
>index_subquery
- 糟糕:
range
>index
(全索引扫描) >ALL
(全表扫描) - 对于JOIN,如果被驱动表的
type
是eq_ref
或ref
,说明索引用上了,很好!如果是ALL
或index
,那就要警惕了,可能是BNL或Hash Join。
- 最佳:
possible_keys
: 可能用到的索引。key
: 实际用到的索引。如果是NULL
,说明没用上索引。key_len
: 用到的索引长度。越短越好(在能区分记录的前提下)。ref
: 显示了哪些列或常量被用于索引查找。rows
: MySQL估计需要扫描的行数。越小越好。Extra
: 包含大量重要信息!Using index
: 覆盖索引,非常好!Using where
: 使用了WHERE子句进行过滤。Using temporary
: 可能用了临时表(比如GROUP BY
或UNION
操作)。Using filesort
: 文件排序,性能杀手,需要优化ORDER BY
或相关索引。Using join buffer (Block Nested Loop)
: 说明用了BNL算法。Using join buffer (Batched Key Access)
: BKA是一种优化的BNL,结合了MRR(Multi-Range Read)。Using hash join
(MySQL 8.0.18+): 说明用了哈希连接。Not exists
: 用于反连接优化。
通过仔细解读EXPLAIN
的输出,你就能诊断出JOIN的瓶颈在哪里,是索引没用上?还是驱动表选错了?还是Join Buffer太小(或者说,应该加索引)?
6. MySQL 8.0+ 的其他JOIN优化特性 (锦上添花) 🌸
除了Hash Join,MySQL 8.0还在JOIN优化方面做了一些其他改进:
- Lateral Derived Tables (LATERAL关键字):MySQL 8.0.14引入。允许派生表(子查询)引用FROM子句中在它之前定义的表的列。这可以实现一些以前很难或效率低下的“依赖性JOIN”。
- 优化器对IN子查询的转换:很多
IN (SELECT ...)
的子查询会被优化器转换为更高效的JOIN。 - 更智能的代价模型:优化器在选择执行计划时,会基于更精确的成本估算。
7. JOIN查询的“七宗罪” (常见避坑指南) 🚫
- “无情”笛卡尔积:忘了写
ON
或WHERE
连接条件,或者条件写错导致全匹配。 - “裸奔”连接列:连接字段没有索引,或者索引失效(类型不匹配、用函数等)。
- “贪婪”SELECT:明明只需要几列,却非要
SELECT *
,尤其是在JOIN大表时,会增加大量不必要的IO和网络传输,也可能让覆盖索引失效。按需索取,才是王道! - “迷糊”OUTER JOIN条件:把本该放在
ON
里的被驱动表筛选条件,错放到了WHERE
里,导致结果不符合预期。 - “臃肿”大事务JOIN:在一个超大的事务里执行复杂的JOIN,长时间锁住资源,影响并发。
- “盲目”相信优化器/“过度”人工干预:既不能完全不看
EXPLAIN
就上线,也不能芝麻大点事就用STRAIGHT_JOIN
或FORCE INDEX
。先理解,再优化。 - “忽视”数据分布和统计信息:如果表的统计信息严重过时或不准确,优化器可能会做出错误的执行计划。定期
ANALYZE TABLE
可能有助于更新统计信息。
第四章:实战演练——看个例子压压惊 👨🏫
假设我们有两张表:
employees
(员工表):emp_no
(PK),first_name
,last_name
,hire_date
,dept_no
(FK, 有索引)departments
(部门表):dept_no
(PK),dept_name
查询需求:找出所有在 ‘Sales’ 部门,并且是在 '2023-01-01’之后入职的员工姓名。
糟糕的写法 (可能):
-- 假设departments表非常大,employees表相对较小
SELECT e.first_name, e.last_name
FROM departments d, employees e -- 隐式JOIN,容易写漏条件
WHERE d.dept_name = 'Sales'AND e.hire_date > '2023-01-01'AND e.dept_no = d.dept_no; -- 连接条件放最后,可读性稍差
优化思路与较好的写法:
- 明确JOIN类型和连接条件:使用显式
INNER JOIN
。 - 索引检查:确保
employees.dept_no
,departments.dept_no
,employees.hire_date
都有索引。departments.dept_name
也最好有索引,如果经常用它查询。 - 驱动表考量:
- 如果
departments.dept_name = 'Sales'
能筛选出很少的部门(比如就1个),那么departments
作为驱动表可能更好。 - 如果
employees.hire_date > '2023-01-01'
能筛选出很少的员工,那么employees
作为驱动表可能更好。 - MySQL优化器通常会尝试估算。
- 如果
推荐写法:
SELECT e.first_name, e.last_name
FROM employees e
INNER JOIN departments d ON e.dept_no = d.dept_no -- 连接条件清晰
WHERE d.dept_name = 'Sales' -- 筛选条件1AND e.hire_date > '2023-01-01'; -- 筛选条件2-- 使用EXPLAIN分析:
EXPLAIN SELECT e.first_name, e.last_name
FROM employees e
INNER JOIN departments d ON e.dept_no = d.dept_no
WHERE d.dept_name = 'Sales'AND e.hire_date > '2023-01-01';
EXPLAIN结果分析要点:
- 看哪个表是驱动表,哪个是被驱动表。
- 看被驱动表的
type
是不是eq_ref
或ref
。 - 看
key
列是否都用上了合适的索引。 - 看
rows
列估算的扫描行数是不是尽可能小。 - 看
Extra
列有没有Using filesort
或不希望出现的Using join buffer
。
如果发现性能不佳,比如EXPLAIN
显示某个表的type
是ALL
,那就要重点检查该表的连接列和WHERE
条件列的索引情况。
总结:JOIN优化🧘♂️
呼!关于MySQL的JOIN优化,咱们今天这趟“星际穿越”算是把主要景点都逛了一遍。从JOIN的种类、执行原理,到各种优化秘籍和避坑指南,信息量确实不小。
但记住,JOIN优化不是一门“玄学”,它是有章可循的科学。核心就三点:
- 让索引飞起来! (尤其是被驱动表的连接列)
- 让数据量小下去! (通过有效的
WHERE
和ON
条件尽早过滤) - 让算法跑起来! (理解MySQL如何选择JOIN算法,并创造条件让它选最优的)
而这一切的基础,都离不开你对EXPLAIN
输出的“火眼金睛”般的解读能力。
JOIN优化,就像当一个数据库界的“月老”,你的目标就是用最少的“相亲成本”(系统资源),让合适的“男女嘉宾”(数据行)最高效地“牵手成功”(组合成结果)。这需要你对双方(表结构、数据分布、索引情况)都有深入的了解,还需要一点点“成人之美”的耐心和智慧。