虽然,上面通过物化表的方式,将IN子查询转换成了联接查询,但还是会有建立临时表的成本,能不能不进行物化操作直接把子查询转换为连接呢?直接转换肯定不行。
-- 这里我们先构造了3条记录,其实也是构造不唯一的普通索引
+------+------+------+ | id | t2_a | t2_b | +------+------+------+ | 1100 | 1000 | 1000 | | 1101 | 1000 | 1000 | | 1102 | 1000 | 1000 | +------+------+------+ -- 加限制条件where t2.id>=1100是为了减少要显示的数据 mysql root@localhost:test> select * from t3 where t3_a in (select t2_a from t2 where t2.id>=1100); +-----+------+------+ | id | t3_a | t3_b | +-----+------+------+ | 101 | 1000 | 101 | +-----+------+------+ 1 row in set Time: 0.016s mysql root@localhost:test> select * from t3 left join t2 on t3.t3_a=t2.t2_a where t2.id>=1100; +-----+------+------+------+------+------+ | id | t3_a | t3_b | id | t2_a | t2_b | +-----+------+------+------+------+------+ | 101 | 1000 | 101 | 1100 | 1000 | 1000 | | 101 | 1000 | 101 | 1101 | 1000 | 1000 | | 101 | 1000 | 101 | 1102 | 1000 | 1000 | +-----+------+------+------+------+------+ 3 rows in set Time: 0.018s
所以说 IN 子查询和表联接之间并不完全等价。而我们需要的是另一种叫做半联接 (semi-join) 的联接方式 :对于 t3 表的某条记录来说,我们只关心在 t2 表中是否存在与之匹配的记录,而不关心具体有多少条记录与之匹配,最终的结果集中也只保留 t3 表的记录。
注意:semi-join 只是在 MySQL 内部采用的一种执行子查询的方式,MySQL 并没有提供面向用户的 semi-join 语法。
4.2.2 半联接的实现:
- Table pullout (子查询中的表上拉)
当子查询的查询列表处只有主键或者唯一索引列时,可以直接把子查询中的表上拉到外层查询的 FROM 子句中,并把子查询中的搜索条件合并到外层查询的搜索条件中,比如这个:
mysql root@localhost:test> select * from t3 where t3_a in (select t2_a from t2 where t2.id=999) +-----+------+------+ | id | t3_a | t3_b | +-----+------+------+ | 102 | 999 | 102 | +-----+------+------+ 1 row in set Time: 0.024s mysql root@localhost:test> select * from t3 join t2 on t3.t3_a=t2.t2_a where t2.id=999; +-----+------+------+-----+------+------+ | id | t3_a | t3_b | id | t2_a | t2_b | +-----+------+------+-----+------+------+ | 102 | 999 | 102 | 999 | 999 | 999 | +-----+------+------+-----+------+------+ 1 row in set Time: 0.028s mysql root@localhost:test> explain select * from t3 where t3_a in (select t2_a from t2 where t2.id=999) +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------+ | 1 | SIMPLE | t2 | const | PRIMARY,idx_a | PRIMARY | 4 | const | 1 | <null> | | 1 | SIMPLE | t3 | ref | idx_a | idx_a | 5 | const | 1 | <null> | +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------+
- FirstMatch execution strategy (首次匹配)
FirstMatch 是一种最原始的半连接执行方式,跟相关子查询的执行方式是一样的,就是说先取一条外层查询的中的记录,然后到子查询的表中寻找符合匹配条件的记录,如果能找到一条,则将该外层查询的记录放入最终的结果集并且停止查找更多匹配的记录,如果找不到则把该外层查询的记录丢弃掉。然后再开始取下一条外层查询中的记录,重复上边这个过程。
mysql root@localhost:test> explain select * from t3 where t3_a in (select t2_a from t2 where t2.t2_a=1000) +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+ | 1 | SIMPLE | t3 | ref | idx_a | idx_a | 5 | const | 1 | <null> | | 1 | SIMPLE | t2 | ref | idx_a | idx_a | 5 | const | 4 | Using index; FirstMatch(t3) | +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+
- DuplicateWeedout execution strategy (重复值消除)
转换为半连接查询后,t3 表中的某条记录可能在 t2 表中有多条匹配的记录,所以该条记录可能多次被添加到最后的结果集中,为了消除重复,我们可以建立一个临时表,并设置主键id,每当某条 t3 表中的记录要加入结果集时,就首先把这条记录的id值加入到这个临时表里,如果添加成功,说明之前这条 t2 表中的记录并没有加入最终的结果集,是一条需要的结果;如果添加失败,说明之前这条 s1 表中的记录已经加入过最终的结果集,直接把它丢弃。
- LooseScan execution strategy (松散扫描)
这种虽然是扫描索引,但只取值相同的记录的第一条去做匹配操作的方式称之为松散扫描。
4.2.3 半联接的适用条件
当然,并不是所有包含IN子查询的查询语句都可以转换为 semi-join,只有形如这样的查询才可以被转换为 semi-join:
SELECT ... FROM outer_tables? ? ? WHERE expr IN (SELECT ... FROM inner_tables ...) AND ... -- 或者这样的形式也可以: SELECT ... FROM outer_tables? ? ? WHERE (oe1, oe2, ...) IN (SELECT ie1, ie2, ... FROM inner_tables ...) AND ...
用文字总结一下,只有符合下边这些条件的子查询才可以被转换为 semi-join:
- 该子查询必须是和IN语句组成的布尔表达式,并且在外层查询的 WHERE 或者 ON 子句中出现
- 外层查询也可以有其他的搜索条件,只不过和 IN 子查询的搜索条件必须使用AND 连接起来
- 该子查询必须是一个单一的查询,不能是由若干查询由 UNION 连接起来的形式
- 该子查询不能包含 GROUP BY 或者 HAVING 语句或者聚集函数
4.2.4 转为 EXISTS 子查询
不管子查询是相关的还是不相关的,都可以把 IN 子查询尝试转为 EXISTS子查询。其实对于任意一个 IN 子查询来说,都可以被转为 EXISTS 子查询,通用的例子如下:
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where) -- 可以被转换为: EXISTS (SELECT inner_expr FROM ... WHERE subquery_where AND outer_expr=inner_expr)
当然这个过程中有一些特殊情况,比如在 outer_expr 或者 inner_expr 值为 NULL 的情况下就比较特殊。因为有 NULL 值作为操作数的表达式结果往往是 NULL,比方说:
mysql root@localhost:test> SELECT NULL IN (1, 2, 3); +-------------------+ | NULL IN (1, 2, 3) | +-------------------+ | <null> | +-------------------+ 1 row in set
而 EXISTS 子查询的结果肯定是 TRUE 或者 FASLE 。但是现实中我们大部分使用 IN 子查询的场景是把它放在 WHERE 或者 ON 子句中,而 WHERE 或者 ON 子句是不区分 NULL 和 FALSE 的,比方说:
mysql root@localhost:test> SELECT 1 FROM s1 WHERE NULL; +---+ | 1 | +---+ 0 rows in set Time: 0.016s mysql root@localhost:test> SELECT 1 FROM s1 WHERE FALSE; +---+ | 1 | +---+ 0 rows in set Time: 0.033s
所以只要我们的IN子查询是放在 WHERE 或者 ON 子句中的,那么 IN -> EXISTS 的转换就是没问题的。说了这么多,为啥要转换呢?这是因为不转换的话可能用不到索引,比方说下边这个查询:
mysql root@localhost:test> explain select * from t3 where t3_a in (select t2_a from t2 where t2.t2_a>=999) or t3_b > 1000; +----+-------------+-------+-------+---------------+--------+---------+--------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+--------+---------+--------+------+--------------------------+ | 1 | PRIMARY | t3 | ALL | <null> | <null> | <null> | <null> | 1000 | Using where | | 2 | SUBQUERY | t2 | range | idx_a | idx_a | 5 | <null> | 107 | Using where; Using index | +----+-------------+-------+-------+---------------+--------+---------+--------+------+--------------------------+
但是将它转为 EXISTS 子查询后却可以使用到索引:
mysql root@localhost:test> explain select * from t3 where exists (select 1 from t2 where t2.t2_a>=999 and t2.t2_a=t3.t3_a) or t3_b > 1000; +----+--------------------+-------+------+---------------+--------+---------+--------------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+-------+------+---------------+--------+---------+--------------+------+--------------------------+ | 1 | PRIMARY | t3 | ALL | <null> | <null> | <null> | <null> | 1000 | Using where | | 2 | DEPENDENT SUBQUERY | t2 | ref | idx_a | idx_a | 5 | test.t3.t3_a | 1 | Using where; Using index | +----+--------------------+-------+------+---------------+--------+---------+--------------+------+--------------------------+
需要注意的是,如果 IN 子查询不满足转换为 semi-join 的条件,又不能转换为物化表或者转换为物化表的成本太大,那么它就会被转换为 EXISTS 查询。或者转换为物化表的成本太大,那么它就会被转换为 EXISTS 查询。
05总结
1. 如果IN子查询符合转换为 semi-join 的条件,查询优化器会优先把该子查询转换为 semi-join,然后再考虑下边执行半连接的策略中哪个成本最低,
1)Table pullout
2)DuplicateWeedout
3)LooseScan
4)FirstMatch
选择成本最低的那种执行策略来执行子查询。
2. 如果IN子查询不符合转换为 semi-join 的条件,那么查询优化器会从下边两种策略中找出一种成本更低的方式执行子查询:
1)先将子查询物化之后再执行查询
2)执行 IN to EXISTS 转换
到此这篇关于MySQL子查询原理的文章就介绍到这了,更多相关MySQL子查询原理内容请搜索
更多相关Mysql内容来自木庄网络博客
标签:Mysql
相关阅读 >>
更多相关阅读请进入《mysql》频道 >>

数据库系统概念 第6版
本书主要讲述了数据模型、基于对象的数据库和XML、数据存储和查询、事务管理、体系结构等方面的内容。