SQL执行流程
查询缓存
Server 如果在查询缓存中发现了这条 SQL 语句,就会直接将结果返回给客户端;如果没 有,就进入到解析器阶段。需要说明的是,因为查询缓存往往效率不高,所以在 MySQL8.0 之后就抛弃了这个功能。
MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中 。key是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。 所以,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
大多数情况下查询缓存就是个鸡肋,看下面的例子:
SELECT employee_id,last_name FROM employees WHERE employee_id = 101;
SELECT employee_id,last_name FROM employees WHERE employee_id = 101;
查询缓存是提前把查询结果缓存起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL中的查询缓存,不是缓存查询计划,而是查询对应的结果。这就意味着查询匹配的鲁棒性大大降低 ,只有相同的查询操作才会命中查询缓存。两个查询请求在任何字符上的不同(例如:空格、注释、大小写),都会导致缓存不会命中 。因此MySQL的查询缓存命中率不高 。
同时,如果查询请求中包含某些系统函数、用户自定义变量和函数、一些系统表,如mysql、information_schema、performance_schema数据库中的表,那这个请求就不会被缓存。以某些系统函数举例,可能同样的函数的两次调用会产生不一样的结果,比如函数NOW ,每次调用都会产生最新的当前时间,如果在一个查询请求中调用了这个函数,那即使查询请求的文本信息都一样,那不同时间的两次查询也应该得到不同的结果 ,如果在第一次查询时就缓存了,那第二次查询的时候直接使用第一次查询的结果就是错误的!
此外,既然是缓存,那就有它缓存失效的时候 。MySQL的缓存系统会监测涉及到的每张表,只要该表的结构或者数据被修改 ,如对该表使用了INSERT、UPDATE、DELETE、TRUNCATE TABLE、ALTER TABLE、DROPTABLE或 DROP DATABASE语句,那使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除!对于更新压力大的数据库来说,查询缓存的命中率会非常低。 总之,因为查询缓存往往弊大于利,查询缓存的失效非常频繁。
一般建议大家在静态表里使用查询缓存,什么叫静态表 呢?就是一般我们极少更新的表。比如,一个系统配置表、字典表,这张表上的查询才适合使用查询缓存。好在MySQL也提供了这种“按需使用 ”的方式。你可以将 my.cnf 参数 query_cache_type 设置成 DEMAND,代表当 sql 语句中有 SQL_CACHE关键字时才缓存。比如:
query_cache_type=2
这样对于默认的SQL语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以供SQL_CACHE显示指定,像下面这个语句一样:
SELECT SQl_CACHE * FROM test WHERE ID=5;
查看当前mysql是否开启缓存机制 监控查询缓存的命中率:
show status like '%Qcache%';
运行结果解析:
Qcache_free_blocks : 表示查询缓存中海油多少剩余的blocks,如果该值显示较大,则说明查询缓存中的内部碎片 过多了,可能在一定的时间进行整理。
Qcache_free_memory : 查询缓存的内存大小,通过这个参数可以很清晰的知道当前系统的查询内存是否够用,DBA可以根据实际情况做出调整。
Qcache_hits : 表示有 多少次命中缓存 。我们主要可以通过该值来验证我们的查询缓存的效果。数字越大,缓存效果越理想。
Qcache_inserts : 表示多少次未命中然后插入 ,意思是新来的SQL请求在缓存中未找到,不得不执行查询处理,执行查询处理后把结果insert到查询缓存中。这样的情况的次数越多,表示查询缓存应用到的比较少,效果也就不理想。当然系统刚启动后,查询缓存是空的,这也正常。
Qcache_lowmem_prunes : 该参数记录有多少条查询因为内存不足而被移除 出查询缓存。通过这个值,用户可以适当的调整缓存大小。
Qcache_not_cached : 表示因为query_cache_type的设置而没有被缓存的查询数量。
Qcache_queries_in_cache : 当前缓存中缓存的查询数量 。
Qcache_total_blocks : 当前缓存的block数量。
解析器
在解析器中对 SQL 语句进行语法分析、语义分析。 如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL需要知道你要做什么,因此需要对SQL语句做解析。SQL语句的分析分为词法分析与语法分析。
分析器先做“ 词法分析 ”。你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面 的字符串分别是什么,代表什么。
MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语 句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。
接着,要做“ 语法分析 ”。根据词法分析的结果,语法分析器(比如:Bison)会根据语法规则,判断你输 入的这个 SQL 语句是否 满足 MySQL 语法 。
select department_id,job_id, avg(salary) from employees group by department_id;
如果SQL语句正确,则会生成一个这样的语法树: 下图是SQL分词分析的过程步骤:
优化器
在优化器中会确定 SQL 语句的执行路径,比如是根据 全表检索 ,还是根据 索引检索 等。
经过解释器,MySQL就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
比如:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联 (join) 的时候,决定各个表的连接顺序,还有表达式简化、子查询转为连接、外连接转为内连接等。
举例:如下语句是执行两个表的 join:
select * from test1 join test2 using(ID)
where test1.name='zhangwei' and test2.name='mysql高级课程';
方案1:可以先从表 test1 里面取出 name='zhangwei'的记录的 ID 值,再根据 ID 值关联到表 test2,再判
断 test2 里面 name的值是否等于 'mysql高级课程'。
方案2:可以先从表 test2 里面取出 name='mysql高级课程' 的记录的 ID 值,再根据 ID 值关联到 test1,
再判断 test1 里面 name的值是否等于 zhangwei。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化
器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。
如果你还有一些疑问,比如优化器是怎么选择索引的,有没有可能选择错等。后面讲到索引我们再谈。
在查询优化器中,可以分为 逻辑查询 优化阶段和 物理查询 优化阶段。
逻辑查询优化就是通过改变SQL语句的内容来使得SQL查询更高效,同时为物理查询优化提供更多的候选执行计划。通常采用的方式是对SQL语句进行等价变换 ,对查询进行重写 ,而查询重写的数学基础就是关系代数。对条件表达式进行等价谓词重写、条件简化,对视图进行重写,对子查询进行优化,对连接语义进行了外连接消除、嵌套连接消除等。
物理查询优化是基于关系代数进行的查询重写,而关系代数的每一步都对应着物理计算,这些物理计算往往存在多种算法,因此需要计算各种物理路径的代价,从中选择代价最小的作为执行计划。在这个阶段里,对于单表和多表连接的操作,需要高效地使用索引 ,提升查询效率。
执行器
截止到现在,还没有真正去读写真实的表,仅仅只是产出了一个执行计划。于是就进入了执行器阶段 。 在执行之前需要判断该用户是否 具备权限 。如果没有,就会返回权限错误。如果具备权限,就执行 SQL 查询并返回结果。在 MySQL8.0 以下的版本,如果设置了查询缓存,这时会将查询结果进行缓存。 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,调用存储引擎API 对表进行的读写。存储引擎API只是抽象接口,下面还有个存储引擎层 ,具体实现还是要看表选择的存储引擎。 比如:表 test 中,ID 字段没有索引,那么执行器的执行流程是这样的:
调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是1,如果不是则跳过,如果是则将这行存在结果集中;
调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句就执行完成了。对于有索引的表,执行的逻辑也差不多。
小结
SQL 语句在 MySQL 中的流程是: SQL语句 →查询缓存 →解析器 →优化器 →执行器 。
MySQL8中SQL执行原理
前面的结构图很复杂,我们需要抓取最核心的部分: SQL的执行原理。不同的DBMS的SQL的执行原理是相通的,只是在不同的软件中,各有各的实现路径。 既然一条sQL语句会经历不同的模块,那我们就来看下,在不同的模块中,sQL执行所使用的资源(时间)是怎样的。如何在MySQL中对一条sQL语句的执行时间进行分析。
1) 确认profiling是否开启
了解查询语句底层执行的过程:select @profiling 或者 show variables like '%profiling' 查看是否开启计划。开启它可以让MySQL收集在SQL
执行时所使用的资源情况,命令如下:
mysql> select @@profiling;
mysql> show variables like 'profiling';
profiling=0 代表关闭,我们需要把 profiling 打开,即设置为 1:
mysql> set profiling=1;
2) 多次执行相同SQL查询
然后我们执行一个 SQL 查询(你可以执行任何一个 SQL 查询):
mysql> select * from employees;
3) 查看profiles
查看当前会话所产生的所有 profiles:
mysql> show profiles; # 显示最近的几次查询
4) 查看profile
显示执行计划,查看程序的执行步骤:
mysql> show profile;
当然你也可以查询指定的 Query ID,比如:
mysql> show profile for query 7;
查询 SQL 的执行时间结果和上面是一样的。
此外,还可以查询更丰富的内容:
mysql> show profile cpu,block io for query 6;
继续:
mysql> show profile cpu,block io for query 7;
1、除了查看cpu、io阻塞等参数情况,还可以查询下列参数的利用情况。
Syntax:
SHOW PROFILE [type [, type] ... ]
[FOR QUERY n]
[LIMIT row_count [OFFSET offset]]
type: {
| ALL -- 显示所有参数的开销信息
| BLOCK IO -- 显示IO的相关开销
| CONTEXT SWITCHES -- 上下文切换相关开销
| CPU -- 显示CPU相关开销信息
| IPC -- 显示发送和接收相关开销信息
| MEMORY -- 显示内存相关开销信息
| PAGE FAULTS -- 显示页面错误相关开销信息
| SOURCE -- 显示和Source_function,Source_file,Source_line 相关的开销信息
| SWAPS -- 显示交换次数相关的开销信息
}
2、发现两次查询当前情况都一致,说明没有缓存。
在 8.0 版本之后,MySQL 不再支持缓存的查询 。一旦数据表有更新,缓存都将清空,因此只有数据表是静态的时候,或者数据表很少发生变化时,使用缓存查询才有价值,否则如果数据表经常更新,反而增加了 SQL 的查询时间。
MySQL5.7中SQL执行原理
上述操作在MySQL5.7中测试,发现前后两次相同的sql语句,执行的查询过程仍然是相同的。不是会使用 缓存吗?这里我们需要 显式开启查询缓存模式 。在MySQL5.7中如下设置:
1) 配置文件中开启查询缓存
在 /etc/my.cnf 中新增一行:
query_cache_type=1
2) 重启mysql服务
systemctl restart mysqld
3) 开启查询执行计划
由于重启过服务,需要重新执行如下指令,开启profiling。
mysql> set profiling=1;
4) 执行语句两次:
mysql> select * from locations;
5) 查看profiles
6) 查看profile
显示执行计划,查看程序的执行步骤:
mysql> show profile for query 1;
mysql> show profile for query 2;
结论不言而喻。执行编号2时,比执行编号1时少了很多信息,从截图中可以看出查询语句直接从缓存中 获取数据。
SQL语法顺序
随着Mysql版本的更新换代,其优化器也在不断的升级,优化器会分析不同执行顺序产生的性能消耗不同 而动态调整执行顺序。
|