SQL界一直以来都流传这样一种说法,不要用in,要用exists代替in,in的性能很低。甚至在程序中使用了in还会被同行嫌弃,认为在任何时候exists的性能都比in高。
小数据量这几个用起来肯定没有太多区别,而要造几百万的数据进行测试,毕竟稍显麻烦。既然所有的大神都这么说,那么就这么信吧。
现在主流的观点认为,外表比内表(子查询表)结果集大,用in效率比exists高;内表(子查询表)比外表结果集大,用exists效率比in高。但实际如何,还是看最后的测试结果吧。
in和洪水猛兽一般,以至于静态数据都不太敢用in,要拆成or的写法。比如 where id_ in (1,2,3)
改写成 where id_ = 1 or id_ = 2 or id_ = 3
。这就很奇怪了,in和or的写法虽然不同,但逻辑是一样的,怎么in就更慢呢。
后来有些贤者已经证明了,in 改成 or效果是一样的。只不过有些数据库的in有数量限制,比如oracle的in的数量最多为1000个。
in在可读性、方便性上有着极大的优势,不管换成exists还是join的方式,都比不上in。心里一直想用in的渴望终于驱使自己亲自测试,性能到底有多大差距。
这里使用MySQL-5.7.32
进行测试。在数据库中产生大量数据,必须用到存储过程,并且还需要一些批量处理数据的技巧。否则几百万的数据会让你处于无尽的等待中。
以下表结构来源于真实的开源java cms项目:UJCMS。
每篇文章都属于某个栏目。即可通过角色和栏目的关联,控制文章的权限;也可通过组织和栏目的关联,控制文章权限。角色、栏目和组织、栏目都是多对多关联关系。
CREATE TABLE `ujcms_article` (
`id_` int(11) NOT NULL,
`site_id_` int(11) NULL DEFAULT NULL,
`channel_id_` int(11) NULL DEFAULT NULL,
`org_id_` int(11) NULL DEFAULT NULL,
`name_` varchar(255) NULL DEFAULT NULL,
`status_` smallint(6) NULL DEFAULT 0,
PRIMARY KEY (`id_`) USING BTREE,
INDEX `idx_article_channel`(`channel_id_`) USING BTREE,
INDEX `idx_article_org`(`org_id_`) USING BTREE,
INDEX `idx_article_site`(`site_id_`) USING BTREE
) ENGINE = InnoDB;
CREATE DEFINER=`ujcms`@`%` PROCEDURE `insert_article`()
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE j INT DEFAULT 1;
-- 1000条批量插入性能最好,非常重要。
DECLARE batch INT DEFAULT 1000;
SET @sql_insert = "INSERT INTO ujcms_article (id_, site_id_, channel_id_, org_id_, name_, status_) VALUES";
SET @sql_values = "";
SET autocommit = 0;
WHILE i < 2000 DO
WHILE j <= batch DO
SET @id_val = i*batch+j;
SET @channel_id = FLOOR((@id_val-1)/200) + 1;
SET @org_id = FLOOR((@id_val-1)/2224) + 1;
SET @sql_values = CONCAT(@sql_values, "(", @id_val, ",", ((@id_val-1) % 100) + 1, ",", @channel_id, ",", @org_id, ",'文章", @id_val, "',", ((@id_val-1) % 5) + 1, ")");
IF j < batch THEN
SET @sql_values = CONCAT(@sql_values, ",");
END IF;
SET j = j + 1;
END WHILE;
SET @sql_whole = CONCAT(@sql_insert, @sql_values);
PREPARE stmt FROM @sql_whole;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 100个事务提交性能最好,不过效果非常细微,使用自动提交事务也基本没区别,100万数据差2-5秒,聊胜于无。
IF i > 0 AND (i+1) % 100 = 0 THEN
COMMIT;
END IF;
SET @sql_values = "";
SET j = 1;
SET i = i + 1;
END WHILE;
SET autocommit = 1;
END
CREATE TABLE `ujcms_channel` (
`id_` int(11) NOT NULL,
`name_` varchar(255) NULL DEFAULT NULL,
PRIMARY KEY (`id_`) USING BTREE
) ENGINE = InnoDB;
CREATE DEFINER=`ujcms`@`%` PROCEDURE `insert_channel`()
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE max INT DEFAULT 10000;
SET @sql_insert = "INSERT INTO ujcms_channel VALUES ";
SET @sql_values= "";
WHILE i <= max DO
SET @sql_values = CONCAT(@sql_values, "(", i, ",", "'栏目", i, "')");
IF i < max THEN
SET @sql_values = CONCAT(@sql_values, ",");
END IF;
SET i = i + 1;
END WHILE;
SET @sql_whole = CONCAT(@sql_insert, @sql_values);
PREPARE stmt FROM @sql_whole;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql_values = "";
END
CREATE TABLE `ujcms_role` (
`id_` int(11) NOT NULL,
`name_` varchar(255) NULL DEFAULT NULL,
PRIMARY KEY (`id_`) USING BTREE
) ENGINE = InnoDB;
CREATE DEFINER=`ujcms`@`%` PROCEDURE `insert_role`()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 9 DO
INSERT INTO ujcms_role VALUES(i, CONCAT('角色', i));
SET i = i + 1;
END WHILE;
END
CREATE TABLE `ujcms_role_channel` (
`role_id_` int(11) NOT NULL,
`channel_id_` int(11) NOT NULL,
PRIMARY KEY (`role_id_`, `channel_id_`) USING BTREE,
INDEX `idx_rolechannel_channel`(`channel_id_`) USING BTREE
) ENGINE = InnoDB;
CREATE DEFINER=`ujcms`@`%` PROCEDURE `insert_role_channel`()
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE j INT DEFAULT 1;
DECLARE role_max INT DEFAULT 9;
SET @sql_insert = "INSERT INTO ujcms_role_channel VALUES ";
SET @sql_values = "";
WHILE i <= role_max DO
SET j = (i-1)*1000 + 1;
WHILE j <= i*1000 + 1000 DO
SET @sql_values = CONCAT(@sql_values, "(", i, ",", j, ")");
IF j < i*1000 + 1000 THEN
SET @sql_values = CONCAT(@sql_values, ",");
END IF;
SET j = j + 1;
END WHILE;
SET @sql_whole = CONCAT(@sql_insert, @sql_values);
PREPARE stmt FROM @sql_whole;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql_values = "";
SET i = i + 1;
END WHILE;
END
CREATE TABLE `ujcms_org` (
`id_` int(11) NOT NULL,
`name_` varchar(255) NULL DEFAULT NULL,
PRIMARY KEY (`id_`) USING BTREE
) ENGINE = InnoDB;
CREATE DEFINER=`ujcms`@`%` PROCEDURE `insert_org`()
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE max INT DEFAULT 900;
SET @sql_insert = "INSERT INTO ujcms_org VALUES ";
SET @sql_values= "";
WHILE i <= max DO
SET @sql_values = CONCAT(@sql_values, "(", i, ",", "'组织", i, "')");
IF i < max THEN
SET @sql_values = CONCAT(@sql_values, ",");
END IF;
SET i = i + 1;
END WHILE;
SET @sql_whole = CONCAT(@sql_insert, @sql_values);
PREPARE stmt FROM @sql_whole;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql_values = "";
END
CREATE TABLE `ujcms_org_channel` (
`org_id_` int(11) NOT NULL,
`channel_id_` int(11) NOT NULL,
PRIMARY KEY (`org_id_`, `channel_id_`) USING BTREE,
INDEX `idx_orgchannel_channel`(`channel_id_`) USING BTREE
) ENGINE = InnoDB;
CREATE DEFINER=`ujcms`@`%` PROCEDURE `insert_org_channel`()
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE j INT DEFAULT 1;
DECLARE left_max INT DEFAULT 900;
SET @sql_insert = "INSERT INTO ujcms_org_channel VALUES ";
SET @sql_values = "";
WHILE i <= left_max DO
SET j = (i-1)*10 + 1;
WHILE j <= i*10 + 1000 DO
SET @sql_values = CONCAT(@sql_values, "(", i, ",", j, ")");
IF j < i*10 + 1000 THEN
SET @sql_values = CONCAT(@sql_values, ",");
END IF;
SET j = j + 1;
END WHILE;
SET @sql_whole = CONCAT(@sql_insert, @sql_values);
PREPARE stmt FROM @sql_whole;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql_values = "";
SET i = i + 1;
END WHILE;
END
join可谓是最平常的解决方案了,虽然这里存在多对多
关系,join后会导致数据重复,但用distinct
去重就好了。
以下SQL实现,按用户所拥有的角色,查询有权限的文章列表。
SELECT
count(distinct t.id_)
FROM
ujcms_article t
JOIN ujcms_role_channel rc ON t.channel_id_ = rc.channel_id_
WHERE
rc.role_id_ in (4,6,5,1,9)
执行时间0.721
秒。好像还不错,可以接受。
如果功能再复杂一些,支持通过角色和组织两个地方给权限,SQL如下:
SELECT
count(distinct t.id_)
FROM
ujcms_article t
JOIN ujcms_role_channel rc ON t.channel_id_ = rc.channel_id_
JOIN ujcms_org_channel oc ON t.channel_id_ = oc.channel_id_
WHERE
rc.role_id_ in (4,6,5,1,9) or oc.org_id_ = 233
执行时间57.626
秒。是的,你没有看错,将近一分钟。这就是我们不敢把功能做的太复杂的原因。
既然join性能如此不堪,试试传说中的exists神器,会不会有奇效呢?
SELECT
count(t.id_)
FROM
ujcms_article t
WHERE
EXISTS (
SELECT *
FROM ujcms_role_channel rc
WHERE t.channel_id_ = rc.channel_id_
AND rc.role_id_ in ( 4, 6, 5, 1, 9 )
)
执行时间5.934
秒。一个关联表就没法用了,这是神器还是神坑?
SELECT
count(t.id_)
FROM
ujcms_article t
WHERE
EXISTS (
SELECT *
FROM ujcms_role_channel rc
WHERE t.channel_id_ = rc.channel_id_
AND rc.role_id_ in (4,6,5,1,9)
)
or
EXISTS (
SELECT *
FROM ujcms_org_channel oc
WHERE t.channel_id_ = oc.channel_id_ and oc.org_id_ = 233
)
执行时间7.296
秒。果然效果比join的57.626
秒好太多了,只可惜7秒的时间还是太慢了。
exists神器都如此不堪,那就来好好看看臭名昭著的in是怎么出洋相的吧。
SELECT
count(t.id_)
FROM
ujcms_article t
WHERE
t.channel_id_ IN (
SELECT rc.channel_id_
FROM ujcms_role_channel rc
WHERE rc.role_id_ in (4,6,5,1,9)
)
执行时间0.275
秒。这可比join的0.721
秒都快的多呀。
但也就比join快了几百毫秒,无关痒痛,还有两个关联表的呢,看in是怎么死的。
SELECT
count(t.id_)
FROM
ujcms_article t
WHERE
t.channel_id_ IN (
SELECT rc.channel_id_
FROM ujcms_role_channel rc
WHERE rc.role_id_ in (4,6,5,1,9)
)
or
t.channel_id_ IN (
SELECT oc.channel_id_
FROM ujcms_org_channel oc
WHERE oc.org_id_ = 233
)
执行时间0.422
秒。What?是数据大意了没有闪吗?你让join的57.626
秒和exists的7.296
秒情何以堪?
前面说过,exists在外表数据集小,内表(子查询)数据集大的情况下,性能比in好。那么我们就来复现一下这个场景吧。
ujcms_channel
表只有1万条数据,作为外表;ujcms_article
表有200万数据,作为内表。
in查询
select count(t.id_) from ujcms_channel t where t.id_ in (
select a.channel_id_ from ujcms_article a where a.site_id_ in (1,3,4,5)
) and t.name_ like '栏目1%'
执行时间1.325
秒。
exists查询
select count(t.id_) from ujcms_channel t where exists (
select * from ujcms_article a where t.id_ = a.channel_id_ and a.site_id_ in (1,3,4,5)
) and t.name_ like '栏目1%'
执行时间0.540
秒。
join查询
select count(distinct t.id_)
from ujcms_channel t
join ujcms_article a on t.id_ = a.channel_id_
where a.site_id_ in (1,3,4,5) and t.name_ like '栏目1%'
执行时间0.181
秒。
可以看到in确实比exists更耗时,但join性能更好。
在多对多
关联时,由于会导致数据大量膨胀,用join
要慎重,特别是多个多对多
关联,要慎重。
exists
目前没有看到太多高光的表现。原因是外表中的每一条数据都要执行一次嵌套的子查询,当外表数据量大的时候,exists
的性能不可能好到哪里去。
in
的子查询只需要执行一次,主要是怕这个子查询的结果集非常大,占用内存。但一般in
里的子查询结果集大到离谱的情形非常少,所以in
绝不是性能低下洪水猛兽,多对多
关联时,大胆的,甚至尽可能的用in
。
只有当外表数据量很小,而内表的数据量有十分巨大时,exists
才可能有一些优势。不过通过实际测试也没发现有多大的优势。由于外表数据量非常小,而内表数据量却十分巨大的情形非常少,没有做特别严谨的测试。因此exists
的使用场景非常少,实在要用,要先做好测试。