赞
踩
详细介绍了Mysql的基础架构以及一条查询sql的执行流程。
如果不想作为一个只能简单的写写sql工程师,而是想要深入的学习MySQL,那么我们有必要首先从宏观的角度来了解MySQL的整体架构,只有把握住了整体,才能深入细节。
面试中高级工程师的时候,常常被问到:一条sql语句在mysql中如何执行的?在学习了本文之后,你将会得到答案。
MySQL的简略整体架构图如下:
根据上图,Mysql架构可以简单分为Server层和存储引擎层:
下面依次简单介绍每一个组件!
连接器主要用于连接处理、授权认证、安全相等相关功能。
如果采用连接命令,则如下:mysql -h$ip -P$port -u$user -p
。
当客户端通过TCP三次握手
连接到MySQL服务器时,服务器对首先其进行身份认证
,可以通过用户名和密码的方式进行认证,也可以通过SSL证书进行认证。登录认证通过后,服务器还会查询出当前用户拥有的权限
,之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
每个客户端的连接都对应着服务器上的一个线程。服务器的连接器上维护了一个线程池
,避免为每个连接都创建销毁一个线程。
如果连接完成后,未来的一段时间里没做任何操作,这个连接就处于空闲
的状态,通过show processlist
命令中看到空闲连接列表,如下所示,Sleep标志就表示一个空闲连接:
客户端如果太长时间没动静,连接器就会自动将它断开,此时想要继续操作就需要重连。这个时间是由参数wait_timeout
控制的,通过show variables like 'wait_timeout';
可以查看,默认值是28800秒,即8小时
。
数据库建立连接的过程通常是比较复杂的,使用中应该尽量减少连接的动作,也就是尽量使用长连接
。但全部使用长连接后,有些时候MySql占用的内存会飙涨的很快。这是由于MySql在执行的过程中临时使用的内存是管理在连接对象里面的,这些资源会在连接断开的时候才释放,所以如果长连接累积下来,可能导致内存占用太大,导致被系统强行杀掉OOM(Out Of Memory),进而导致MySql异常重启。
此时有两个解决办法:
定期断开长连接
。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连;mysql 5.7
以后的版本,则可以在执行一个较大的操作后,通过执行mysql_reset_connection
来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态(初始化)。查询缓存(Query Cache)主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。
连接建立后,执行查询语句的时候,会先查询缓存
,MySQL 会先校验这个 sql 是否执行过,之前执行过的sql语句会以Key-Value
的形式缓存在Mysql内存中,Key 是查询sql语句
,Value 是结果集
。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。
对应的现象就是:同一条语句在MySQL执行两次,第一次和后面的时间是不一样的,后者明显快一些,这就是因为查询缓存的存在。
MySQL 查询不建议使用缓存,有两个原因:
单一的全局锁
来控制,这时候大量的查询将被阻塞,直至锁释放。所以不要简单认为设置缓存必定会带来性能提升。缓存失效
的问题,并且MySQL查询缓存的失效很容易:只要对表有任何的更新,这个表的所有查询缓存就会全部被清空
。这就很有可能出现缓存还没被使用过就被清空的情况,或者说一个更新将所有缓存都清除的情况。专业来说就是:缓存命中率低
,一般只有那种只查询不更新
的表适用缓存,但是这样的表往往很少存在,一般都是什么配置表之类的。可以通过设置MySQL参数query_cache_type
为DEMAND
,这样sql默认不使用缓存。
想要在某个sql中使用缓存就在select后使用SQL_CACHE
,在某个sql中不想使用缓存就在select后使用SQL_NO_CACHE
。例如:
select SQL_NO_CACHE * from XX
自MySQL 5.6
(2013)以来,默认情况下已禁用
查询缓存,而在MySQL 8.0
之后,查询缓存的整块功能已被移除
,移除的原因:https://mysqlserverteam.com/mysql-8-0-retiring-support-for-the-query-cache/,应该还是性能问题。
如果存在查询缓存且缓存没有命中,或者没有查询缓存,那么接下来会真正的开始sql执行,首先会走到分析器。分析器主要是用来分析 SQL 语句是来干嘛的,解析sql语句,即词法分析和语法分析
:
词法分析
:sql语句有很多单词、空格,MySQL就需要识别每个字符串所代表的是什么,是关键字,还是表名,还是列名等等。语法分析
:根据词法分析的结果,语法分析会判断你sql的语法对错,错了会提醒“you have an error in your SQL syntax
”错误,并且会提示你哪里错了,一般来说语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near
”的内容。词法分析和语法分析的具体过程涉及到《编译原理》,本人也不懂……。
经过了分析器后,即使sql语句语法没问题,在真正执行sql之前,还需要经过优化器的优化处理
。优化器会对你的sql选择他认为最优的执行方案去执行,但优化器的选择也并不一定是最优的
。
通常优化器的作用是:
最优索引
。连接顺序
。也就是说数据表的关联并不总是按照在查询中指定的顺序决定。重写查询,消除冗余的操作,选择子任务最优的策略
。比如在WHERE之后根一个1=1的条件,显然,查询条件中的 1=1 是完全多余的。在经过优化器优化之后的sql中这个条件将被去除。但有时候优化器的选择并不是最好的。将外连接转换为内连接
。并非所有的outer join都必须以外连接的方式执行,优化器能够识别等价条件并重写查询。优化count()、min()和max()
,比如MyISAM
就维护了一个变量来记录总数,比如mix()和max()
的字段如果有索引
,则直接取索引树的最左边和最右边
的值即可。提前终止查询
。查询已经满足条件时,MySQL总是能够立刻终止查询,比如使用了limit
字句的时候。索引覆盖扫描
。如果索引中包含需要返回的所有数据,则无需回表查询对应的数据行。负责生成执行计划
,也就是常说的通过EXPLAIN
看到的内容。如下Sql:
SELECT C FROM T WHERE A= 'value1' AND B = 'value2';
假设A、B字段都使用了单独的索引,其中A上扫描了 100个符合条件的数据行,B 上扫描 50个符合条件的数据行,最终结果只有30个数据行符合两个条件。
如果走A索引会有100个数据行,接着进行匹配找到其中的30个与 B 中的值匹配记录,其中就有 70 次是失败了。先根据 B会有 50 个数据行,接着进行匹配找到其中的 30 个与 A中的值匹配的记录,只有 20次是失败的。显然,选择使用B索引的效率会更高。
优化器选择索引
的目的就是选择一个扫描行数最少
的方案。行数越少,磁盘读取越少。但扫描行数不是唯一标准
,优化器还会结合是否使用临时表,是否回表,是否排序
等因素。
扫描行数
怎么判断?真正执行语句之前,mysql不知道具体有多少条,只能根据统计信息估算。这个统计信息就是索引的“区分度
”。索引上不同值越多,区分度越好。而一个索引上不同值的个数称为“基数
”,使用show index from tablename
可以查看,但在统计信息中,基数值都不一定准确
。
MySQL是怎样得到索引的基数
的呢?实际上是使用采样统计
的方法,为什么要采样统计呢?因为把整张表取出来一行行统计,虽然可以得到精确的结果,但是代价太高了,所以只能选择“采样统计
”。
采样统计的时候,InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
而数据表是会持续更新的,索引统计信息也不会固定不变。所以,当变更的数据行数超过1/M
的时候,会自动触发重新做一次索引统计。
在MySQL中,有两种存储索引统计的方式,可以通过设置参数innodb_stats_persistent
的值来选择:
on
的时候,表示统计信息会持久化存储。这时,默认的N是20,M是10
。off
的时候,表示统计信息只存储在内存中。这时,默认的N是8,M是16
。由于是采样统计,所以不管N是20还是8,这个基数都是很容易不准的。analyze table tablename
命令,可以用来重新统计索引信息。force index
则可以让查询强制选择指定的索引。
经过了优化器之后可以说这个语句具体该如何执行就已经定下来,接下来就进入执行器阶段,开始执行sql。
首先会对当前用户进行执行权限校验
,如果没有权限,就会返回没有权限的错误:ERROR 1142 (42000): SELECT command denied to user
。
如果有权限,就会去调用引擎的接口,返回接口执行的结果
。执行的时候,从第一行开始一行一行的去判断是否满足条件,读取一行的接口都是执行引擎定义好的,直接调用即可,直到取到表的最后一行,有索引的执行起来可能就好点,执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
数据库的慢查询日志中看到看一个rows_examined
字段,表示这个语句执行过程中扫描了多少行,这个值就是在执行器每次调用引擎获取数据行的时候累加的
,在有些场景下,执行器调用一次,在引擎内部则扫描了很多行,因此引擎扫描行数跟rows_examined
并不是完全相同的。
EXPLAIN
可以看到执行计划,也可以通过rows字段看到我们扫描了多少行:
面试中高级工程师的时候,常常被问到:一条sql语句在mysql中如何执行的?
那么在我们学习了Mysql的基础架构之后,我们可以总结出一条sql的基本执行流程,实际上就是按照组件顺序一步步的走就行,不需要死记硬背:
连接器
连接到数据库,然后校验是否有执行当前查询的权限,如果没有权限,则直接返回错误信息,如果有权限,则执行下一步;查询缓存(Query Cache)
,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有找到缓存,则执行下一步。MySQL 5.6及其之后默认都不会查询缓存,直接执行下一步。分析器
进行词法分析,识别出这条sql语句字符串里面的字符串分别是什么,代表什么意思,比如到底是操作的哪个表,哪个字段等等,随后进行语法分析,校验sql语法比如关键词的使用是否有误等。如果分析器检查没问题就执行下一步。优化器
对sql进行各种优化,比如在sql中可以使用多个多个索引的时候,决定使用哪个最优索引,或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序,或者改写查询,去除没必要的条件,比如where 1=1。优化结束执行下一步。执行器
,首先会进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,最后返回引擎的执行结果(如果有查询缓存则还会将结果存入缓存中),sql执行结束。上面的流程仅仅是针对一条查询的sql,如果这个sql不是只读sql而是一条更新sql,那么其过程将更加复杂, 将会涉及到Mysql的日志系统、隔离级别、锁等等知识,我们后面再学习。
参考资料:
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。