Mysql sql的书写和执行顺序

前言:

  一直是想知道一条SQL语句是怎么被执行的,它执行的顺序是怎样的,然后查看总结各方资料,就有了下面这一篇博文了。

  本文将从MySQL总体架构--->查询执行流程--->语句执行顺序来探讨一下其中的知识。

 

一、MySQL架构总览:

  架构最好看图,再配上必要的说明文字。

  下图根据参考书籍中一图为原本,再在其上添加上了自己的理解。

 

  从上图中我们可以看到,整个架构分为两层,上层是MySQLD的被称为的‘SQL Layer’,下层是各种各样对上提供接口的存储引擎,被称为‘Storage Engine Layer’。其它各个模块和组件,从名字上就可以简单了解到它们的作用,这里就不再累述了。

 

二、查询执行流程

  下面再向前走一些,容我根据自己的认识说一下查询执行的流程是怎样的:

1.连接

  1.1客户端发起一条Query请求,监听客户端的‘连接管理模块’接收请求

  1.2将请求转发到‘连接进/线程模块’

  1.3调用‘用户模块’来进行授权检查

  1.4通过检查后,‘连接进/线程模块’从‘线程连接池’中取出空闲的被缓存的连接线程和客户端请求对接,如果失败则创建一个新的连接请求

 

2.处理

  2.1先查询缓存,检查Query语句是否完全匹配,接着再检查是否具有权限,都成功则直接取数据返回

  2.2上一步有失败则转交给‘命令解析器’,经过词法分析,语法分析后生成解析树

  2.3接下来是预处理阶段,处理解析器无法解决的语义,检查权限等,生成新的解析树

  2.4再转交给对应的模块处理

  2.5如果是SELECT查询还会经由‘查询优化器’做大量的优化,生成执行计划

  2.6模块收到请求后,通过‘访问控制模块’检查所连接的用户是否有访问目标表和目标字段的权限

  2.7有则调用‘表管理模块’,先是查看table cache中是否存在,有则直接对应的表和获取锁,否则重新打开表文件

  2.8根据表的meta数据,获取表的存储引擎类型等信息,通过接口调用对应的存储引擎处理

  2.9上述过程中产生数据变化的时候,若打开日志功能,则会记录到相应二进制日志文件中

 

3.结果

  3.1Query请求完成后,将结果集返回给‘连接进/线程模块’

  3.2返回的也可以是相应的状态标识,如成功或失败等

  3.3‘连接进/线程模块’进行后续的清理工作,并继续等待请求或断开与客户端的连接

 

一图小总结

 

 

三、SQL解析顺序

  接下来再走一步,让我们看看一条SQL语句的前世今生。

  首先看一下示例语句

 

SELECT DISTINCT
    < select_list >
FROM
    < left_table > < join_type >
JOIN < right_table > ON < join_condition >
WHERE
    < where_condition >
GROUP BY
    < group_by_list >
HAVING
    < having_condition >
ORDER BY
    < order_by_condition >
LIMIT < limit_number >

 

    然而它的执行顺序是这样的

 

FROM <left_table>
ON <join_condition>
<join_type> JOIN <right_table>
WHERE <where_condition>
GROUP BY <group_by_list>
HAVING <having_condition>
SELECT 
DISTINCT <select_list>
ORDER BY <order_by_condition>
LIMIT <limit_number>

 

  虽然自己没想到是这样的,不过一看还是很自然和谐的,从哪里获取,不断的过滤条件,要选择一样或不一样的,排好序,那才知道要取前几条呢。

既然如此了,那就让我们一步步来看看其中的细节吧。

 

准备工作

  1.创建测试数据库

create database test

  2.创建测试表

 

CREATE TABLE `score` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` char(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `score` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

 

  3.插入数据  同名同姓的多个张三参加了多次考试,一个打酱油的还忘记在试卷上写名字了

INSERT INTO user (name)
VALUES
	('张三'),('pendant59'),('张三'),('张三');
	
INSERT INTO score ( uid,score )
VALUES
	( '1', 99 ),( '1', 69 ),
	( '2', 59 ),( '2', 80 ),
	( '2', 99 ),( '3', 75 ),
	( '', 59);

  4.最后想要的结果: 让我们找出得分最高的张三的ID和他考试的总次数

 

SELECT
	u.`id`,
	u.`name`,
	count( s.id ) AS total,
	max( s.score ) AS max_score 
FROM
	`user` AS u
	LEFT JOIN score AS s ON u.`id` = s.`uid` 
WHERE
	u.NAME = '张三' 
GROUP BY
	u.id
HAVING
	max(s.score) > 0
ORDER BY
    max_score DESC
LIMIT 1;

 

 

现在开始SQL解析之旅吧!

主表:user , 关联表 score

1. FROM

当涉及多个表的时候,左边表的输出会作为右边表的输入,之后会生成一个虚拟表VT1。

(1-J1) 笛卡尔积

计算两个相关联表的笛卡尔积(CROSS JOIN) ,生成虚拟表VT1-J1。28条记录  u:4 s:7   4*7=28

SELECT * FROM `user`, score

目标sql 生成的 VT1-J1 表: 

 

(1-J2) on 是为 join 服务的,外联接下,关注的是关联表的数据,内连接(inner join)则关注关联表和主表的数据

1. on 的目的 是 基于虚拟表VT1-J1这一个虚拟表进行过滤,过滤出所有满足on 条件的列,生成虚拟表VT1-J2。

2. 外联接 on 关注的是 关联表, 虽然在on中添加了 u.name 限制条件,但是结果还是返回了主表中所有数据:

3. 外联接 on 是多条件,仍然只对关联表进行筛选:​​​​​​

 蓝色框是关联表的数据 pendant59已经被剔除,所以红色框内主表中pendant59对应的关联表中的数据是空

 

inner join ,不需要添加外部列,主表和关联表在on的基础上 取交集 (可以看作是目标sql查询生成的 VT1-J2 表,实际上不是,但是 on 和 添加外部列 没办法拆开显示)

因为目标sql是 left join,需要添加外部列,所以会有J3表

 

(1-J3) 添加外部列(外连接)

如果使用了外连接(LEFT,RIGHT,FULL),主表(user)中所有列会被加入到VT1-J2中,作为外部行,生成虚拟表VT1-J3

目标sql查询结果 VT1 表 : 最后id为 4的 张三 是添加的外部列 (这样主表所有的列都在VT1中了)

下面从网上找到一张很形象的关于‘SQL JOINS'的解释图,如若侵犯了你的权益,请劳烦告知删除,谢谢。

 

 

2. WHERE

1. 对上一步生成的临时表VT1进行过滤(这时只针对VT1表内的数据,会把VT1中不符合where 条件的记录全部过滤掉),满足WHERE子句的列被插入到VT2表中。

注意:

此时因为分组,不能使用聚合运算;也不能使用SELECT中创建的别名:

更改sql 查询的字段,统计记录条数并设置别名

目标sql查询结果 VT2 表: 已将 VT1中 name != 张三的记录排除了

Where 和 On 的比较:

在外连接的情况下 on 关注的是关联表,筛选关联表的数据,目的是生成虚拟表 VT1

在内连接的情况下 on 关注的是关联表 和 主表,筛选关联表和主表的数据,目的是生成虚拟表 VT1

where 关注的是 VT1 虚拟表, 筛选虚拟表VT1中的数据服务,目的是生成虚拟表VT2

on 和 where 关注的对象不同,目的也不同。 

3. GROUP BY

这个子句会把VT2中生成的表按照GROUP BY中的列进行分组。生成VT3表。

注意:

其后处理过程的语句,如SELECT,HAVING,所用到的列必须包含在GROUP BY中,对于没有出现的,得用聚合函数;

原因:

GROUP BY改变了对表的引用,将其转换为新的引用方式,能够对其进行下一级逻辑操作的列会减少;

原作者的理解是:

根据分组字段,将具有相同分组字段的记录归并成一条记录,因为每一个分组只能返回一条记录,除非是被过滤掉了,而不在分组字段里面的字段可能会有多个值,多个值是无法放进一条记录的,所以必须通过聚合函数将这些具有多值的列转换成单值;

这里的 select * 就是 就是只通过group by 聚合后的数据生成的VT3表包含的字段,但是因为包含 非聚合的 s.id 所以无法生成VT3表,于是会抛出异常。

可以通过去掉配置文件 中sql_mode 选项值 ONLY_FULL_GROUP_BY 使mysql 支持当前sql,但是不推荐,还是按标准的方式书写。

(原文作者可以正常查询,博文发表时间是15年,应该是5.7以下版本,默认就没有ONLY_FULL_GROUP_BY)

 这里报错说 包含非聚合的s.id 跟原文作者的理解是一样的,在上面的VT2表中, 

s.id,s.score 都是 有多个不同值,在group by 的时候无法合并,所以要想select 中包含 s.id 和 s.score 就要用聚合函数来使他们变成一个值,然后group by 才能生成一条记录(聚合函数:max(), min(),count(),sum(),avg())

这个是不包含有多个值的查询(group by 可以 u.id 正常分组,返回唯一记录),查询正常,结果也生成了当前查询对应的VT3表

经过聚合函数处理后的 s.id 生成了新的唯一字段  也可以正常查询了,结果也生成了当前查询对应的VT3表(你也可以用sum,avg 等聚合函数,这里是为了目标sql)

 

目标sql查询结果 VT3 表: 

呐呐呐,这里答案就已经呼之欲出了 (因为sql_mode的配置原因,跟原作者查询的sql不一样,原作者可以select * )。

4. HAVING

这个子句对VT3表中的不同的组进行过滤,只作用于分组后的数据,满足HAVING条件的子句被加入到VT4表中。

 

 

接下来就意思一下 来个having( 关于 having 后面跟不跟select 的别名 详见文末补充),对VT3的记录进行筛选生成VT4

目标sql查询结果 VT4 表: 

(因为sql_mode的配置原因,跟原作者查询的sql不一样,原作者可以select * )

5. SELECT

这个子句对SELECT子句中的元素进行处理,生成VT5表。

(5-J1)计算表达式 计算SELECT 子句中的表达式,生成VT5-J1

(5-J2)DISTINCT  (如果有用到)

寻找VT5-1中的重复列,并删掉,生成VT5-J2

如果在查询中指定了DISTINCT子句,则会创建一张内存临时表(如果内存放不下,就需要存放在硬盘了)。这张临时表的表结构和上一步产生的虚拟表VT5是一样的,不同的是对进行DISTINCT操作的列增加了一个唯一索引,以此来除重复数据。

目标sql查询结果 VT5 表: 

这里和having 生成的表其实是一样的 因为select 的已经在vt4表中了 (因为sql_mode的配置原因,跟原作者查询的sql不一样,原作者可以select * 所以可以把 VT4 和VT5 分开展示)

 

 

6.ORDER BY

从VT5-J2中的表中,根据ORDER BY 子句的条件对结果进行排序,生成VT6表。

注意:

唯一可使用SELECT中别名的地方;

目标sql查询结果 VT6 表:

 

 

7.LIMIT

LIMIT子句从上一步得到的VT6虚拟表中选出从指定位置开始的指定行数据。

注意:

offset和rows的正负带来的影响;

当偏移量很大时效率是很低的,可以使用 inner join 优化:

例如  select * from test a inner join (select id from test where val=4 limit 300000,5) b on a.id=b.id;  # join里面不要用 *

参考链接 

 https://www.cnblogs.com/zhangyachen/p/8030252.html

 

 https://github.com/zhangyachen/zhangyachen.github.io/issues/117

至此SQL的解析之旅就结束了,上图总结一下:

 

参考书籍:

《MySQL性能调优与架构实践》

《MySQL技术内幕:SQL编程》

 

补充:

1. 关于having后面是否可以跟 select中定义的别名的问题,经测试,Mysql 5.7.x版本 允许在having condition中使用select list中的alias, (配置文件中的 sql_mode 包含 ONLY_FULL_GROUP_BY 依然可以), 但是标准的sql语句是不能在having后面使用select 中定义的别名 https://www.w3school.com.cn/sql/sql_having.asp 

看了原作者的文章,又用了四五个小时自己过了一遍,添加了一些补充说明,收获颇多。撒花~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章