在数据库查询优化中,查询效率直接关系到应用程序性能。其中,IN和EXISTS是两种常见的子查询操作符,广泛应用于SQL查询语句,但它们在执行效率上有所不同。
本文深入探讨IN和EXISTS的工作原理,以及在何种情境下选择更为合适。通过对这两种操作符的详细分析,揭示它们在实际应用中的优缺点,一起了解如何在数据库查询中灵活运用IN和EXISTS,以优化查询语句的执行。
IN与EXISTS基本概念与用法
IN子查询
在MySQL中,当使用IN
子查询时,主查询(外表)中的每一行都会与子查询(内表)的结果集进行比较。先执行子查询生成一个临时表,然后主查询取出对应的字段值,系统会遍历子查询结果集,检查这个字段值是否存在于子查询结果集中。如果存在,则该行满足条件,会被加入到最终的查询结果中。例如:
SELECT * FROM t_order where customer_no in (SELECT customer_no FROM t_customer WHERE country = 'US');
在这个例子中,对于t_order
表中的每一行,MySQL会查看t_customer
表中是否存在与其customer_no
相匹配的记录。如果t_customer
表中有任何行的customer_no
与t_order
表中当前行的customer_no
相同,那么这一行就会被包含在最终查询结果中。
IN
子查询的效率通常在子查询结果集较小的情况下较高,因为它需要处理并可能缓存整个子查询结果。
EXISTS子查询
EXISTS
子查询则是用于判断关联性,它并不关心子查询返回的具体数据值,而只关注是否存在匹配的行。对于主查询表中的每一行,执行内部的EXISTS
子查询。当EXISTS
子查询找到一行或多行符合WHERE
条件的记录时,立即返回真(TRUE
)。这个TRUE
值会导致外层查询的那一行被纳入最终结果中,因为WHERE EXISTS
条件为真。一旦EXISTS
子查询找到匹配项,它就不需要继续查找剩余的记录了,即实现了所谓的“短路”或“早期终结”。例如:
SELECT * FROM t_order torder WHERE EXISTS(SELECT 1 FROM t_customer tcustomer WHERE tcustomer.customer_no = torder.customer_no AND tcustomer.country = 'US');
在这个例子中,只要t_customer
表中存在至少一条记录,其customer_no
与t_order
表中的当前行customer_no
相符,MySQL就认为EXISTS
条件为真,并将当前的t_order
表行作为结果返回。无论t_customer
表有多少其他相关记录,都不再影响此条目是否被选中。
EXISTS
在子查询表大但只需验证是否存在对应关系时更高效,它支持“短路”机制,一旦找到匹配项就结束子查询,不必遍历完整个子查询表。
结论
MySQL中的IN
语句是把外表和内表作HASH
连接,而EXISTS
语句是对外表作LOOP
循环,每次LOOP
循环再对内表进行查询,单纯的理解EXISTS
比IN
语句的效率要高的说法其实是不准确的,要区分情景:
- 如果查询的两不表大小相当,那么用
EXISTS
和IN
差别不大。 - 如果两个表中一个较小,一个是大表,则子查询表大的用
EXISTS
,子查询表小的用IN
。
验证
下面我们来通过实际案例去验证数据量和索引对IN
与EXISTS
子查询性能的影响。
我们创建两张表:
-- t_order
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`(
id bigint UNSIGNED AUTO_INCREMENT COMMENT '自增主键'
PRIMARY KEY,
`order_no` varchar(16) NOT NULL DEFAULT '' COMMENT '订单编号',
`customer_no` varchar(16) NOT NULL DEFAULT '' COMMENT '客户编号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='测试订单表';
-- t_customer
DROP TABLE IF EXISTS `t_customer`;
CREATE TABLE `t_customer`(
id bigint UNSIGNED AUTO_INCREMENT COMMENT '自增主键'
PRIMARY KEY,
`customer_no` varchar(16) NOT NULL DEFAULT '' COMMENT '客户编号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
)ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='测试订单客户表';
我们通过Python
脚本往t_order
中插入100万条数据,t_customer
中插入1万条数据。
案例sql
:
SELECT * FROM t_order where customer_no in (SELECT customer_no FROM t_customer WHERE country = 'US');
SELECT * FROM t_order torder WHERE EXISTS(SELECT 1 FROM t_customer tcustomer WHERE tcustomer.customer_no = torder.customer_no AND tcustomer.country = 'US');
案例执行建立在没有加索引情况下进行。
IN小表,EXISTS小表
我们在执行上面两条sql时会发现IN
查询的速度远远高于EXISTS
。
SELECT * FROM t_order where customer_no in (SELECT customer_no FROM t_customer WHERE country = 'US');
SELECT * FROM t_order torder WHERE EXISTS(SELECT 1 FROM t_customer tcustomer WHERE tcustomer.customer_no = torder.customer_no AND tcustomer.country = 'US');
我们先看两个sql的执行计划:
本案例中IN
查询的SQL执行了近5秒。
本案例中EXISTS
查询的SQL执行了超过5分钟。
从上述执行计划中,我们可以看到IN
查询和EXISTS
查询在没有索引的情况下都进行了全表扫描:
-
IN查询:
- 主查询对
t_order
表进行了全表扫描(ALL),由于没有索引,MySQL需要遍历1005915行数据。 - 子查询对
t_customer
表也进行了全表扫描(MATERIALIZED),查找国家为’US’的客户编号。该表大小较小,有1000行数据。
- 主查询对
-
EXISTS查询:
- 主查询同样对
t_order
表进行了全表扫描(ALL),同理,无索引导致效率较低。 - 子查询对
t_customer
表进行了全表扫描(DEPENDENT SUBQUERY),并且根据WHERE条件过滤出与主查询关联的数据。
- 主查询同样对
虽然两者都未使用索引,但根据执行计划中的rows值,IN
查询的子查询涉及的数据量要远小于主查询涉及的数据量。具体来说,在IN
查询中,子查询只需要处理1000行数据,并将结果用于筛选主查询中的1005915行数据。而在EXISTS
查询中,子查询虽然只返回1.00(几乎为1)个匹配记录,但它需要针对每一行主查询的结果进行检查,总共要处理1005915次。
因此,在这种情况下,IN
查询的效率高于EXISTS
查询的原因主要是子查询数据集大小的不同以及子查询对主查询的影响程度。尽管两个查询都没有利用到索引优化,但在实际执行时,IN
查询所需的计算量相对较小,故其性能优于EXISTS
查询。
IN大表,EXISTS大表
我们再次变更一下sql,让子查询是大表,观察一下他们的执行情况。即sql:
SELECT * FROM t_customer WHERE customer_no IN (SELECT customer_no FROM t_order);
SELECT * FROM t_customer tcustomer WHERE EXISTS(SELECT 1 FROM t_order torder WHERE tcustomer.customer_no = torder.customer_no);
我们再次查看sql的执行计划:
执行IN查询语句时花费2秒。
执行EXISTS
查询花费0.25秒。
由此可以看出EXISTS
查询的效率远高于IN
查询,我们结合执行计划进行分析:
- IN查询:
- 主查询(
t_customer
)由于没有索引,MySQL需要对整个表进行全表扫描,涉及行数为1000行。 - 子查询(
t_order
)同样进行了全表扫描,涉及行数为1005915行。虽然子查询的结果通过自动生成的临时键与主查询关联,并且对于每一个主查询中的customer_no
,子查询都能很快找到对应的记录(rows值为1),但由于子查询的数据量巨大,所以整体查询效率不高。
- 主查询(
- EXISTS查询:
- 主查询(
t_customer
)仍然进行了全表扫描,涉及行数为1000行。 - 子查询(
t_order
)也是全表扫描,但关键在于它是“DEPENDENT SUBQUERY”,这意味着它会依赖于外部查询(即主查询)的每一行结果来决定是否执行。尽管子查询需要处理1005915行数据,但由于其是根据主查询的每一条customer_no
逐个检查是否存在匹配项,因此当遇到第一条不满足条件的customer_no
时,就可以立即停止对子查询中剩余行的处理。这导致了在实际执行过程中,可能只需要检查一部分t_order
表的数据即可完成所有主查询记录的验证,从而提高了查询效率。
- 主查询(
在这个案例中,因为主查询表(t_customer
)较小,而子查询表(t_order
)较大,EXISTS
查询能够在较早阶段停止不必要的计算,使得整体查询效率优于IN
查询。
数据量以及索引对IN
与`EXISTS性能的影响
在MySQL中,IN
和EXISTS
子查询的性能很大程度上取决于内外表的数据量以及相关的索引设置。
数据量的影响
-
对于
IN
子查询:当内表(子查询结果集)较小且数据能够被有效索引时,IN
通常表现良好。如果内表很大,即使有索引,由于需要生成并存储完整的子查询结果集以供主查询进行比对,因此随着内表记录数的增长,性能会逐渐下降。 -
对于
EXISTS
子查询:当外层主查询表较大,而内表虽大但匹配条件的行数较少时,EXISTS
的优势更加明显。因为它仅需找到一个匹配项就可以立即结束内部循环,返回真值,无需遍历整个内表。当内表数据量巨大但能快速定位到满足条件的少数行时,EXISTS
相比IN
更高效。
索引的影响:
-
对于
IN
子查询:如果IN
子查询中的字段具有有效的索引,可以减少内表的全表扫描,转而通过索引查找,显著提高查询效率。尤其是覆盖索引(索引包含了查询所需的所有列),可以直接从索引中获取信息,避免回表操作。 -
对于
EXISTS
子查询:对于EXISTS
子查询,同样要求相关联的字段上有合适的索引。例如,在上面的例子中,若t_customer
的customer_no
字段有索引,那么在执行WHERE tcustomer.customer_no = torder.customer_no
时,可以通过索引快速定位匹配记录,从而加速子查询的执行过程。
在决定使用IN
还是EXISTS
时,首先应考虑的是内外表的数据规模以及关联字段上的索引情况。若内表较小或子查询结果集易于通过索引优化,IN
可能是更好的选择。若关注是否存在关联关系且内表虽大但能满足条件的行数有限,同时外层主查询表可能更大,则EXISTS
可能提供更高的查询性能。
当然最佳实践是结合实际业务需求、数据分布特点以及数据库统计信息,通过分析SQL执行计划来确定最合适的查询策略,并根据实际情况调整表结构和索引设计。
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等。