深入理解MYSQL执行流程
本文由自己学习而记录笔记,主要便于增加记忆,更深入的理解MYSQL的执行流程,如有错误还请见谅!!,废话不多说,直接开始吧
首先,看下MYSQL整体的组件结构图如下
MySQL 可以分为 Server 层和存储引擎层两部分
1. 客户端发送一条查询给服务器。
2.服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段(MySQL8.0后已移除改功能)。
3.服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。
4. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
5.将结果返回给客户端。
所以MySQL查询的生命周期大致可以按照顺序来看:
从客户端,到服务器,然后在服务器 上进行解析,生成执行计划,执行,并返回结果给客户端。
其中“执行”可以认为是整 个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调 用后的数据处理,包括排序、分组等。
主要包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数 (如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在 最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。也就是说如果我们在create table时不指定 表的存储引擎类型,默认会给你设置存储引擎为InnoDB。
常用的客户端:navicat,mysql front,jdbc,SQLyog等非常丰富的客户端,这些 客户端要向mysql发起通信都必须先跟Server端建立通信连接,而建立连接的工作就是有连接器完成的。
第一步,你会先连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令一般是这么写的:
[root@192 ~]# mysql ‐h host[数据库地址] ‐u root[用户] ‐p root[密码] ‐P 3306
连接命令中的 mysql 是客户端工具,用来跟服务端建立连接。在完成经典的 TCP 握手后,连接器就要开始认证你的身份, 这个时候用的就是你输入的用户名和密码。 1、如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。 2、如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权 限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权 限。修改完成后,只有再新建的连接才会使用新的权限设置。用户的权限表在系统表空间的mysql的user表中。
修改user密码
mysql> CREATE USER 'username'@'host' IDENTIFIED BY 'password'; //创建新用户
mysql> grant all privileges on *.* to 'username'@'%'; //赋权限,%表示所有(host)
mysql> flush privileges //刷新数据库
mysql> update user set password=password(”123456″) where user=’root’;(设置用户名密码)
mysql> show grants for root@"%"; 查看当前用户的权限
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist(或者show full processlist) 命令中看到它。下图是 show processlist 的结果,其中的 Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。
客户端如果长时间不发送command到Server端,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值 是 8 小时。 查看wait_timeout
mysql> show global variables like "wait_timeout";
mysql>set global wait_timeout=28800; 设置全局服务器关闭非交互连接之前等待活动的秒数
如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候就需要重连,才能再执行请求了。
数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次 查询就断开连接,下次查询再重新建立一个。 而我们大多数时候用的都是长连接,把连接放在Pool内进行管理,但是长连接有些时候会导致 MySQL 占用内存涨得特别快,因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如 果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了。
解决问题思路?
1、定期断开长连接。
使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
2、如果用的是 MySQL 5.7 或更高版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
Mysql查询缓存是一个鸡肋的,这里只提一下。
连接建立完成后,就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存。 MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果查询能够直接在这个缓存中找 到 key,那么这个 value 就会被直接返回给客户端。 如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查 询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
高版本8.0已经移除查询缓存功能
为什么查询缓存就是个鸡肋?
因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。
因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率 会非常低。
一般建议大家在静态表里使用查询缓存,什么叫静态表呢?就是一般我们极少更新的表。
比如,一个系统配置表、字典 表,那这张表上的查询才适合使用查询缓存。
当缓存没有命中时,就需要执行真正的查询语句,这时MYSQL并不知道客户端要做什么,所以需要对语句做解析。
简单的说,就是将一条语句进行关键字提取和条件提取,然后按找语法规则生成一颗语法树。
词法分析器原理
词法分析器分成6个主要步骤完成对sql语句的分析
1、词法分析
2、语法分析
3、语义分析
4、构造执行树
5、生成执行计划
6、计划的执行
SQL词法分析的过程步骤:
SQL语句的分析分为词法分析与语法分析,mysql的词法分析由MySQLLex[MySQL自己实现的]完成,语法分析由Bison生 成。关于语法树如果想要深入研究可以参考这篇wiki文章:
语法树
经过了分析器,MySQL 就知道要做什么了。在开始执行之前,还要先经过优化器的处理。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。这里设计MYSQL内部的执行成本计算,关于成本计算请参考
MYSQL执行计划成本计算
开始执行的时候,要先判断一下用户对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。如果有权限,这按执行计划调用引擎提供的接口执行查询。
什么是bin-log呢?
binlog是Server层实现的二进制日志,他会记录我们的cud操作。
Binlog有以下几个特点:
1、Binlog在MySQL的Server层实现(引擎共用)
2、Binlog为逻辑日志,记录的是一条语句的原始逻辑
3、Binlog不限大小,追加写入,不会覆盖以前的日志
在SQL执行时,会将sql语句的执行逻辑记录在MYSQL的bin-log当中,如果我们误删了数据库,可以使用binlog进行归档!要使用binlog归档,首先我们得记录binlog,因此需要先开启MySQL的 binlog功能。
配置my.cnf
配置开启binlog
log‐bin=/usr/local/mysql/data/binlog/mysql‐bin
注意5.7以及更高版本需要配置本项:server‐id=123454(自定义,保证唯一性);
#binlog格式,有3种statement,row,mixed
binlog‐format=ROW
#表示每1次执行写入就与硬盘同步,会影响性能,为0时表示,事务提交时mysql不做刷盘操作,由系统决定
sync‐binlog=1
binlog命令
1 mysql> show variables like '%log_bin%'; 查看bin‐log是否开启
2 mysql> flush logs; 会多一个最新的bin‐log日志
3 mysql> show master status; 查看最后一个bin‐log日志的相关信息
4 mysql> reset master; 清空所有的bin‐log日志
查看binlog内容
1 mysql> /usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults /usr/local/mysql/data/binlog/mysql‐bin. 000001 查看binlog内容
binlog里的内容不具备可读性,所以需要我们自己去判断恢复的逻辑点位,怎么观察呢?看重点信息,比如begin,commit这种 关键词信息,只要在binlog当中看到了,你就可以理解为begin-commit之间的信息是一个完整的事务逻辑,然后再根据位置 position判断恢复即可。
数据归档操作
1 从bin‐log恢复数据
2 恢复全部数据
3 /usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults /usr/local/mysql/data/binlog/mysql‐bin.000001 |mysql ‐uroot ‐p tuling(数据库名)
4 恢复指定位置数据
5 /usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults ‐‐start‐position="408" ‐‐stop‐position="731" /usr/local/mysql/data/binlog/mysql‐bin.000001 |mysql ‐uroot ‐p tuling(数据库)
6 恢复指定时间段数据
7 /usr/local/mysql/bin/mysqlbinlog ‐‐no‐defaults /usr/local/mysql/data/binlog/mysql‐bin.000001 ‐‐stop‐date= "2018‐03‐02 12:00:00" ‐‐start‐date= "2019‐03‐02 11:55:00"|mysql ‐uroot ‐p test(数 据库)
慢查询日志,顾名思义,就是查询花费大量时间的日志,是指mysql记录所有执行超过long_query_time参数设定的时间阈值的SQL语句的日志。该日志能为SQL语句的优化带来很好的帮助。默认情况下,慢查询日志是关闭的,要使用慢查询日志功能,首先要开启慢查询日志功能。
1、慢查询配置
MYSQL默认值为关闭的,需要手动开启:
## 查看状态
show VARIABLES like 'slow_query_log'; ## OFF表示关闭 ON表示开启
## 开启
set GLOBAL slow_query_log=1;
## 查看慢查询阈值
show VARIABLES like '%long_query_time%';##默认10秒
## 设置阈值,如设置为1秒
set global long_query_time=1;
## 同时对于SQL语句没有使用索引,则MySQL数据库也可以将这条SQL语句记录到慢查询日志文件,控制参数是:
show VARIABLES like '%log_queries_not_using_indexes%';OFF表示关闭 ON表示开启
##指定输出的位置,通过参数log_output来控制 ,输出到[TABLE][FILE][FILE,TABLE]
set global log_output='FILE,TABLE';
##查询位置
show VARIABLES like 'log_output';
2、日志格式
“Time”:查询执行时间
“User@Host: root[root] @ localhost [] Id: 3”:用户名 、用户的IP信 息、线程ID号
“Query_time”:执行花费的时长【单位:秒】
“Lock_time”:执行获得锁的时长
“Rows_sent”:获得的结果行数
“Rows_examined”:扫描的数据行数
“SET timestamp”:这SQL执行的具体时间
最后一行:执行的SQL语句
3、慢查询分析
mysqldumpslow
常用的慢查询日志分析工具,汇总除查询条件外其他完全相同的SQL,并将分析结果 按照参数中所指定的顺序输出。常用参数。
语法: mysqldumpslow -s r -t 10 slow-mysql.log
-s order (c,t,l,r,at,al,ar)
c:总次数
t:总时间
l:锁的时间
r:获得的结果行数
at,al,ar :指t,l,r平均数 【例如:at = 总时间/总次数】*
-s 对结果进行排序,怎么排,根据后面所带的 (c,t,l,r,at,al,ar),缺省为at
-t NUM just show the top n queries:仅显示前n条查询
-g PATTERN grep: only consider stmts that include this string:通过grep 来筛选语句
4、SQL优化方法论
1).确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。
2).确认MySQL服务器层是否在分析大量超过需要的数据行。
分析步骤方法:
业务层:
1、是否查询不需要的记录
2、是否总是取出全部列
3、重复查询相同的数据
执行层:
1、响应时间
2、扫描的行数和返回的行数
3、扫描的行数和访问类型
重构SQL:
1、一个复杂查询还是多个简单查询
2、切分查询,分而治之
3、分解关联查询,减少资源竞争
1、show processlist
show processlist;查看线程状态
2、show profile分析
## 1、当前MySQL是否支持profile
select @@have_profiling; ## YES开启 NO关闭
## 2、Session级别开启 profiling:
select @@profiling;
set profiling=1;
##3、查询SQL之前情况
sql;
show profiles;## show profiles语句,看到当前SQL的Query_ID
## 4、查询
## 4.1 查看执行过程中线程的每个状态和消耗的时 间
show profile for query Query_ID;
## 4.2 all、cpu、block io、 contextswitch、page faults等明细类型来查看MySQL在使用什么资源上耗费了过高的 时间
show profile all for query Query_ID;
Intersection的意思是交集,通常是使用不同索引的搜索条件之间使用AND连接起来的情况,查询可以使用多个二级索引,将从多个二级索引中查询到的结果取交集比如:
SELECT * FROM Table WHERE idx1= ‘a’ AND idx2= ‘b’;
在innodb中除主键索引外,其他的索引都是非聚集索引,所以如果上面使用索引合并的话过程大概是:
1、查询idx1索引树中值为a的记录,在查询idx2索引树中的值为b的记录。
2、将两个记录合并取交集得到一个较少的主键id集合
3、再根据主键id集合去主键索引书中查询(回表),返回结果集
而是否合并取决于优化器的成本计算
成本计算可参考:
成本计算
大概的成本代价如下:
1、只读取一个二级索引的成本:
按照某个搜索条件读取一个二级索引,根据从该二级索引得到的主键值进行回表操作,然 后再过滤其他的搜索条件
2、读取多个二级索引之后取交集成本:
按照不同的搜索条件分别读取不同的二级索引,将从多个二级索引得到的主键值取交集, 然后进行回表操作。 虽然读取多个二级索引比读取一个二级索引消耗性能,但是大部分情况下读取二级索引的 操作是顺序I/O,而回表操作是随机I/O,所以如果只读取一个二级索引时需要回表的记录 数特别多,而读取多个二级索引之后取交集的记录数非常少,当节省的因为回表而造成的 性能损耗比访问多个二级索引带来的性能损耗更高时,读取多个二级索引后取交集比只读 取一个二级索引的成本更低。 MySQL在某些特定的情况下才可能会使用到Intersection索引合并。
可能会使用到所有合并的场景:
情况一:等值匹配
SELECT * FROM Table WHERE idx1= ‘a’ AND idx2= ‘b’;
二级索引列是等值匹配的情况,对于联合索引来说,在联合索引中的每个列都必须等值匹 配,不能出现只匹配部分列的情况。
情况二:主键列可以是范围匹配
SELECT * FROM Table WHERE primary_key > 10 AND idx1= ‘a’;
对于InnoDB的二级索引来说,记录先是按照索引列进行排序,如果该二级索引是一个联合索引,那么会按照联合索引中的各个列依次排序。而二级索引的用户记录是由索引列 + 主键构成的,二级索引列的值相同的记录可能会有好多条,这些索引列的值相同的记录又是按照主键的值进行排序的。
所以重点来了,之所以在二级索引列都是等值匹配的情况下才可能使用Intersection-索引合并,是因为只有在这种情况下根据二级索引查询出的结果集是按照主键值排序的。 Intersection索引合并会把从多个二级索引中查询出的主键值求交集,如果从各个二级索引中查询的到的结果集本身就是已经按照主键排好序的,那么求交集的过程就很容易。
ROR(Rowid Ordered Retrieval)
在求交集过程中,逐个取出这两个结果集中最小的主键值,如果两个值相等, 则加入最后的交集结果中,否则丢弃当前较小的主键值,再取该丢弃的主键值所在结果集 的后一个主键值来比较,直到某个结果集中的主键值用完了,时间复杂度是O(n)。 但是如果从各个二级索引中查询出的结果集并不是按照主键排序的话,那就要先把结果集 中的主键值排序完再来做上边的那个过程,就比较耗时了。这种按照有序的主键值去回表取记录有个专有名词,叫:Rowid Ordered Retrieval,简称 ROR。
另外,不仅是多个二级索引之间可以采用Intersection索引合并,索引合并也可以有聚簇索引参加,也就是我们上边写的情况二:在搜索条件中有主键的范围匹配的情况下也可以使用Intersection索引合并索引合并。
注意:
上边说的情况一和情况二只是发生Intersection索引合并的必要条件,不是充分条件。也就是说即使情况一、情况二成立,也不一定发生Intersection索引合并,这得看优化器的心情。优化器只有在单独根据搜索条件从某个二级索引中获取的记录数太多,导致回表开销太大,而通过Intersection索引合并后需要回表的记录数大大减少时才会使用 Intersection索引合并。
这里的union合并并非在SQL中使用union关键字,而是MYSQL中的一种优化策略。
在写查询语句时经常想把既符合某个搜索条件的记录取出来,也把符合另外的某个搜索条件的记录取出来,我们说这些不同的搜索条件之间是OR关系。有时候OR关系的不同搜 索条件会使用到不同的索引。
Union是并集的意思,适用于使用不同索引的搜索条件之间使用OR连接起来的情 况。与Intersection索引合并类似,MySQL在某些特定的情况下才可能会使用到Union索引合并
情况一:等值匹配
情况二:主键列可以是范围匹配
情况三:使用Intersection索引合并的搜索条件
搜索条件的某些部分使用Intersection索引合并的方式得到的主键集合和其他方式得到的主键集合取并集
当然和交集合并一样,并非满足情况就会使用合并,最终还是取决于成本代价。
从上面知道,Union索引合并的使用条件太苛刻,必须保证各个二级索引列在进行等值匹配的条件下才 可能被用到。
比如:idx1(c1), idx2(c2)
SELECT * FROM Table WHERE c1 < ‘a’ OR c2 > ‘b’;
但这两个条件都是索引字段条件,所以
1、先从idx1中取得满足小于a的记录,并按主键值排序,
2、从idx2中取得满足大于b的记录,并按主键值排序
3、然后再Union合并
这种先按照二级索引记录的主键值进行排序,之后按照Union索引合并方式执行的方式称之为Sort-Union索引合并
显然,这种Sort-Union索引合并比单纯的Union索引合并多了一步对二级索引记录的主键值排序的过程,效率会相对低些,成本也相对会高些。
在实际应用中,有时可能建立多个单字段索引,而实际业务中可能经常用到了多个索引字段条件,MYSQL在优化选择时会尽量考虑利用索引,这时就可能出现索引合并这种策略。而我们知道索引合并会多读几颗索引树,那如果把这几个字段合并为一个联合索引,就可以减少多读的这个过程,从而提升查询效率。