1. 背景

在之前介绍MySQL执行计划的博文中已经谈及了一些关于子查询相关的执行计划与优化。本文将重点介绍MySQL中与子查询相关的内容,设计子查询优化策略,包含半连接子查询的优化与非半连接子查询的优化。其中非半连接子查询优化由于策略较少不作详细展开。

2. 子查询概念

子查询简单理解就是在sql中嵌套了select查询子句。子查询的优点在于它的可读性会比较高(相比写join和union)。子查询根据查询结果的形式可以大致分为标量子查询、列子查询、行子查询、表子查询等。根据与外部语句的关联性,也可以分为相关子查询和独立子查询。

有一个谬误是所有子查询能做的事情,用连接也能做。

举例,如下在where中嵌套一个其他表最大值子查询来等值比较的子查询就没法用join做到(不要钻牛角尖硬套join)

select x from a where x = (select max(y) from b);

3. 子查询的效率

很多人都会关心子查询效率,是否子查询真的就很慢?这个问题很难回答。但在MySQL低版本(5.5以下)对子查询的支持确实不是太好。在《高性能MySQL》一书中的6.5.1(关联子查询)章节中也提及了MySQL对于where子句中带有in子查询的处理,就是用exists转写为关联子查询,这样的话查询复杂度非常高,性能很差。所以在这种情况下,建议把子查询改写为关联查询并保证从表的连接字段上有索引。

4. 子查询的优化

在MySQL 5.6及更高版本,有了更多的子查询优化策略,具体如下:

对于in或者=any子查询:

Semi-Join(半连接)

Materialization(物化)

EXISTS策略

对于not in或者<>any子查询:

Materialization(物化)

EXISTS策略

对于派生表(from子句中的子查询):

把派生表结果与外部查询块合并

把派生表物化为临时表

4.1 半连接优化

只有满足如下条件的子查询,MySQL可以应用半连接优化。

in或者=any出现在where或者on的顶层

子查询为不含union的单一select

子查询不含有group和having

子查询不能带有聚合函数

子查询不能带有order by... limit

子查询不能带有straight_join查询提示

外层表和内层表的总数不能超过join允许最大的表数量

如果满足如上条件,MySQL会基于成本代价从如下几种优化策略选择一个来进行半连接子查询优化

4.1.1 Table pullout优化

Table pullout优化将半连接子查询的表提取到外部查询,将查询改写为join。这与人们在使用低版本MySQL时的常见的手工调优策略很相似。

下面来举例看看:

首先建立两张表,一张为stu(学生表),一张为stu_cls(学生班级关联表)

create table stu (id int primary key auto_increment);

create table stu_cls(s_id int, c_id int, unique key(s_id));

往stu表里初始化一些数据

insert into stu select null;

insert into stu select null from stu;

insert into stu select null from stu;

insert into stu select null from stu;

insert into stu select null from stu;

insert into stu select null from stu;

insert into stu select null from stu;

insert into stu select null from stu;

insert into stu select null from stu;

insert into stu select null from stu;

往stu_cls表中初始化一些数据

insert into stu_cls select 1,1;

insert into stu_cls select 2,1;

insert into stu_cls select 3,2;

insert into stu_cls select 4,3;

insert into stu_cls select 5,2;

insert into stu_cls select 6,2;

insert into stu_cls select 7,4;

insert into stu_cls select 8,2;

insert into stu_cls select 9,1;

insert into stu_cls select 10,3;

在MySQL5.5的环境下,当查询下面的执行计划时,显示的结果很可能是这样的

mysql> explain extended select * from stu where id in (select s_id from stu_cls)\G

*************************** 1. row ***************************

id: 1

select_type: PRIMARY

table: stu

type: index

possible_keys: NULL

key: PRIMARY

key_len: 4

ref: NULL

rows: 512

filtered: 100.00

Extra: Using where; Using index

*************************** 2. row ***************************

id: 2

select_type: DEPENDENT SUBQUERY

table: stu_cls

type: index_subquery

possible_keys: s_id

key: s_id

key_len: 5

ref: func

rows: 1

filtered: 100.00

Extra: Using index; Using where

2 rows in set, 1 warning (0.01 sec)

可以看到stu进行了一次索引全扫描(因为我们的试验表结构比较简单,stu表里就一个主键id),子查询的类型为相关子查询。近一步查看MySQL改写过的SQL。

mysql> show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: select `test`.`stu`.`id` AS `id` from `test`.`stu` where (`test`.`stu`.`id`,(((`test`.`stu`.`id`) in stu_cls on s_id where ((`test`.`stu`.`id`) = `test`.`stu_cls`.`s_id`))))

1 row in set (0.00 sec)

可以看到在MySQL5.5中采用了EXISTS策略将in子句转写为了exists子句。

在MySQL5.7的环境下,上面的执行计划如下所示

mysql> explain select * from stu where id in (select s_id from stu_cls)\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: stu_cls

partitions: NULL

type: index

possible_keys: s_id

key: s_id

key_len: 5

ref: NULL

rows: 10

filtered: 100.00

Extra: Using where; Using index

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: stu

partitions: NULL

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: test.stu_cls.s_id

rows: 1

filtered: 100.00

Extra: Using index

2 rows in set, 1 warning (0.00 sec)

可以看到执行计划中的两行id是一样的,从rows列的信息可以看到,预估扫描记录数仅仅为之前的2%;查看改写过的SQL如下

mysql> show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: /* select#1 */ select `test`.`stu`.`id` AS `id` from `test`.`stu_cls` join `test`.`stu` where (`test`.`stu`.`id` = `test`.`stu_cls`.`s_id`)

1 row in set (0.00 sec)

可以看到原来的子查询已经被改写为join了。

值得一提的是,Table pullout优化没有单独的优化器开关。如果想要禁用Table pullout优化,则需要禁用semijoin优化。如下所示:

mysql> set optimizer_switch='semijoin=off';

Query OK, 0 rows affected (0.00 sec)

mysql> explain select * from stu where id in (select s_id from stu_cls)\G

*************************** 1. row ***************************

id: 1

select_type: PRIMARY

table: stu

partitions: NULL

type: index

possible_keys: NULL

key: PRIMARY

key_len: 4

ref: NULL

rows: 512

filtered: 100.00

Extra: Using where; Using index

*************************** 2. row ***************************

id: 2

select_type: SUBQUERY

table: stu_cls

partitions: NULL

type: index

possible_keys: s_id

key: s_id

key_len: 5

ref: NULL

rows: 10

filtered: 100.00

Extra: Using index

2 rows in set, 1 warning (0.00 sec)

mysql> show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: /* select#1 */ select `test`.`stu`.`id` AS `id` from `test`.`stu` where (`test`.`stu`.`id`,`test`.`stu`.`id` in ( (/* select#2 */ select `test`.`stu_cls`.`s_id` from `test`.`stu_cls` where 1 ), (`test`.`stu`.`id` in on where ((`test`.`stu`.`id` = `materialized-subquery`.`s_id`)))))

1 row in set (0.00 sec)

可以看到当禁用semijoin优化后,MySQL5.7会采用非半连接子查询的物化策略来优化来处理此查询。

4.1.2 FirstMatch优化

下面来讲讲FirstMatch优化,这也是在处理半连接子查询时可能会用到的一种优化策略。

下面展示了一个构造FirstMatch的小demo例子:

create table department (id int primary key auto_increment);

create table employee (id int primary key auto_increment, dep_id int, key(dep_id));

mysql> explain select * from department where id in (select dep_id from employee)\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: department

partitions: NULL

type: index

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: NULL

rows: 1

filtered: 100.00

Extra: Using index

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: employee

partitions: NULL

type: ref

possible_keys: dep_id

key: dep_id

key_len: 5

ref: test.department.id

rows: 1

filtered: 100.00

Extra: Using index; FirstMatch(department)

2 rows in set, 1 warning (0.00 sec)

mysql> show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: /* select#1 */ select `test`.`department`.`id` AS `id` from `test`.`department` semi join (`test`.`employee`) where (`test`.`employee`.`dep_id` = `test`.`department`.`id`)

1 row in set (0.00 sec)

我们可以看到上面查询计划中,两个id都为1,且extra中列可以看到FirstMatch(department)。MySQL使用了连接来处理此查询,对于department表的行,只要能在employee表中找到1条满足即可以不必再检索employee表。从语义角度来看,和IN-to-EXIST策略转换为Exist子句是相似的,区别就是FirstMath以连接形式执行查询,而不是子查询。

4.1.3 Semi-join Materialization优化

Semi-join Materialization优化策略是将半连接的子查询物化为临时表。

我们可以将上面FirstMatch优化策略中的例子进行改造以构造Materialization优化的例子。

mysql> alter table employee drop index dep_id;

Query OK, 0 rows affected (0.10 sec)

Records: 0 Duplicates: 0 Warnings: 0

mysql> explain select * from department where id in (select dep_id from employee)\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table:

partitions: NULL

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: NULL

filtered: 100.00

Extra: Using where

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: department

partitions: NULL

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: .dep_id

rows: 1

filtered: 100.00

Extra: Using index

*************************** 3. row ***************************

id: 2

select_type: MATERIALIZED

table: employee

partitions: NULL

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 1

filtered: 100.00

Extra: NULL

3 rows in set, 1 warning (0.00 sec)

mysql> show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: /* select#1 */ select `test`.`department`.`id` AS `id` from `test`.`department` semi join (`test`.`employee`) where (`test`.`department`.`id` = ``.`dep_id`)

1 row in set (0.00 sec)

上述操作,先是drop掉了employee表上的dep_id索引,然后查看相同的执行计划。可以看到查询计划返回了三段结果,其中前两个id为1,最后一个id为2,且select_type为MATERIALIZED。这说明MySQL对于此查询会创建临时表,会将employee表物化为临时表,然后再以临时表为驱动表与department进行连接。

使用该优化策略需要打开优化器的semijoin和materialization开关。

4.1.4 Duplicate Weedout优化

这种优化策略就是将半连接子查询转换为INNER JOIN,再删除重复重复记录。

weed out有清除杂草的含义,所谓Duplicate Weedout优化策略就是先连接,得到有重复的结果集,再从中删除重复记录,好比是清理杂草。

由于构造Duplicate Weedout例子比较困难,我们不妨这里显式关闭半连接其他几个优化策略:

set optimizer_switch = 'materialization=off';

set optimizer_switch = 'firstmatch=off';

set optimizer_switch = 'loosescan=off';

以上面Semi-join Materialization优化中的表继续查询相同的执行计划:

mysql> explain select * from department where id in (select dep_id from employee)\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: employee

partitions: NULL

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 1

filtered: 100.00

Extra: Using where; Start temporary

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: department

partitions: NULL

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: test.employee.dep_id

rows: 1

filtered: 100.00

Extra: Using index; End temporary

2 rows in set, 1 warning (0.00 sec)

mysql> show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: /* select#1 */ select `test`.`department`.`id` AS `id` from `test`.`department` semi join (`test`.`employee`) where (`test`.`department`.`id` = `test`.`employee`.`dep_id`)

1 row in set (0.00 sec)

可以看到两段结果中Extra中分别有Start temporary和End temporary,这正是Duplicate Weedout过程中创建临时表,保存中间结果到临时表,从临时表中删除重复记录的过程。

4.1.5 LooseScan优化

LooseScan(松散索引扫描)和group by优化中的LooseScan差不多。

它采用松散索引扫描读取子查询的表,然后作为驱动表,外部表作为被驱动表进行连接。

它可以优化如下形式的子查询

select ... from ... where expr in (select key_part1 from ... where ...);

select ... from ... where expr in (select key_part2 from ... where key_part1=常量);

我们新建一张employee_department关联表

create table employee_department(id int primary key auto_increment,d_id int,e_id int,unique key(e_id,d_id));

往employee和新建的employee_department关联表中填充一些数据(填充语句省略)

查询如下执行计划:

mysql> explain select * from employee where id in (select e_id from employee_department)\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: employee_department

partitions: NULL

type: index

possible_keys: e_id

key: e_id

key_len: 10

ref: NULL

rows: 4

filtered: 100.00

Extra: Using where; Using index; LooseScan

*************************** 2. row ***************************

id: 1

select_type: SIMPLE

table: employee

partitions: NULL

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: test.employee_department.e_id

rows: 1

filtered: 100.00

Extra: NULL

2 rows in set, 1 warning (0.00 sec)

mysql> show warnings\G

*************************** 1. row ***************************

Level: Note

Code: 1003

Message: /* select#1 */ select `test`.`employee`.`id` AS `id`,`test`.`employee`.`dep_id` AS `dep_id` from `test`.`employee` semi join (`test`.`employee_department`) where (`test`.`employee`.`id` = `test`.`employee_departm

可以看到返回的结果中第一段的extra中带有LooseScan,且两段结果的id也都为1,也即采用了半连接查询优化中的松散索引扫描。

此查询MySQL会以employee_department来作为驱动表,读取employee_department的唯一索引,连接employee得到最终结果集。

LooseScan对应的优化器开关为loosecan

4.2 非半连接优化

以上部分介绍了基于半连接的子查询优化,对于如下一些非半连接子查询,则无法使用上面的几种优化方式了:

in与or混在一起

select *

from xxx

where expr1 in (select ...) or expr2;

用了not in

select *

from xxx

where expr not in (select ...);

select中套了子查询

select ...,(select ...)

from xxx;

having里套了子查询

select ...

from xxx

where expr

having (select ...);

子查询里用了union

select ...

form

where expr in (select ... union select ...)

这种非半连接子查询优化比较少,主要就是Materialization(物化)和IN-to-EXISTS(in转exist子句),这里不作过多介绍。

5. 参考

MySQL官方手册

《深入理解MariaDB与MySQL》

mysql 子查询概念_聊聊MySQL的子查询相关推荐

  1. mysql select 所有表_怎样用SQL语句查询一个数据库中的所有表

    展开全部 查询32313133353236313431303231363533e59b9ee7ad9431333431356639一个数据库中的所有表sql语句是show tables: 显示所有数据 ...

  2. dml语句包括哪些_聊聊MySQL基本操作DDL,DML,DQL,DCL

    本篇文章来回顾一下MySQL的基本操作之DDL,DML,DQL,DCL,每种操作都有各自不同的语法,常用的操作汇总如下. 一.DDL-数据定义语言 作用:数据定义语言主要用来定义数据库中的各类对象,包 ...

  3. mysql高级查询面试_高级MySQL数据库面试问题 附答案

    因为有大家的支持,我们才能做到现在,感谢你们这一路上对我们的支持.在这篇文章中,我们将主要针对MySQL的实用技巧,讲讲面试中相关的问题. 1. 如何使用SELECT语句找到你正在运行的服务器的版本并 ...

  4. mysql pool返回值_【Mysql】你知道一条查询语句是如何执行的吗?

    [Mysql]你知道一条查询语句是如何执行的吗?​mp.weixin.qq.com 前言 在默认大家学习了Mysql结构的基础上,我们来深入的解析一下sql语句在mysql中是如何流转和实现的.本文会 ...

  5. c语言 mysql 查询数字_使用mysql C语言API编写程序—MYSQL数据库查询操作

    //MYSQL_RES保存查询结构 MYSQL_RES* result_ = NULL; int error_code = 0;//保存错误码 char error_info[1024] = '\0' ...

  6. mysql最大句柄数_【mysql】最大连接数和最大句柄数的查询和修改

    查看mysql查询最大连接数 show variables like'%connections%' :mysql查询最大连接数 set global max_connections=5000: 修改最 ...

  7. mysql覆盖索引解决模糊查询失效_关于MySQL的SQL优化之覆盖索引

    前些天,有个同事跟我说:"我写了个SQL,SQL很简单,但是查询速度很慢,并且针对查询条件创建了索引,然而索引却不起作用,你帮我看看有没有办法优化?". 我对他提供的case进行了 ...

  8. 查询mysql日志存储位置_查看mysql日志存放地点

    登录mysql终端 日志文件路径 mysql> show variables like 'general_log_file'; +------------------+------------- ...

  9. mysql模糊查询提速_【MySQL】Mysql模糊查询like提速优化

    [MySQL]Mysql模糊查询like提速优化 在使用msyql进行模糊查询的时候,很自然的会用到like语句,通常情况下,在数据量小的时候,不容易看出查询的效率,但在数据量达到百万级,千万级的时候 ...

最新文章

  1. 【转】PendingIntent的总结
  2. 排序算法——基数排序
  3. 区县政府网站群建设新思路
  4. 十二、Python第十二课——函数
  5. (32)System Verilog模块调用包中类的方法
  6. Mysql之数据库锁(表锁和行锁)详解
  7. php判断是不是iphone访问,php基于http协议访问,判断访问来源iphone,android,微信浏览器,pc电脑...
  8. 什么是同步异步?阻塞非阻塞?
  9. android 如何正确使用 泛型 和 多参数 “偷懒”
  10. PCIe协议在具体实现时的大体框图
  11. MIMO系统的信号检测
  12. 4g模块Linux拨号ppp脚本,在ARM-linux上实现4G模块PPP拨号上网【转】
  13. p9刷Android原生系统,华为P9怎么刷机 华为P9卡刷机方法【图文教程】
  14. iOS组件化——蘑菇街案例分析
  15. c语言编计分秒时的程序,用c语言编写程序,用于c51单片机四位共阴数码管显示,显示分秒的计时器。...
  16. GEE:GEE平台怎么提高用户内存限制
  17. win10 无法访问samba文件,提示SMB1是不安全协议解决方案
  18. 卡卷接口API文档分享
  19. 我的生活(小兵,班长,连长....大将)
  20. 超级账本HyperLedger的cello项目的部署和使用

热门文章

  1. 转发与重定向的使用详解
  2. 简单的总是好的,在这个复杂的世界: java simple log
  3. Flutter学习之认知基础组件
  4. JavaScript事件循环探索
  5. windows下mongodb的安装与配置
  6. Java集成PageOffice在线打开编辑word文件 - Spring Boot
  7. 腾讯下载视频转换MP4
  8. 如何进行软件测试需求分析
  9. 如何使用CloudStats监控Linux服务器
  10. 短序列组装Sequence Assembly(转载)