在项目中碰到一个SQL的慢查询,查阅以后发现是由于SQL中使用了IN子查询,也许大部分有开发经验的人都会语重心长的告诉你“千万别用IN,使用JOIN或者EXISTS代替它”。好吧,我认可我不喜欢这句话,由于任何事物都有它存在的理由,因此今天来探讨一下IN关于子查询的问题html
首先定义一下表结构,假如如今有3张表employee,user,user_dept它们分别是雇员表,用户表,用户部门关系表java
CREATE TABLE `employee` ( `id` int(20) NOT NULL AUTO_INCREMENT `user_id` varchar(50) DEFAULT NULL, `dept_id` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ID` (`id`) USING HASH ) ENGINE=InnoDB CREATE TABLE `user_dept` ( `id` int(22) NOT NULL AUTO_INCREMENT, `user_id` varchar(50) DEFAULT NULL, `dept_id` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ID` (`id`) USING HASH ) ENGINE=InnoDB CREATE TABLE `user` ( `id` varchar(50) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `ID` (`id`) ) ENGINE=InnoDB
如今须要查询:某一个特定用户所在部门下的全部人(听起来有点拗口)mysql
首先咱们尝试使用含有IN的子查询SQL语句算法
select a.* from employee a, user b where a.user_id = b.id and a.dept_id IN (select dept_id from user_dept where user_id = 'specific user_id')
运行SQL后发现时间是6.783s(真实的employee表有5000多条记录,user表有3000多条记录, user_dept表有1000多条记录)sql
数据量并很少的状况下居然如此耗时,看来IN中嵌套子查询确实不是什么好主意,观察一下该SQL的日志发现Handler_read_rnd_next = 6677791,这意味着MYSQL在处理过程当中扫描(遍历)表多达百万级别,怪不得运行的时候会如此的长express
为了找出缘由我尝试将子查询分开分别执行2次SQLapp
select a.* from employee a, user b where a.user_id = b.id
select dept_id from user_dept where user_id = 'specific user_id'
运行结果都是在毫秒级别,而且第一个sql的Handler_read_rnd_next = 5179,第二个sql的Handler_read_rnd_next = 1280 ide
细心的人会发现1280 乘以 5179 约等于 6677791, 这是否是意味着加入IN中含有子查询,外围的查询每遍历一次都须要在重复执行子查询中的语句,也就是说IN中含有子查询的算法复杂度为
$$O(M * N) 其中M为外围查询的时间,N为子查询的时间$$oop
接下来我尝试使用JOIN语句来执行SQL优化
select a.* from employee a, user b, user_dept c where a.user_id = b.id and a.dept_id = c.dept_id and a.user_id = 'specific user_id'
执行时间在毫秒级别,而且Handler_read_rnd_next = 6459,而后我发现当表作JOIN查询时候,遍历表的总数 约等于 表中记录总数的总和,算法复杂度为$$O(M + N) 其中M,N为关联表的总记录数$$
可是使人疑惑的地方出现了为什么在IN中使用独立子查询(和外围查询没有任何关联)的算法复杂度变成$$O(M * N)$$难道MYSQL不该该是先将独立子查询只运行一次,而后在由外围查询当条件使用这样的效率最高吗?伪代码以下:
// 根据上面使用IN子查询的SQL来编写伪代码,使用最简单的Nested-Loop Join来实现 // 1.执行一次子查询,获取到子查询的结果集合 Set subSet = subquery(); // 2.遍历外围查询 for(employee e : employeeList) { for(user u : userList) { // 根据条件过滤数据,这步相对于使用IN来判断 if(subSet.contains(e.dept_id) && e.user_id == u.user_id) { // 输出数据 sys.out(); } } }
理想中的算法复杂度应该是$$O(S1 + N M C)$$
其中S1为子查询的算法复杂度, N为employee表的数量,M为user表的数量,C为子查询结果集的大小,通常为常数
复杂度为$$O(N^2)$$
若是user表中的user_id有作索引的话,其算法复杂度为:$$O(N)$$
这已是至关快的速度了,为了验证个人想法,我使用EXPLAIN命令查看MYSQL的执行计划,结果很意外
子查询的select_type居然是DEPENDENT SUBQUERY(相关子查询),可是很显然咱们的SQL子查询中并无和外围查询有关联的条件,难道MYSQL作了什么特殊的优化?为了考清楚这个问题,我尝试在MYSQL官方手册寻找答案,结果查找到一篇文章Optimizing Subqueries with EXISTS Strategy
其中有这么一段:
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
MySQL evaluates queries “from outside to inside.” That is, it first obtains the value of the outer expression outer_expr, and then runs the subquery and captures the rows that it produces.
A very useful optimization is to “inform” the subquery that the only rows of interest are those where the inner expression inner_expr is equal to outer_expr. This is done by pushing down an appropriate equality into the subquery's WHERE clause. That is, the comparison is converted to this:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)
大概意思就是说当MYSQL碰到IN子查询时MYSQL的优化器会将IN子查询转化为EXISTS相关子查询
因此我猜测MYSQL应该是在执行的时候把咱们上面的SQL改为了
select a.* from employee a, user b where a.user_id = b.id and EXISTS(select 1 from user_dept c where c.user_id = 'specific user_id' and a.dept_id = c.dept_id)
使用EXPLAIN命令查看执行计划发现和使用IN的使用彻底同样
Oop~为何MYSQL要作如此“多此一举”的事呢?
for(employee e : employeeList) { for(user u : userList) { for(user_dept ud : user_deptList) { if(ud.dept_id == a.dept_id && e.user_id == u.user_id) { // 输出数据 sys.out(); } } } }
算法复杂度为$$O(N^3)$$
关于MYSQL使用IN子查询的问题就暂时告已段落了,因为本人水平有限,不能再更深刻的研究下去,关于为何MYSQL会将IN中的子查询转化为EXISTS中的相关子查询,若是有哪位高手知晓缘由请告知
对于那句“千万别用IN,使用JOIN或者EXISTS代替它”,应该理解为“尽力避免嵌套子查询,使用索引来优化它们”