关于java:不要再问我-inexists-走不走索引了

44次阅读

共计 9215 个字符,预计需要花费 24 分钟才能阅读完成。

微信搜『烟雨星空』,获取最新好文。

前言

最近,有一个业务需要,给我一份数据 A,把它在数据库 B 中存在,而又比 A 多出的局部算进去。因为数据比拟芜杂,我这里简化模型。

而后就会发现,我去,这不就是 not in,not exists 嘛。

那么问题来了,in, not in , exists , not exists 它们有什么区别,效率如何?

已经从网上据说,in 和 exists 不会走索引,那么事实真的是这样吗?

带着疑难,咱们钻研上来。

留神: 在说这个问题时,不阐明 MySQL 版本的都是耍流氓,我这里用的是 5.7.18。

用法解说

为了不便,咱们创立两张表 t1 和 t2。并别离退出一些数据。(id 为主键,name 为一般索引)

-- t1
DROP TABLE IF EXISTS `t1`;
CREATE TABLE `t1` (`id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `address` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_t1_name` (`name`(191)) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1009 DEFAULT CHARSET=utf8mb4;

INSERT INTO `t1` VALUES ('1001', '张三', '北京'), ('1002', '李四', '天津'), ('1003', '王五', '北京'), ('1004', '赵六', '河北'), ('1005', '杰克', '河南'), ('1006', '汤姆', '河南'), ('1007', '贝尔', '上海'), ('1008', '孙琪', '北京');

-- t2
DROP TABLE IF EXISTS `t2`;
CREATE TABLE `t2`  (`id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_t2_name`(`name`(191)) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `t2` VALUES (1001, '张三', '北京');
INSERT INTO `t2` VALUES (1004, '赵六', '河北');
INSERT INTO `t2` VALUES (1005, '杰克', '河南');
INSERT INTO `t2` VALUES (1007, '贝尔', '上海');
INSERT INTO `t2` VALUES (1008, '孙琪', '北京');
INSERT INTO `t2` VALUES (1009, '曹操', '魏国');
INSERT INTO `t2` VALUES (1010, '刘备', '蜀国');
INSERT INTO `t2` VALUES (1011, '孙权', '吴国');
INSERT INTO `t2` VALUES (1012, '诸葛亮', '蜀国');
INSERT INTO `t2` VALUES (1013, '典韦', '魏国');

那么,对于以后的问题,就很简略了,用 not in 或者 not exists 都能够把 t1 表中比 t2 表多出的那局部数据给挑出来。(当然,t2 比 t1 多进去的那局部不算)

这里假如用 name 来匹配数据。

select * from t1 where name not in (select name from t2);
或者用
select * from t1 where not exists (select name from t2 where t1.name=t2.name);

失去的后果都是一样的。

然而,须要留神的是,not in 和 not exists 还是有不同点的。

在应用 not in 的时候,须要保障子查问的匹配字段是非空的。如,此表 t2 中的 name 须要有非空限度。如若不然,就会导致 not in 返回的整个后果集为空。

例如,我在 t2 表中退出一条 name 为空的数据。

INSERT INTO `t2` VALUES (1014, NULL, '魏国');

则此时,not in 后果就会返回空。

另外须要明确的是,exists 返回的后果是一个 boolean 值 true 或者 false,而不是某个后果集。因为它不关怀返回的具体数据是什么,只是外层查问须要拿这个布尔值做判断。

区别是,用 exists 时,若子查问查到了数据,则返回真。用 not exists 时,若子查问没有查到数据,则返回真。

因为 exists 子查问不关怀具体返回的数据是什么。因而,以上的语句齐全能够批改为如下,

-- 子查问中 name 能够批改为其余任意的字段,如此处改为 1。select * from t1 where not exists (select 1 from t2 where t1.name=t2.name);

从执行效率来说,1 > column > *。因而举荐用 select 1。(精确的说应该是常量值)

in, exists 执行流程

1、对于 in 查问来说,会先执行子查问,如上边的 t2 表,而后把查问失去的后果和表面 t1 做笛卡尔积,再通过条件进行筛选(这里的条件就是指 name 是否相等),把每个符合条件的数据都退出到后果集中。

sql 如下,

select * from t1 where name in (select name from t2);

伪代码如下:

for(x in A){for(y in B){if(condition is true) {result.add();}
    }
}

这里的 condition 其实就是比照两张表中的 name 是否雷同。

2、对于 exists 来说,是先查问遍历表面 t1,而后每次遍历时,再查看在内表是否合乎匹配条件,即查看是否存在 name 相等的数据。

sql 如下,

select * from t1 where name exists (select 1 from t2);

伪代码如下:

for(x in A){if(exists condition is true){result.add();}
}

对应于此例,就是从 id 为 1001 开始遍历 t1 表,而后遍历时查看 t2 中是否有相等的 name。

如 id=1001 时,张三存在于 t2 表中,则返回 true,把 t1 中张三的这条记录退出到后果集,持续下次循环。id=1002 时,李四不在 t2 表中,则返回 false,不做任何操作,持续下次循环。直到遍历残缺个 t1 表。

是否走索引?

针对网上说的 in 和 exists 不走索引,那么到底是否如此呢?

咱们在 MySQL 5.7.18 中验证一下。(留神版本号哦)

单表查问

首先,验证单表的最简略的状况。咱们就以 t1 表为例,id 为主键,name 为一般索引。

别离执行以下语句,

explain select * from t1 where id in (1001,1002,1003,1004);
explain select * from t1 where id in (1001,1002,1003,1004,1005);
explain select * from t1 where name in ('张三','李四');
explain select * from t1 where name in ('张三','李四','王五');

为什么我要别离查不同的 id 个数呢?看截图,

会惊奇的发现,当 id 是四个值时,还走主键索引。而当 id 是五个值时,就不走索引了。这就很回味无穷了。

再看 name 的状况,

同样的当值多了之后,就不走索引了。

所以,我猜想这个跟匹配字段的长度无关。依照汉字是三个字节来计算,且程序设计中喜爱用 2 的 n 次幂的尿性,这里大略就是以 16 个字节为分界点。

然而,我又以同样的数据,去我的服务器上查问(版本号 5.7.22),发现四个 id 值时,就不走索引了。因而,估算这里的临界值为 12 个字节。

不管怎样,这阐明了,在 MySQL 中应该对 in 查问的字节长度是有限度的。(没有官网确切说法,所以,仅供参考)

多表波及子查问

咱们次要是去看以后的这个例子中的两表查问时,in 和 exists 是否走索引。

一、别离执行以下语句,主键索引(id)和一般索引(name),在 in , not in 下是否走索引。

explain select * from t1 where id in (select id from t2); --1
explain select * from t1 where name in (select name from t2); --2
explain select * from t1 where id not in (select id from t2); --3
explain select * from t1 where name not in (select name from t2); --4

后果截图如下,

1、t1 走索引,t2 走索引。

2、t1 不走索引,t2 不走索引。(此种状况,实测若把 name 改为惟一索引,则 t1 也会走索引)

3、t1 不走索引,t2 走索引。

4、t1 不走索引,t2 不走索引。

我滴天,这后果看起来乌七八糟的,如同走不走索引,齐全看情绪。

然而,咱们发现只有第一种状况,即用主键索引字段匹配,且用 in 的状况下,两张表才都走索引。

这个到底是不是法则呢?有待考查,且往下看。

二、接下来测试,主键索引和一般索引在 exists 和 not exists 下的状况。sql 如下,

explain select * from t1 where exists (select 1 from t2 where t1.id=t2.id);
explain select * from t1 where exists (select 1 from t2 where t1.name=t2.name);
explain select * from t1 where not exists (select 1 from t2 where t1.id=t2.id);
explain select * from t1 where not exists (select 1 from t2 where t1.name=t2.name);

这个后果就十分有法则了,且看,

有没有发现,t1 表哪种状况都不会走索引,而 t2 表是有索引的状况下就会走索引。为什么会呈现这种状况?

其实,上一大节说到了 exists 的执行流程,就曾经阐明问题了。

它是以外层表为驱动表,无论如何都会循环遍历的,所以会全表扫描。而内层表通过走索引,能够疾速判断以后记录是否匹配。

效率如何?

针对网上说的 exists 肯定比 in 的执行效率高,咱们做一个测试。

别离在 t1,t2 中插入 100W,200W 条数据。

我这里,用的是自定义函数来循环插入,语句参考如下,(没有把表名抽离成变量,因为我没有找到办法,难堪)

-- 传入须要插入数据的 id 开始值和数据量大小,函数返回后果为最终插入的条数,此值失常应该等于数据量大小。-- id 自增,循环往 t1 表增加数据。这里为了不便,id、name 取同一个变量,address 就为北京。delimiter // 
drop function if exists insert_datas1//
create function insert_datas1(in_start int(11),in_len int(11)) returns int(11)
begin  
  declare cur_len int(11) default 0;
  declare cur_id int(11);
  set cur_id = in_start;
    
  while cur_len < in_len do
     insert into t1 values(cur_id,cur_id,'北京');
  set cur_len = cur_len + 1;
  set cur_id = cur_id + 1;
  end while; 
  return cur_len;
end  
//
delimiter ;
-- 同样的,往 t2 表插入数据
delimiter // 
drop function if exists insert_datas2//
create function insert_datas2(in_start int(11),in_len int(11)) returns int(11)
begin  
  declare cur_len int(11) default 0;
  declare cur_id int(11);
  set cur_id = in_start;
    
  while cur_len < in_len do
     insert into t2 values(cur_id,cur_id,'北京');
  set cur_len = cur_len + 1;
  set cur_id = cur_id + 1;
  end while; 
  return cur_len;
end  
//
delimiter ;

在此之前,先清空表里的数据,而后执行函数,

select insert_datas1(1,1000000);

对 t2 做同样的解决,不过为了两张表数据有穿插,就从 70W 开始,而后插入 200W 数据。

select insert_datas2(700000,2000000);

在家里的电脑,理论执行工夫,别离为 36s 和 74s。

不知为何,家里的电脑还没有在 Docker 虚拟机中跑的脚本快。。害,就这样凑合着用吧。

等我有了 新欢 钱,就把它换掉,哼哼。

同样的,把上边的执行打算都执行一遍,进行比照。我这里就不贴图了。

in 和 exists 孰快孰慢

为了不便,次要拿以下这两个 sql 来比照剖析。

select * from t1 where id in (select id from t2);
select * from t1 where exists (select 1 from t2 where t1.id=t2.id);

执行结果显示,两个 sql 别离执行 1.3s 和 3.4s。

留神此时,t1 表数据量为 100W,t2 表数据量为 200W。

依照网上对 in 和 exists 区别的艰深说法,

如果查问的两个表大小相当,那么用 in 和 exists 差异不大;如果两个表中一个较小一个较大,则子查问表大的用 exists,子查问表小的用 in;

对应于此处就是:

  • 当 t1 为小表,t2 为大表时,应该用 exists,这样效率高。
  • 当 t1 为大表,t2 为小表时,应该用 in,这样效率较高。

而我用理论数据测试,就把第一种说法给颠覆了。因为很显著,t1 是小表,然而 in 比 exists 的执行速度还快。

为了持续测验它这个观点,我把两个表的内表表面关系调换一下,让 t2 大表作为表面,来比照查问,

select * from t2 where id in (select id from t1);
select * from t2 where exists (select 1 from t1 where t1.id=t2.id);

执行结果显示,两个 sql 别离执行 1.8s 和 10.0s。

是不是很有意思。能够发现,

  • 对于 in 来说,大表小表调换了内外层关系,执行工夫并无太大区别。一个是 1.3s,一个是 1.8s。
  • 对于 exists 来说,大小表调换了内外层关系,执行工夫天壤之别,一个是 3.4s,一个是 10.0s,足足慢了两倍。

一、以查问优化器维度比照。

为了探索这个后果的起因。我去查看它们别离在查问优化器中优化后的 sql。

select * from t1 where id in (select id from t2); 为例,程序执行以下两个语句。

-- 此为 5.7 写法,如果是 5.6 版本,须要用 explain extended ...
explain select * from t1 where id in (select id from t2);
-- 本意为显示正告信息。然而和 explain 一块儿应用,就会显示出优化后的 sql。须要留神应用程序。show warnings;

在后果 Message 里边就会显示咱们要的语句。

-- message 优化后的 sql
select `test`.`t1`.`id` AS `id`,`test`.`t1`.`name` AS `name`,`test`.`t1`.`address` AS `address` from `test`.`t2` join `test`.`t1` where (`test`.`t2`.`id` = `test`.`t1`.`id`)

能够发现,这里它把 in 转换为了 join 来执行。

这里没有用 on,而用了 where,是因为当只有 join 时,后边的 on 能够用 where 来代替。即 join on 等价于 join where。

PS: 这里咱们也能够发现,select 最终会被转化为具体的字段,晓得为什么咱们不倡议用 select 了吧。

同样的,以 t2 大表为表面的查问状况,也查看优化后的语句。

explain select * from t2 where id in (select id from t1);
show warnings;

咱们会发现,它也会转化为 join 的。

select `test`.`t2`.`id` AS `id`,`test`.`t2`.`name` AS `name`,`test`.`t2`.`address` AS `address` from `test`.`t1` join `test`.`t2` where (`test`.`t2`.`id` = `test`.`t1`.`id`)

这里不再贴 exists 的转化 sql,其实它没有什么大的变动。

二、以执行打算维度比照。

咱们再以执行打算维度来比照他们的区别。

explain select * from t1 where id in (select id from t2);
explain select * from t2 where id in (select id from t1);
explain select * from t1 where exists (select 1 from t2 where t1.id=t2.id);
explain select * from t2 where exists (select 1 from t1 where t1.id=t2.id);

执行后果别离为,

能够发现,对于 in 来说,大表 t2 做表面还是内表,都会走索引的,小表 t1 做内表时也会走索引。看它们的 rows 一列也能够看进去,前两张图后果一样。

对于 exists 来说,当小表 t1 做表面时,t1 全表扫描,rows 近 100W;当 大表 t2 做表面时,t2 全表扫描,rows 近 200W。这也是为什么 t2 做表面时,执行效率非常低的起因。

因为对于 exists 来说,表面总会执行全表扫描的,当然表数据越少越好了。

最终论断: 外层大表内层小表,用 in。外层小表内层大表,in 和 exists 效率差不多(甚至 in 比 exists 还快,而并不是网上说的 exists 比 in 效率高)。

not in 和 not exists 孰快孰慢

此外,实测比照 not in 和 not exists。

explain select * from t1 where id not in (select id from t2);
explain select * from t1 where not exists (select 1 from t2 where t1.id=t2.id);
explain select * from t1 where name not in (select name from t2);
explain select * from t1 where not exists (select 1 from t2 where t1.name=t2.name);

explain select * from t2 where id not in (select id from t1);
explain select * from t2 where not exists (select 1 from t1 where t1.id=t2.id);
explain select * from t2 where name not in (select name from t1);
explain select * from t2 where not exists (select 1 from t1 where t1.name=t2.name);

小表做表面的状况下。对于主键来说,not exists 比 not in 快。对于一般索引来说,not in 和 not exists 差不了多少,甚至 not in 会稍快。

大表做表面的状况下,对于主键来说,not in 比 not exists 快。对于一般索引来说,not in 和 not exists 差不了多少,甚至 not in 会稍快。

感兴趣的同学,可自行尝试。以上边的两个维度(查问优化器和执行打算)别离来比照一下。

join 的嵌套循环 (Nested-Loop Join)

为了了解为什么这里的 in 会转换为 join,我感觉有必要理解一下 join 的三种嵌套循环连贯。

1、简略嵌套循环连贯,Simple Nested-Loop Join,简称 SNLJ

join 即是 inner join,内连贯,它是一个笛卡尔积,即利用双层循环遍历两张表。

咱们晓得,个别在 sql 中都会以小表作为驱动表。所以,对于 A,B 两张表,若 A 的后果集较少,则把它放在外层循环,作为驱动表。天然,B 就在内层循环,作为被驱动表。

简略嵌套循环,就是最简略的一种状况,没有做任何优化。

因而,复杂度也是最高的,O(mn)。伪代码如下,

for(id1 in A){for(id2 in B){if(id1==id2){result.add();
        }
    }
}

2、索引嵌套循环连贯,Index Nested-Loop Join,简称 INLJ

看名字也能看进去了,这是通过索引进行匹配的。外层表间接和内层表的索引进行匹配,这样就不须要遍历整个内层表了。利用索引,缩小了外层表和内层表的匹配次数。

所以,此种状况要求内层表的列要有索引。

伪代码如下,

for(id1 in A){if(id1 matched B.id){result.add();
    }
}

3、块索引嵌套连贯,Block Nested-Loop Join,简称 BNLJ

块索引嵌套连贯,是通过缓存外层表的数据到 join buffer 中,而后 buffer 中的数据批量和内层表数据进行匹配,从而缩小内层循环的次数。

以外层循环 100 次为例,失常状况下须要在内层循环读取外层数据 100 次。如果以每 10 条数据存入缓存 buffer 中,并传递给内层循环,则内层循环只须要读取 10 次 (100/10) 就能够了。这样就升高了内层循环的读取次数。

MySQL 官网文档也有相干阐明,能够参考:https://dev.mysql.com/doc/refman/5.7/en/nested-loop-joins.html#block-nested-loop-join-algorithm

所以,这里转化为 join,能够用到索引嵌套循环连贯,从而进步了执行效率。

申明: 以上是以我的测试数据为准,测进去的后果。理论实在数据和测试后果很有可能会不太一样。如果有不同意见,欢送留言探讨。

正文完
 0