MySQL exists 与 in 的性能及效率

注意:下面结果针对5.7版本,无论大表在前还是小表在前,in 的查询效率都要远远高于exists。但在8.0版本中,得益于版本的改善,in 的查询效率 与 exists 几乎没有明显差别。

MySQL 8 里面的优化可参考:Optimizing Subqueries with Materialization

MySQL 中的 in 语句是把外表和内表做 hash 连接,而 exists 语句是对外表做 loop 循环,每次 loop 循环再对内表进行查询。一直大家都认为 exists 比 in 语句的效率要高,这种说法其实是不准确的。需要分不同情况讨论。

exists 对外表用 loop 逐条查询,每次查询都会查看 exists 的条件语句,当 exists 里的条件语句能够返回记录行时(无论记录行有多少,只要能返回),条件就为真,返回当前 loop 到的这条记录,反之如果 exists 里的条件语句不能返回记录行,则当前 loop 到的这条记录被丢弃,exists 的条件就像一个 bool 条件,当能返回结果集则为 true,不能返回结果集则为 false。

实例一:exists 子句永远为 true

1
select * from user where exists (select 1);

对 user 表的记录逐条取出,由于子条件中的 select 1 永远能返回记录行,那么 user 表的所有记录都将被加入结果集,所以与 select * from user; 是一样的。

实例二:exists 子句永远为 false

这里假设 user 表的 userId 都是大于 0 的

1
select * from user where exists (select * from user where userId = 0);

可以知道对 user 表进行 loop 时,检查条件语句 select * from user where userId = 0,由于 userId 永远不为 0,所以条件语句永远返回空集,条件永远为 false,那么 user 表的所有记录都将被丢弃。

not exists 与 exists 相反,也就是当子查询有结果返回时,loop 到的记录将被丢弃,否则将 loop 到的记录加入结果集。

exists 的执行过程

总的来说,如果 A 表有 n 条记录,那么 exists 查询就是将这 n 条记录逐条取出,然后判断 n 遍 exists 条件。

in 的实际效果

in 查询相当于多个 or 条件的叠加,这个比较好理解,比如下面的查询:

1
select * from user where userId in (1, 2, 3);

等效于:

1
select * from user where userId = 1 or userId = 2 or userId = 3;

not in 与 in 相反,如下:

1
select * from user where userId not in (1, 2, 3);

等效于:

1
select * from user where userId != 1 and userId != 2 and userId != 3;

in 的执行过程

总的来说,in 查询就是先将子查询结果集拿出来,假设结果集为 B,共有 m 条记录,然后在将子查询条件的结果集分解成 m 个,再进行 m 次查询。

值得一提的是,in 查询的子条件返回结果必须只有一个字段,例如:

1
select * from user where userId in (select id from B);

而不能是:

1
select * from user where userId in (select id, age from B);

而 exists 就没有这个限制。

exists 和 in 的性能

考虑如下SQL语句

  1. select * from A where exists (select * from B where B.id = A.id);

  2. select * from A where A.id in (select id from B);

查询 1 可以转化为以下伪代码,便于理解:

1
2
3
4
5
6
7
8
9
10
$result = [];

for ($i = 0; $i < count(A); $i++) {
  $a = get_record(A, $i); // 从A表逐条获取记录

  if (B.id = $a[id]) // 如果子条件成立
    $result[] = $a;
}

return $result;

查询 1 主要是用到了 B 表的索引,A 表如何对查询的效率的影响应该不大。

假设B表的所有id为1,2,3,查询2可以转换为:

1
select * from A where A.id = 1 or A.id = 2 or A.id = 3;

这个就好理解了,这里主要是用到了 A 的索引,B 表如何对查询影响不大。

not exists 和 not in

  1. select * from A where not exists (select * from B where B.id = A.id);

  2. select * from A where A.id not in (select id from B);

查询 1 还是和上面一样,用了 B 的索引。

而对于查询 2,可以转化为如下语句:

1
select * from A where A.id != 1 and A.id != 2 and A.id != 3;

可以知道 not in 是个范围查询,这种 != 的范围查询无法使用任何索引,等于说 A 表的每条记录,都要在 B 表里遍历一次,查看 B 表里是否存在这条记录。

故 not exists 比 not in 效率高。

mysql 中的 in 语句是把外表和内表作 hash 连接,而 exists 语句是对外表作 loop 循环,每次 loop 循环再对内表进行查询。一直大家都认为 exists 比 in 语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。

如果查询的两个表大小相当,那么用 in 和 exists 差别不大

如果两个表中一个较小,一个是大表,则子查询表大的用 exists,子查询表小的用 in:

例如,表 A(小表),表 B(大表)

  1. select * from A where cc in (select cc from B) 效率低,用到了 A 表上 cc 列的索引;

select * from A where exists(select cc from B where cc=A.cc) 效率高,用到了 B 表上 cc 列的索引。

相反的

  1. select * from B where cc in (select cc from A) 效率高,用到了 B 表上 cc 列的索引;

select * from B where exists(select cc from A where cc=B.cc) 效率低,用到了 A 表上 cc 列的索引。

not in 和 not exists 如果查询语句使用了 not in 那么内外表都进行全表扫描,没有用到索引;而 not exists 的子查询依然能用到表上的索引。

所以无论那个表大,用 not exists 都比 not in 要快。