MySQL子查询(IN)碰到的问题,深刻分析

在项目中碰到一个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的执行计划,结果很意外
mysql-EXPLAIN
子查询的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代替它”,应该理解为“尽力避免嵌套子查询,使用索引来优化它们”

相关文章
相关标签/搜索