使用 JOIN 实现表关联,面试挂了!

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

前言

记得有一次去面试,面试官问了一道关于数据库的问题,问题大致内容如下:

面试官:你平时经常写sql吗?

我:肯定啊,这不是必备小技能吗。

面试官:那你知道多表数据之间有关联,你是怎么查数据的呢?

我:JOIN啊。

面试官:好用吗?

我:贼好用,我特别是LEFT JOIN,我用的贼熟练,天天写。

面试官:emm......,那你回去等通知吧。

不知道大家有没有遇到过这种问题,或者说大家平时是如何处理多表关联的数据的,是不是也用JOIN语句来处理呢。

我们暂且不直接下定义说JOIN语句有啥问题,先来看一个简单的例子。

测试数据准备

这里我使用的是mysql数据库,由于测试需要我们先创建两张表t/t2,语句比较简单

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) NOT NULL,
  `b` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `b` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
​
CREATE TABLE `t2` (
  `id` bigint(19) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `c` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
复制代码

再使用函数创建一些测试数据,我这里在 t 表中创建一千万条,在 t2 表中创建一万条。

CREATE DEFINER=`root`@`localhost` PROCEDURE `testdata`()
begin
 declare item int;
 set item=1;
 while(item<=10000000)do
 insert into t values(item, item, item);
 set item=item+1;
 end while;
end
复制代码

JOIN测试

先写一句小学生都会的语句测一下:

select t.*,t2.* from t left join t2 on t.a = t2.a 
复制代码

emm......不知道等了多久之后,还是没有结束,于是乎我就加了一个limit限制条件,只查询10条数据

select t.*,t2.* from t left join t2 on t.a = t2.a limit 10000,10
复制代码

这次等待的时间要少了不少,共计消耗 19.99s

image.png

这速度,感觉可以拎包袱回家种地了...

问题分析

想要搞明白为什么消耗这么长时间,就要先理解这个查询过程是怎么实现的,然后才能得出结论,JOIN究竟能不能被使用。

首先看一下执行计划

image.png

其中比较重要的三个参数:

  • type:判断是全表扫描还是索引扫描,这里我们是 ALL ,也就是全表扫描;
  • rows:找到最终结果的过程中,需要扫描的行数,越少越好;
  • Extra:不适合其他列显示的额外信息,本例中关注 Block Nested Loop

从上述执行计划中我们看到,为了拿到最终的 10条数据,我们需要扫描将近10000000万行数据,这个过程是非常耗时且浪费的。

整个过程可以理解为下面几步:

  1. 从表t 中读取一条数据R;
  2. 对比R中的a字段与表t2 比较;
  3. 将表t 全部数据加载到内存(join_buffer)中,逐行取出表t2数据,在内存中对比并找到满足条件 t.a = t2.a 的行,形成结果集;
  4. 重复上述步骤,直至 表t 中的数据全部遍历完成。

时间复杂度:

  • 单次查询消耗的时间复杂度为:M+N,M驱动表数据,N被驱动表数据。
  • 总时间复杂度:M*N。

初步解决

对数据库有过一点了解的小伙伴肯定问了,表t2 中为啥不给 字段a 加索引呢,加了不就快了吗。

没毛病,我们就先尝试加一下索引,然后看看查询同样的sql耗时多少。

image.png

oh my god,这快的可不是一点两点,现在查询一次仅仅耗时0.04s,简直快到飞起

再来看一下执行计划:

image.png

此时表t依旧为全表扫描,不同的是 表t2 中的type是ref,也就是说此时我们是用索引,再来看一下整个执行过程:

  1. 从表t 中读取一条数据R;
  2. 对比R中的a字段与表t2 比较;
  3. 根据索引a, 匹配满足条件 t.a = t2.a的行,形成结果集;
  4. 重复上述步骤,直至 表t 中的数据全部遍历完成。

可以发现,与上面的步骤相比,只是在第三步 ,查找被驱动表t2 时有所不同,每执行一次步骤3,就只需要执行两次搜索,即搜索索引a和主键索引,然后找到所需要的被驱动表中的数据。

时间复杂度:

  • 单次查询消耗的时间复杂度为:1 + 2 * log2N,N为被驱动表t2 的行数。
  • 总时间复杂度:M * (1 + 2 * log2N)* ,M驱动表数据,N被驱动表数据。

JOIN算法对比分析

上面分析了两种JOIN过程,即 使用索引和无索引状态下的JOIN过程:

  • Simple Nested-Loop Join

该算法就是嵌套循环,依次读取驱动表中的数据,对于每次取出的数据,再和被驱动表进行比较,比如本例中我们驱动表数据(10000000)* 被驱动表数据(10000) = 100000000000 亿次的时间复杂度,当然这种算法没有数据库会采用的

  • Block Nested-Loop Join

该算法就是我们没有新增索引时,MySQL的InnoDB引擎 就会使用这种算法,具体过程如上,时间复杂度和上述算法相同,都是M * N。

  • Index Nested-Loop Join

该算法是增加索引后,MySQL会使用该算法,具体过程上面也已经介绍过。

JOIN能不能用

如果我们使用 Index Nested-Loop Join 算法,其实是没有多大影响的,完全满足绝大多数场景的使用。

如果使用 Block Nested-Loop Join 算法,就会导致大量的扫描行数,尤其像本例中这种上千万,乃至上亿的数据量,这样会导致被驱动表扫描过多次,占用大量的系统资源和缓存资源,非常不建议使用。

至于开头提到的面试官的问题,其实是真的发生过的,面试官难道想得到的答案是在内存中处理表关联的数据,在sql执行时只查询单表吗,其实这种时间复杂度并没有减少,反而会增加sql执行次数,不知道大家是怎么使用的呢?

下一章我们就要探讨一下,当使用JOIN时如何对其进行优化?以及本例中的缓存(join_buffer)是什么,如何优化?以及其他提升查询效率的有效手段。

Guess you like

Origin juejin.im/post/7031185816495849509