select country.* from country join city on country.code=city.country_code \and population>20000000;+---------+----------+| code | name |+---------+----------+| 1 | china || 1 | china |+---------+----------+2 rows in set (0.00 sec)
而子查询则不会:
select * from country where code in \(select country_code from city where population>20000000);+------+---------+| code | name |+------+---------+| 1 | china |+------+---------+1 row in set (0.00 sec)
在子查询中,优化器可以识别出 in 子句中每组只需要返回一个值,在这种情况下,可以使用半联接 Semi-join 来优化子查询,提升查询效率。
Semi-join 限制
不过并不是所有子查询都是半联接,必须满足以下条件:
子查询必须是出现在顶层的 WHERE、ON 子句后面的 IN 或者 =ANY
子查询必须是单个 select,不能是 union;
子查询不能有 group by 或者 having 子句(可以用 semijoin materialization 策略,其他不可以 );
It must not be implicitly grouped (it must contain no aggregate functions). (不知道啥意思,保持原文);
子查询不能有 order by with limit;
父查询中不能有 STRAIGHT_JOIN 指定联接顺序;
The number of outer and inner tables together must be less than the maximum number of tables permitted in a join.
从临时表中取出一行数据,到 Country 表中去查找满足联接条件的行,走 Country 表的主键,每次扫描 1 行;
重复 3,直到遍历临时表结束。
所以这里扫描的行数为 15+15+15*1=45。
Materialization-lookup
修改一下 SQL,让子查询的结果集变大,改变联接顺序:
select * from Country where Country.code IN (select City.Country \from City where City.Population > 1*1000*1000) ;+----+--------------+-------------+--------+--------------------+--------------+---------+------+------+-----------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+--------------+-------------+--------+--------------------+--------------+---------+------+------+-----------------------+| 1 | PRIMARY | Country | ALL | PRIMARY | NULL | NULL | NULL | 239 | || 1 | PRIMARY | <subquery2> | eq_ref | auto_key | auto_key | 3 | func | 1 | || 2 | MATERIALIZED | City | range | Population,Country | Population | 4 | NULL | 238 | Using index condition |+----+--------------+-------------+--------+--------------------+--------------+---------+------+------+-----------------------+
这样就变成了 Materialization-lookup,执行流程为:
先执行子查询,走的 Population 索引,扫描了 238 行,得到 238 行结果;
将上一步得到的结果保存到临时表中;
从 Country 表中取出一行数据,到物化临时表中去查找满足联接条件的行,走物化表的主键,每次扫描 1 行;
重复 3,直到遍历 Country 表结束(一共 239 行)。
所以这里扫描的行数为 238+239*1=477。
注意事项
参考资料文章提到在 MariaDB 中,子查询有 group by 分组操作时能用到 Semi-join Materialization 优化策略(其他的 Duplicate Weedout、FirstMatch、LooseScan 不能用)。而在 MySQL 中,子查询有 group by 分组操作时所有的 Semi-join 策略都无法使用,即无法使用 Semi-join 优化,举例:
select dept_name from departments where dept_no in \(select min(dept_no) from dept_emp where emp_no<10020 group by dept_no);+----+-------------+-------------+-------+-----------------+-----------+---------+------+-----------------------------------------------------------+| id | select_type | table | type | possible_keys | key | key_len | rows | Extra |+----+-------------+-------------+-------+-----------------+-----------+---------+------------------------------------------------------------------+| 1 | PRIMARY | departments | index | NULL | dept_name | 42 | 9 | Using where; Using index || 2 | SUBQUERY | dept_emp | range | PRIMARY,dept_no | PRIMARY | 4 | 21 | Using where; Using index; Using temporary; Using filesort |+----+-------------+-------------+-------+-----------------+-----------+---------+------+-----------------------------------------------------------+