Mysql查询优化器之关于子查询的优化

下面这些sql都含有子查询:

mysql> select * from t1 where a in (select a from t2);
mysql> select * from (select * from t1) as t;

按返回的结果集区分子查询

1、标量子查询

那些只返回一个单一值的子查询称之为标量子查询。比如:

select * from t1 where a in (select max(a) from t2);

2、行子查询

返回一条记录的子查询,不过这条记录需要包含多个列。比如:

select * from t1 where (a, b) = (select a, b from t2 limit 1);

3、列子查询

返回一个列的数据的子查询,包含多条记录。比如:

select * from t1 where a in (select a from t2);

4、表子查询

子查询的结果既包含很多条记录,又包含很多个列。比如:

select * from t1 where (a, b) in (select a,b from t2);

按与外层查询关系来区分子查询

1、相关子查询

如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之为相关子查询。比如:

select * from t1 where a in (select a from t2 where t1.a = t2.a);

2、不相关子查询

如果子查询可以单独运行出结果,而不依赖于外层查询的值,我们就可以把这个子查询称之为不相关子查询。前边介绍的那些子查询全部都可以看作不相关子查。

子查询在MySQL中是怎么执行的

1、对于不相关标量子查询或者行子查询

比如:select * from t1 where a = (select a from t2 limit 1);

它的执行步骤是:

1)执行select a from t2 limit 1这个子查询。

2)然后在将上一步子查询得到的结果当作外层查询的参数再执行外层查询select * from t1 where a = …;

2、对于相关标量子查询或者行子查询

比如:select * from t1 where b = (select b from t2 where t1.a = t2.a limit 1);

它的执行步骤是:

1)先从外层查询中获取一条记录,本例中也就是先从t1表中获取一条记录。

2)然后从上一步骤中获取的那条记录中找出子查询中涉及到的值,本例中就是从t1表中获取的那条记录中找出t1.a列的值,然后执行子查询。

3)最后根据子查询的查询结果来检测外层查询WHERE子句的条件是否成立,如果成立,就把外层查询的那条记录加入到结果集,否则就丢弃。

4)再次执行第一步,获取第二条外层查询中的记录,依次类推。。。

3、IN子查询优化

mysql对IN子查询进行了优化。

比如:select * from t1 where a in (select a from t2);

对于不相关的IN子查询来说,如果子查询的结果集中的记录条数很少,那么把子查询和外层查询分别看成两个单独的单表查询效率还是蛮高的,但是如果单独执行子查询后的结果集太多的话,就会导致这些问题:

• 结果集太多,可能内存中都放不下

• 对于外层查询来说,如果子查询的结果集太多,那就意味着IN子句中的参数特别多,这会导致:

• 无法有效的使用索引,只能对外层查询进行全表扫描。

• 在对外层查询执行全表扫描时,由于IN子句中的参数太多,这会导致检测一条记录是否符合和IN子句中的参数匹配花费的时间太长

在mysql中,不直接将不相关子查询的结果集当作外层查询的参数,而是将该结果集写入一个临时表里。写入临时表的过程是这样的:

1)该临时表的列就是子查询结果集中的列。

2)写入临时表的记录会被去重。IN语句是判断某个操作数在不在某个集合中,集合中的值重不重复对整个IN语句的结果并不影响,所以我们在将结果集写入临时表时对记录进行去重可以让临时表变得更小。临时表也是个表,只要为表中记录的所有列建立主键或者唯一索引就可以进行去重。

3)一般情况下子查询结果集不会特别大,所以会为它建立基于内存的使用Memory存储引擎的临时表,而且会为该表建立哈希索引。IN语句的本质就是判断某个操作数在不在某个集合里,如果集合中的数据建立了哈希索引,那么这个匹配的过程就是很快的。

4)如果子查询的结果集非常大,超过了系统变量tmp_table_size或者max_heap_table_size,临时表会转而使用基于磁盘的存储引擎来保存结果集中的记录,索引类型也对应转变为B+树索引。

这个将子查询结果集中的记录保存到临时表的过程称之为物化(Materialize)。那个存储子查询结果集的临时表称之为物化表。正因为物化表中的记录都建立了索引(基于内存的物化表有哈希索引,基于磁盘的有B+树
索引),通过索引执行IN语句判断某个操作数在不在子查询结果集中变得非常快,从而提升了子查询语句的性能。

还是对于上面的那个sql:

mysql> select * from t1 where a in (select a from t2);

当我们把子查询进行物化之后,假设子查询物化表的名称为materialized_table,该物化表存储的子查询结果集的列为m_val,那么这个查询其实可以从下边两种角度来看待:

• 从表t1的角度来看待,整个查询的意思其实是:对于t1表中的每条记录来说,如果该记录的a列的值在子查询对应的物化表中,则该记录会被加入最终的结果集。

• 从子查询物化表的角度来看待,整个查询的意思其实是:对于子查询物化表的每个值来说,如果能在t1表中找到对应的a列的值与该值相等的记录,那么就把这些记录加入到最终的结果集。

也就是说其实上边的查询就相当于表t1和子查询物化表materialized_table进行内连接:

select * from t1 inner join materialized_table on t1.a = m_val;

转化成内连接之后,查询优化器就可以评估不同连接顺序需要的成本是多少,选取成本最低的那种查询方式执行查询。

虽然将子查询进行物化之后再执行查询会有建立临时表的成本,但是可以将子查询转换为JOIN还是会更有效率一点的。那能不能不进行物化操作直接把子查询转换为连接呢。

我们对比下面两个sql:

select * from t1 where a in (select a from t2);
select t1.* from t1 inner join t2 on t1.a = t2.a;

这两个sql的查询结果其实很像,只是说对于第二个sql的结果集没有去重,所以IN子查询和两表连接之间并不完全等价,但是将子查询转换为连接又真的可以充分发挥优化器的作用,所以MySQL提出了一个新概念半连接(semi-join),将t1表和t2表进行半连接的意思就是:对于t1表的某条记录来说,我们只关心在t2表中是否存在与之匹配的记录是否存在,而不关心具体有多少条记录与之匹配,最终的结果集中只保留t1表的记录。semi-join只是在MySQL内部采用的一种执行子查询的方式,MySQL并没有提供面向用户的semi-join语法 。

那么怎么实现semi-join呢?

(1)Table pullout (子查询中的表上拉)

当子查询的查询列表处只有主键或者唯一索引列时,可以直接把子查询中的表上拉到外层查询的FROM子句中,并把子查询中的搜索条件合并到外层查询的搜索条件中。
比如:select * from t1 where a in (select a from t2 where t2.b = 1); – a是主键

我们可以直接把t2表上拉到外层查询的FROM子句中,并且把子查询中的搜索条件合并到外层查询的搜索条件中,上拉之后的查询就是这样的:

select * from t1 inner join t2 on t1.a = t2.a where t2.b = 1; -– a是主键

(2)DuplicateWeedout execution strategy (重复值消除)

对于这个查询来说:

select * from t1 where a in (select e from t2 where t2.b = 1); – e只是一个普通字段

转换为半连接查询后,t1表中的某条记录可能在t2表中有多条匹配的记录,所以该条记录可能多次被添加到最后的结果集中,为了消除重复,我们可以建立一个临时表,比方说这个临时表长这样:

CREATE TABLE tmp (
    id PRIMARY KEY
);

这样在执行连接查询的过程中,每当某条t1表中的记录要加入结果集时,就首先把这条记录的主键值加入到这个临时表里,如果添加成功,说明之前这条t1表中的记录并没有加入最终的结果集,现在把该记录添加到最终的结果集;如果添加失败,说明这条之前这条t1表中的记录已经加入过最终的结果集,这里直接把它丢弃就好了,这种使用临时表消除semi-join结果集中的重复值的方式称之为DuplicateWeedout。

(3)FirstMatch execution strategy (首次匹配)

FirstMatch是一种最原始的半连接执行方式,就是我们最开始的思路,先取一条外层查询的中的记录,然后到子查询的表中寻找符合匹配条件的记录,如果能找到一条,则将该外层查询的记录放入最终的结果集并且停止查找更多匹配的记录,如果找不到则把该外层查询的记录丢弃掉;然后再开始取下一条外层查询中的记录,重复上边这个过程。

(4)LooseScan(松散索引扫描)

子查询扫描了非唯一索引,因为是非唯一索引,所以可能有相同的值,可以利用索引去重。

对于某些使用IN语句的相关子查询,比方这个查询:

select * from t1 where a in (select b from t2 where t1.b = t2.b);

它可以转换为半连接:

select * from t1 semi join t2 on t1.a = t2.a and t1.b = t2.b;

如一下几种情况就不能转换为semi-join:

  • 外层查询的WHERE条件中有其他搜索条件与IN子查询组成的布尔表达式使用OR连接起来

  • 使用NOT IN而不是IN的情况

  • 子查询中包含GROUP BY、HAVING或者聚集函数的情况

  • 子查询中包含UNION的情况

那么对于不能转为semi-join查询的子查询,有其他方式来进行优化:

  • 对于不相关子查询来说,可以尝试把它们物化之后再参与查询
比如对于使用了NOT IN下面这个sql:

select * from t1 where a not in (select a from t2 where t2.a = 1);

请注意这里将子查询物化之后不能转为和外层查询的表的连接,因为用的是not in只能是先扫描t1表,然后对t1表的某条记录来说,判断该记录的a值在不在物化表中。

  • 不管子查询是相关的还是不相关的,都可以把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)

这样转换的好处是,转换前本来不能用到索引,但是转换后可能就能用到索引了,比如:

select * from t1 where a in (select a from t2 where t2.e = t1.e);

这个sql里面的子查询时用不到索引的,转换后变为:

select * from t1 where exists (select 1 from t2 where t2.e = t1.e and t1.a = t2.a)

转换之后t2表就能用到a字段的索引了。

所以,如果IN子查询不满足转换为semi-join的条件,又不能转换为物化表或者转换为物化表的成本太大,那么它就会被转换为EXISTS查询。

对于派生表的优化

select * from (select a, b from t1) as t;

上面这个sql,子查询是放在from后面的,这个子查询的结果相当于一个派生表,表的名称是t,有a,b两个字段。

对于派生表,有两种执行方式:

(一)把派生表物化

我们可以将派生表的结果集写到一个内部的临时表中,然后就把这个物化表当作普通表一样参与查询。当然,在对派生表进行物化时,使用了一种称为延迟物化的策略,也就是在查询中真正使用到派生表时才回去尝试物化派生表,而不是还没开始执行查询就把派生表物化掉。比如:

select * from (select * from t1 where a = 1) as derived1 inner join t2 on derived1.a = t2.a where t2.a =10;

如果采用物化派生表的方式来执行这个查询的话,那么执行时首先会到t1表中找出满足t1.a = 10的记录,如果找不到,说明参与连接的t1表记录就是空的,所以整个查询的结果集就是空的,所以也就没有必要去物化查询中的派生表了。

(二)将派生表和外层的表合并,也就是将查询重写为没有派生表的形式

比如下面这个sql:

select * from (select * from t1 where a = 1) as t;

和下面的sql是等价的:

select * from t1 where a = 1;

再看一些复杂一点的sql:

select * from (select * from t1 where a = 1) as t inner join t2 on t.a = t2.a where t2.b = 1;

我们可以将派生表与外层查询的表合并,然后将派生表中的搜索条件放到外层查询的搜索条件中,就像下面这样:

select * from t1 inner join t2 on t1.a = t2.a where t1.a = 1 and t2.b = 1;

这样通过将外层查询和派生表合并的方式成功的消除了派生表,也就意味着我们没必要再付出创建和访问临时表的成本了。可是并不是所有带有派生表的查询都能被成功的和外层查询合并,当派生表中有这些语句就不可以和外层查询合并:

聚集函数,比如MAX()、MIN()、SUM()啥的
DISTINCT
GROUP BY
HAVING
LIMIT
UNION 或者 UNION ALL
派生表对应的子查询的SELECT子句中含有另一个子查询

所以MySQL在执行带有派生表的时候,优先尝试把派生表和外层查询合并掉,如果不行的话,再把派生表物化掉执行查询。

猜你喜欢

转载自www.cnblogs.com/tongxuping/p/12330122.html