SQL优化-JOIN 优化实践

编程教程 > MySQL (35) 2025-04-23 11:38:25

问题 SQL 描述

问题 SQL 和执行计划是这样的:

explain SELECT
    t1.stru_id AS struId,
    ...
FROM cams_stru_info t1
    LEFT JOIN cams_mainframerel t2 ON t1.stru_id =t2.stru_id
WHERE t1.stru_state="1";
SQL优化:JOIN 优化实践_图示-1b0d50b7d5be4bf3afc8982493e4593a.png

这个 SQL 是非常简单的,关联条件 stru_id 在两张表中都是主键或者主键的第一个字段:

SQL优化:JOIN 优化实践_图示-4645b444b4a74b68835633ab2bc2f718.png

而把 left join 转化成 inner join 后,SQL的效率很高:

SQL优化:JOIN 优化实践_图示-e859f6c94886433682d3cc9890ec5cb4.png

从上述信息来看,这个 SQL 存在的问题有:

  1. 大表驱动小表,这肯定是不好的,t1表近11万行数据,为驱动表;t2表近1.9万行数据,为被驱动表。这主要是 left join 导致的,大部分情况下 left join 左表即驱动表,但是这里业务需求就是如此,没办法改变;
  2. 驱动表的筛选条件 stru_state = 1,这个字段是一个状态值,基数很小,不适合建索引,即使建索引也没有用,所以驱动表一定是全表扫描。这点根据业务需求,也没法改变,其实全表扫描对性能影响不大,后续会解释;
  3. 被驱动表关联字段明明有索引,但做了全表扫描(全索引扫描);
  4. 优化器选择使用的 join 算法为 BNL(Block Nested Loop),SQL执行是计算次数等于11万*1.9万,近20亿次计算,所以执行非常慢。

join 的两种算法:BNL 和 NLJ

在继续分析之前,先得介绍一下 join 的两种算法,方便大家理解后面我分析思路上的错误和心得。
首先是 NLJ(Index Nested-Loop Join)算法,以如下 SQL 为例:

select * from t1 join t2 on t1.a=t2.a

SQL 执行时内部流程是这样的:

  1. 先从 t1(假设这里 t1 被选为驱动表)中取出一行数据 X;
  2. 从 X 中取出关联字段 a 值,去 t2 中进行查找,满足条件的行取出;
  3. 重复1、2步骤,直到表 t1 最后一行循环结束。

这就是一个嵌套循环的过程,注意“Index”,所以这里前提是被驱动表的关联字段有索引,最明显的特征就是在被驱动表上查找数据时可以使用索引,总的对比计算次数等于驱动表满足 where 条件的行数。假设这里 t1、t2都是1万行,则只需要 1万次计算。

如果 t1、t2 的 a 字段都没有索引,还按照上述的嵌套循环流程查找数据呢?每次在被驱动表上查找数据时都是一次全表扫描,要做1万次全表扫描,扫描行数等于 1万+1万*1万,这个效率很低,如果表行数更多,扫描行数动辄几百亿,所以优化器肯定不会使用这样的算法,而是选择 BNL 算法,执行流程是这样的:

  1. 把 t1 表(假设这里 t1 被选为驱动表)满足条件的数据全部取出放到线程的 join_buffer 中;
  2. 每次取 t2 表一行数据,去 join_buffer 中进行查找,满足条件的行取出,直到表 t2 最后一行循环结束。

这个算法下,执行计划的 Extra 中会出现 Using join buffer(Block Nested Loop),t1、t2 都做了一次全表扫描,总的扫描行数等于 1万+1万。但是由于 join_buffer 维护的是一个无序数组,每次在 join_buffer 中查找都要遍历所有行,总的内存计算次数等于1万*1万。说句题外话,如果 join_buffer 维护的是一个哈希表的话,每次查找做一次判断就能找到数据,效率提升飞快,其实这就是 hash join 了,MySQL 8.0 已支持。另外如果 join_buffer 不够大放不下驱动表的数据,则要分多次执行上面的流程,会导致被驱动表也做多次全表扫描。

分析误区

回到分析过程,我一开始疑惑的点就在于:为什么被驱动表 t2 关联字段有索引,却没有使用 NLJ 算法,而是使用了 BNL 算法?显然如果使用 NLJ 算法,总的扫描行数等于 t1 的行数即 19万行,总的计算次数也只有19万次,效率是很高的。

因为是刚学到 join 算法这方面的知识,理解的不是很透彻,思路上一直纠结在算法这里,所以接下来我想的是禁用 BNL 算法,搜索了一下 hint 语法:"select /*+ NO_BNL() */ t1.* from ...",执行计划的结果却跟我预期的不一样:

SQL优化:JOIN 优化实践_图示-cb06f709b39545f09c65680c117a352e.png

这让我更迷惑了,明明没有使用 BNL 算法,为什么被驱动表还是做了全表扫描?是算法出了什么问题吗?还是 hint 产生了其他效果?

直到客户告诉了我答案,两表的关联字段字符集和校对规则不一样... 

关联参考:MySQL SQL语句性能分析/优化函数 explain详解- 字符集导致索引失效部分

得解释下为什么之前没有想这一点,因为前面提到 inner join 执行计划毫无问题,使用了 NLJ 算法,优化器选了小表 t2 做驱动表,被驱动表 t1 按索引查找,效率很高。

继续分析

得知原因后,关于算法的疑问突然就想通了,NLJ 和 BNL 算法的选择根本在于关联字段的索引:不是取决于有没有索引,而是被驱动表能不能使用到索引进行查找。所以这本质上是一个索引失效问题,逻辑上其实只推进了一步,但是因为对新知识的不自信,推理能力不足(之前自认为推理能力不错的...),这一步一直没有走出去,这应该是我最大的收获了。

然后还要解释另一个疑问:既然关联字段字符集和校对规则不一样,为什么 inner join 不受影响?left join 时却索引失效了?

来看个测试,下面是两张表,关联字段的字符集不一样:

CREATE TABLE `t3` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` char(50) CHARACTER SET utf8 DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_a` (`a`)) ;

SQL1 和 SQL3 都是选择了 t3 做驱动表,执行计划一样,都显示索引失效了,使用了 BNL 算法,被驱动表进行全表扫描:

SQL优化:JOIN 优化实践_图示-dd48aa0cdd8d48cf8e3b30b1cd540ab6.png

SQL2 和 SQL4 都是选择了 t4 做驱动表,执行计划一样,被驱动表按照索引查找,使用了 NLJ 算法:

SQL优化:JOIN 优化实践_图示-c5eecefd868042db9de5dda371343386.png

也就是说,在这个测试中,latin1 去 join utf8 时,索引是正常使用的,反过来则索引失效。又测试了 utf8 和 utf8mb4 的情况,utf8 join utf8mb4 正常,反过来则索引失效。为此我的猜测是:被驱动表字段的字符集更大时,索引可以正常使用,反之则索引失效。关于字符集这点就不继续探索了,希望能有这方面的高手来解答。

最后,SQL 改成 inner join 后使用 NLJ 算法的原因就很明了了:NLJ 算法的效率显然是高于 BNL 的,优化器做选择时当然要选择更高效的算法。虽然关联字段字符集不一样,但是按照小>大的顺序,索引还是可以正常使用,一旦索引可以使用,选择 NLJ 算法就是顺理成章的事了。

总结

  1. NLJ 和 BNL 算法的选择根本在于关联字段的索引:不是取决于有没有索引,而是被驱动表能不能使用到索引进行查找;
  2. join 查询关联字段字符集或者校对规则不一致导致的索引失效,跟关联顺序有关,当然规范一定是让各表关联字段的字符集和校对规则一致;
  3. join 的优化,最好的办法就是把 BNL 转化为 NLJ,也就是被驱动表关联字段加索引,并且保证其有效,更多的优化思路可以看参考资料。

另外,一个好消息是从 MySQL8.0.18 开始已经支持 hash join 了,原本选择 BNL 算法的场景会直接使用 hash join,效率提升不止一点点,简直就是 DBA 福音了。

参考资料

 


评论
User Image
提示:请评论与当前内容相关的回复,广告、推广或无关内容将被删除。

相关文章
常见优化技术我们先从工业实践角度总结,几条常见 MySQL 查询优化策略。索引优化为常用的查询条件(WHERE、JOIN、GROUP BY、ORDER BY)添
问题 SQL 描述问题 SQL 和执行计划是这样的:explain SELECT t1.stru_id AS struId, ...FROM cams_stru
案例问题描述有这么一个SQL,外查询 where 子句的 bizCustomerIncoming_id 字段,和子查询 where 字句的 cid 字段都有高效
派生表优化器可以使用两种策略来处理派生表引用(这也适用于视图引用):将派生表合并到外部查询中(5.7引入的优化策略 derived_merge);将派生表物化为
函数使用mysql&gtl; explain SELECT a.custid, b.score, b.xcreditscore, b.lrscore FROM(SELE
MySQL索引优化,MySQL索引类型,MySQL索引怎么用MySQL索引怎么创建这里将会通过一些简单得sql进行讲解
ExtraExtra 是 EXPLAIN 输出中另外一个很重要的列,该列显示MySQL在查询过程中的一些详细信息类型说明Using filesortMySQL有
MySQL慢查询优化_MySQL慢查询排查_MySQL慢查询设置配置
在导入sql备份文件到MySQL数据库中,无论物理机安装MySQL还是docker环境安装的MySQL,思路是一样的。首先,登录进入MySQL如果是物理的,则直接执行命令mysql-u-p&gt...
SQL : 原始不含子查询,排序正常SELECT id, goods_id, create_time, price FROM mkt_price_control
SQL SERVER数据库中SUBSTRING函数的使用SELECT SUBSTRING(a.receiveTime,0,20) formatReceiveTi
MySQL查询结果添加序号常规查询结果如下:SQL命令:SELECT * FROM test;​现在我们需要添加一行序列号,也就是1,2,3,4...那种
sql server 2016数据库导出表数据/表结构1.打开Microsoft SQL Server Management Studio工具选择要导出表的数据
mysql 数据库备份与还原命令1&gtl;导出某个数据库表结构(其他说明:-u 后面的root为用户名,-p后面的password为用户密码,dbname数据库名称)