本文MySQL版本5.7
sql语句:
sql
SELECT
COUNT(*)
FROM
twthy01 t
WHERE
t.STATUS IN ( '115', '117' )
AND (
t.laboratory_code LIKE '01010204%'
OR t.laboratory_code LIKE '010102040108%'
OR t.laboratory_code LIKE '0101020302%'
)
AND t.testing_no IN (
SELECT
t2.request_no
FROM
twthy02 t2
WHERE
t2.testing_id IN (
8,
9,
10,
11,
12,
13,
14,
15,
16
)
)
AND t.testing_no IN ( SELECT t4.request_no FROM tbzff04 t4 );
这个sql执行0.56秒,但是单独执行SELECT t4.request_no FROM tbzff04 t4
需要101秒,为什么整个sql又这么快。我们看看explain的结果:

MySQL 5.6 的优化器会识别出 ... WHERE col IN (SELECT ...) 这种模式,并将其转换为一种特殊的 JOIN,称为"半连接"。半连接的目标是:只关心外层表的行是否在内部表找到匹配,而不关心匹配了多少次。
它有多种策略来实现,最有可能的是以下两种:
策略一:FirstMatch(首次匹配)
这是最高效的策略之一。它的执行逻辑如下:
- 开始扫描
twthy01表(假设它有 1.9 万行)。 - 取出第一行
t。 - 检查
t是否满足STATUS和laboratory_code条件。假设满足。 - 关键一步 :现在要检查
t.testing_no IN (SELECT t4.request_no FROM tbzff04 t4)。优化器会去tbzff04表中查找t.testing_no。 - 因为
tbzff04表的request_no上有索引(idx_request_no),这个查找极快(毫秒级)。 - 一旦找到第一个匹配项 ,优化器就立刻停止在
tbzff04中的查找,并确认t这一行符合条件。它根本不会扫描tbzff04的其他行。 - 返回步骤 2,处理
twthy01的下一行。
对比一下:
- 单独查询 :扫描
tbzff04的 500 万 行。 - 子查询 :为
twthy01的每一行,在tbzff04中进行一次快速的索引查找 ,并且找到就停 。总工作量大约是19000 * (极快的索引查找时间),这和 101 秒比起来,简直是天壤之别。
策略二:Materialization(物化)
如果优化器认为 FirstMatch 不适用,它可能会选择物化策略:
- 先执行子查询
SELECT t4.request_no FROM tbzff04 t4。 - 但它不会把结果返回给你,而是把结果(所有不重复的
request_no)存入一个临时的内存表 中,并为这个临时表建立一个哈希索引。 - 然后,在扫描
twthy01表时,直接用t.testing_no去这个哈希索引里查找,这也是极快的。
等等,这不还是需要 101 秒吗?
不一定。在物化过程中,优化器可能会利用一些并行处理或者更高效的读取方式。但更重要的是,即使这一步花了 101 秒,它也只执行一次 。而原始的 EXISTS 写法,可能会让 twthy02 的全表扫描执行 19000 次!相比之下,101 秒 的成本是可以接受的。但在你的案例中,FirstMatch 的可能性更大,因为它根本不需要物化整个表。
所以从这看出使用IN语句 并不是都是先把子查询查出来然后和主表比较
工作流程:
- 执行子查询 :数据库完整地执行
SELECT t4.request_no FROM qzxcl_lims_ys.tbzff04 t4。 - 生成临时表 :将查询结果(几百万个
request_no)存入一个临时的、内存中的表(如果太大,可能会写在磁盘上)。 - 执行主查询 :扫描
twthy01表,对于每一行,都去这个临时表里检查t.testing_no是否存在。
这确实是一种经典的执行方式。 如果子查询结果集很小,这种方式非常高效。
但是,如果子查询结果集非常大(比如您的 500 万行),这种方式就非常糟糕了!
- 创建临时表需要消耗大量 CPU 和 I/O。
- 临时表占用大量内存或磁盘空间。
- 整个过程耗时极长(就像您单独执行需要 101 秒一样)。
2. 优化器的"智慧":选择最优策略
现代数据库优化器非常"聪明",它知道物化策略在结果集很大时是灾难。所以,它会评估多种执行策略的成本,然后选择成本最低的那一个。
对于 IN 子查询,优化器通常会将其转换为一种特殊的连接,称为半连接 。半连接的目标是"检查是否存在",而不是"获取所有匹配",所以它比普通 JOIN 更高效。
在半连接的大框架下,优化器有多种"武器"可以使用:
武器一:FirstMatch(首次匹配)------ 您的案例就是这个!
这是最高效的策略之一,专门为处理大结果集子查询而生。
工作流程:
- 根本不执行子查询! 优化器直接跳过了"物化"这一步。
- 扫描主表 :开始扫描
twthy01表。 - 索引查找 :对于
twthy01的每一行,直接利用tbzff04表上的idx_request_no索引,去查找t.testing_no。 - 找到就停 :一旦在索引中找到第一个匹配项,就立刻停止对
tbzff04的查找,并确认twthy01的当前行符合条件。
对比一下:
- 物化:我要先抄写整本 500 万行的电话簿,然后再去核对。
FirstMatch:我根本不抄电话簿,而是每来一个名字,我就直接去电话簿里按索引查找,找到就划掉,继续下一个。
显然,FirstMatch 在您的场景下是压倒性的最优解。