Join连接算法
我们之间也说过join其实就是一次又一次的循环对比,A连接B,那么就是先找到A表所以复合条件的行数据,然后一条一条的循环扫描满足条件的B数据,最后输出结果。先看看这几个算法的对比表格:
算法类型 | 引入版本 | 核心机制 | 最佳场景 | 触发条件 |
---|---|---|---|---|
Nested Loop Join | 全版本 | 双重循环匹配 | 中小表关联 | 默认算法 |
Block Nested Loop | 全版本 | 缓冲块优化 | 无索引连接 | join_buffer_size 足够 |
Hash Join | 8.0+ | 哈希表加速 | 大表等值连接 | 无合适索引+等值条件 |
Batched Key Access | 5.6+ | 批量键访问 | 二级索引回表 | mrr=on |
1. 嵌套循环连接 (Nested Loop Join)
这个其实就是开头说的那种,也是mysql中最简单而且是默认的算法,其实这个算法也分两种一个是Simple Nested Loop Join,一个是Index Nested Loop Join,前者就是不使用索引的简单循环算法,假设A有100条,B也有100条,那么总共的成本就是100*100=1w,所以查询的成本其实是非常高的,而第二种就是使用索引,通过聚餐索引或者二级索引直接定位满足条件的行,性能会高很多。
2. 块嵌套循环连接 (Block Nested Loop)
这个其实就是把被驱动表的数据加载到内存块中(没有索引的情况下),本来A的数据扫描B需要加载到内存中,而且是扫描一次就加载一次很浪费资源,而这个就是直接把B表的一部分加载到内存中,然后进行循环匹配,不合适的就加载下一部分内存,下次循环还是会使用这个内存块,不会重复加载,但是这个是受到缓存块大小的限制,而且mysql8.2被Hash join替代了这个算法。
3. 哈希连接 (Hash Join)
这个就是为了替代Block Nested Loop而产生的算法,这个的限制就是必须是等值查询,而且在没有索引的情况下,不支持 LIKE
, >
, <
等范围条件,而且性能不足的情况下性能会显著下降,因为哈希表的特性就是找对应的桶,或者叫数据指针,这个就是为被驱动表建立哈希表,然后循环查找的时候就查询这个哈希表。这个是对比表格:
对比维度 | Hash Join | Block Nested Loop (BNL) |
---|---|---|
算法类型 | 哈希表查找 | 块嵌套循环 |
引入版本 | MySQL 8.0+ | 全版本支持 |
连接条件 | 仅支持等值连接(=) | 支持所有连接条件 |
内存使用 | 高(需构建哈希表) | 中(join_buffer缓存) |
执行阶段 | 构建阶段+探测阶段 | 单阶段块扫描 |
最佳场景 | 大表等值连接 | 中小表无索引连接 |
I/O特点 | 随机I/O为主 | 顺序I/O为主 |
4. 批量键访问 (Batched Key Access)
批量键访问 (Batched Key Access, BKA) 是 MySQL 针对二级索引回表查询的优化技术,通过将随机 I/O 转换为顺序 I/O 提升性能。这个是开启的条件:
条件类型 | 具体要求 | 检查方法 |
---|---|---|
系统配置 | optimizer_switch='batched_key_access=on' |
SHOW VARIABLES |
查询特征 | 使用二级索引且需要回表 | EXPLAIN 显示 Using index condition |
版本要求 | MySQL 5.6+ | SELECT VERSION() |
这个是优点:
优点 | 影响程度 | 产生效果 |
---|---|---|
减少随机I/O | ★★★★★ | 大幅降低延迟 |
提高缓存命中 | ★★★★ | 减少物理读 |
批量处理 | ★★★ | 降低系统调用 |
预读优化 | ★★ | 利用磁盘特性 |
这个是缺点:
缺点 | 影响程度 | 缓解方案 |
---|---|---|
内存消耗 | ★★★ | 调整join_buffer_size |
额外排序 | ★★ | 确保mrr_cost_based=off |
冷数据效果差 | ★★ | 预热缓存 |
版本限制 | ★ | 升级到5.6+ |
简单来说这个其实是搭配Nested Loop Join使用的,本来使用二级索引需要回表的,而且每找到一次就要回表一次,使用这个后会把需要回表的索引记录到内存缓存中,然后一次性回表,把大量的随机IO变成了一次的顺序IO性能提升还是很明显的,而且Mysql默认是开启的。
其实总结起来就是一句话,小表驱动大表,小结果集驱动大结果集,因为小结果驱动代表的就是减少循环的次数。
子查询优化
子查询大家应该都不陌生,子查询是mysql4的时候引入的一个功能,目的就是为了帮助我们完成更加复杂的业务场景,但是子查询其实本质上来说性能并不好,因为子查询会生成一个临时表,我们都知道无法给临时表添加索引(当然主包说的是隐式的临时表,我们显示声明的还是可以加索引的),那么如果一个表的数据集特别大,然后子查询又是select* 这种全字段,然后我们的主表再使用这个子查询的某个字段查询的时候,性能就是非常糟糕的,一个是没有索引,一个就是数据量特别大,而且在生成和最后删除临时表的时候,也是非常消耗系统资源的。所以我们需要对一些需要使用子查询的场景进行优化,从而提高sql的性能。
1. 子查询转为JOIN(最常用)
这个其实很好理解,因为一个是可以使用被驱动表的索引,一个是循环对比被驱动表的行数据,最后形成一个结果集,并不会产生一个中间的临时表,也就不存在大量的系统资源浪费了。
2.派生表条件下推(5.7+)以及减少列数量
这个其实也很好理解,把过滤条件下推到子查询中,不要在外层查询中再过滤,因为这样会降低临时表产生的数据集,从而导致性能提高,另外不要使用select * 只返回需要的字段,这样性能也会提高。
sql
-- 低效写法
SELECT * FROM (
SELECT * FROM orders WHERE status = 'completed'
) AS t WHERE t.amount > 1000;
-- 优化写法(条件下推)
SELECT * FROM (
SELECT * FROM orders WHERE status = 'completed' AND amount > 1000
) AS t;
3.EXISTS重写
这个其实在mysql中会被优化成半连接,也就是只保留true的驱动表行数据,为什么性能会提高其实是上面说过,转Join一样。
排序优化
1.使用limit
其实我们在进行排序的时候,就算有这个字段的索引,也可以会失效,因为二级索引是需要回表的,而且我们也知道Mysql优化器是以成本最低为前提的,假如使用索引比不使用索引成本更高,肯定就不会使用索引,就是说多个列的组合索引全部命中的情况下,所以使用limit进行分页的话,这样就会降低回表的数量集,所以使用索引的可能性会高。
2.索引顺序
这个就很好理解了,我们知道索引是有升序和降序的,默认是升序的索引,假如我们对一个字段进行降序排序,那么肯定就不会使用到索引的,我们需要更加业务情况添加降序索引。
3.文件排序算法------单路排序、双路排序
其实我们大部分的场景使用的都是文件排序,也就是把数据加载到内存,然后再进行排序,之前我们说降序排序的时候就知道了,文件排序是很消耗系统资源的,但是这个也是没办法的,因为我们不可能什么情况下都加索引对不对。所以我们有需要了解一下什么是单路排序什么是双路排序。
单路排序:也就是一次性把满足条件的数据全部加载到内存然后进行排序,缺点就是消耗内存(sort_buffer_size),优点就是比双路快,5.7以上版本默认这个算法。
双路排序:就是先把排序列和主键加载到内存排序,然后再批量回表获取数据,缺点就是2次IO会慢,但是消耗的内存会小很多。
总结
本篇主要讲的就是Join的连接算法以及子查询和排序优化的一些内容。