想要对sql进行一些相关优化,我们需要了解查询sql的整个生命周期,sql的执行流程分成了如下图多个子任务,只要通过降低或者避免其中子任务的耗时,则整个sql性能就能得到提高。
客户端连接服务端并发送请求,执行之前还会做权限的判断。
在查询缓存中,看该SQL有没有执行过,如果有查询过,则把缓存结果返回。在MySQL内部一个查询缓存,查询缓存默认是不开启的。
因为查询缓存要求SQL和参数都要一样,所以这个命中率是非常低的。
没有开启查询缓存或没有命中,那么就到了解析器,解析器主要对SQL语法进行解析生成解析树。
得到没有经过优化解析树之后,需要对这棵树进行预处理,得到优化后的解析数。比如常量放在什么地方,如果有计算的东西,把计算的结果算出来等等…
查询优化器:这一步是MySQL性能中,最关键的核心点,也是我们的优化原则.我们平时所讲的优化SQL。该优化器基于最低成本的执行计划。
SELECT * FROM USER WHERE USERNAME = toby AND PASSWORD = 1是先执行username = toby还是password = 1?
每一条SQL的执行,查询优化器就是根据MySQL对数据统计表的一些信息,比如索引,数据量,会根据统计数据,进行一个综合的判定选择一个运行最快查询计划。
最低成本:由于mysql自身的统计信息可能不准确的,MySQL根据这些信息选了一个它自认为最优的方案,但这个方案可能和我们想象的不一样。
执行计划会传给查询执行引擎,执行引擎选择存储引擎来执行查询计划(涉及到磁盘IO读取),获取对应的结果并返回。
如果开了查询缓存,则返回结果给客户端,并且查询缓存也放一份。
mysql客户端和服务端的连接是”半双工“通信,则每次客户端请求后,需要等待服务端将所有的数据返回给客户端才能结束。
同时每一个客户端与服务端的连接都有如下状态:
对于一条查询有多种执行方式,优化器作用会基于成本(成本最低)选择一条‘最好’的执行计划。
-- 每当执行一个查询 可以通过查询当前会话的Last_query_cost获取其查询成本
selecr * from #{tableName} where #{query}
-- 获取当前查询成本
SHOW STATUS LIKE "Last_query_cost"
上述图显示 查询大概需要做12个数据页的随机查找才能完成。
多种原因会导致mysql优化器选择错误的执行计划
统计信息不准确
执行估算成本等于真实的成本、不考虑并发影响
我们期望时间短,优化器考虑成本最优
静态优化:简单的代数、常量进行等价转换,编译时优化,一次优化,多次执行。
动态优化:每次执行根据数据量和索引优化执行。
重排关联表的顺序。
外链接转换为内连接
等价变幻 (a=5 and a>5 = a>=5)
count()、min()和max() B-tree 可以很快获取到min 和max 通过explain 为"select tables optimized away" 常数计算。
转换为常数表达式 explain: type = const
覆盖索引
自查询优化
提前终止查询 比如空结果、limit。
等值传播
select a.fid from a inner join b using(fid) where a.fid >500
-- 等值传播
select a.fid from a inner join b where a.fid > 500 and b.fid >500
IN()查找 等待于mysql的 多个等值查找 适合列的取值重复。
统计信息由存储引擎实现,mysql生成查询执行计划时候从存储引擎获取统计信息,统计信息包括
表/索引占用的页数(页 存储引擎基本单元)
表的索引基数
基数又叫索引基数,是数据列所包含的不同值的数量。
例如,某个数据列包含值1、3、7、4、7、3,那么它的基数就是4
数据行/索引长度
数据行长度:每行数据长度限制
索引行长度:索引列长度限制
索引的分布情况
关联在mysql中并非指两个表以上的查询,而是每一次查询都是关联查询
Mysql关联执行的策略,任何关联查询均为嵌套循环关联操作,所谓嵌套循环为:现在一张表中取出符合条件的数据,循环取出数据查找下一个表匹配的行,以此类推最终找到所有匹配的行,根据行获取到对应的查找列。
for(int i =0 i<num;i++){
for(int j = 0;j<num2;j++){
for(int k = 0;k<num3;k++){
//...以此往复
}
}
}
对于n表关联,则嵌套循环会有 n阶乘或者n-1阶乘关联顺序,关联顺序种类被称为执行计划的‘’搜索空间“,当搜索空间超过optimizer_search_depth的时候,优化器不会评估每一个关联顺序成本,而是使用贪婪搜索的方式获取最优关联顺序。
mysql的执行计划是一棵包含全部查询的指令树,存储引擎执行这棵指令树并返回结果
-- 查看执行计划
EXPLAIN EXTENDED ${querySql};
SHOW WARNINGS;
# 强制不改变 查询条件为内连接的表关联
select STRAIGHT_JOIN {querySql}
STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取。这可用于联接优化器以次优顺序处理表的那些(少数)情况。
STRAIGHT_JOIN只适用于内连接,因为left join、right join已经知道了哪个表作为驱动表,哪个表作为被驱动表
排序是一个成本很高的操作,文件排序会使用很大的临时存储空间,如果可能避免排序或者使用索引排序。 索引排序参考之前博文高性能mysql学习笔记二构建高性能索引
当不能使用索引排序,mysql需要自行进行排序,排序方式数据量是否超过“排序缓冲区”大小不同,如果不超过该阈值内存排序,否则磁盘排序,两者都统称为文件排序。
磁盘排序:先将数据分开,对每个块使用快速排序,最终各个排好序的块进行合并。
排序算法:
同时mysql排序需要使用大量的临时存储空间。
关联表的排序
所有排序字段均来自第一个表:extra字段为“using filesort”
非上述情况,mysql会先将关联的结果存放临时表,再进行文件排序,extra字段"using temporary;Using filesort"。
这个产生的性能问题属于低级错误,大概率开发不会犯这种问题。
是否扫描了过多的数据性能指标
响应时间:需要区分是排队时间(等待锁,等待资源) + 执行sql时间。
扫描的行数返回的行数: 通过EXPLAIN分析sql。主要看type字段和extra字段。
mysql性能分析 第二章节。
复杂查询简化成多个简单查询(具体问题具体分析,衡量多个查询和一个复杂查询成本优劣)。
数据分治处理,比如批量更新、删除不必一次性全部执行,可以分成多个部分执行,减轻对服务器的压力,不必锁太多的数据。
分解关联查询
select * from tag
left join tag_post on tag.tag_id = tag_post.tag_id
left join post on post.post_id = tag_post.post_id
where
tag.name = "mysql"
#转换为三个单表查询
select * from tag where name = "mysql"
select * from tag_post where tag_id in (xxx,xxxx)
select * from post where post_id in (xxx,xxxx,xxxx,xxxx)
-- 语法示例
SELECT 字段 FROM table WHERE EXISTS (subquery);
-- 语法示例
SELECT 字段 FROM table WHERE cloumn_name in (subquery);
-- 例子
SELECT * FROM A WHERE EXISTS (SELECT 1 FROM B WHERE B.id = A.id);
SELECT * FROM A WHERE EXISTS A.id in(SELECT b.id FROM B WHERE B.cloumn = ${query});
上述使用exists 和in语法涉及到了相关子查询,主表A的数据依赖内部子查询的结果。
EXISTS / NOT EXISTS 用于检查子查询是否至少会返回一行数据,该子查询实际上并不返回任何数据,而是返回布尔值true或false>
创建两张表,一张singer 歌手表,一张song 歌曲表,两表之间的关联关系为1对多。
DROP TABLE IF EXISTS `singer`;CREATE TABLE `singer` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `signer_id` int(11) DEFAULT NULL COMMENT 'qq音乐的主键', `signer_mid` varchar(50) DEFAULT NULL COMMENT 'qq音乐唯一标识', `full_name` varchar(50) DEFAULT NULL COMMENT '歌手全名', `english_name` varchar(50) DEFAULT NULL COMMENT '英文名', `short_name` varchar(50) DEFAULT NULL COMMENT '歌手简称(别名 多个)', `contract_id` varchar(10) DEFAULT NULL COMMENT '国籍', PRIMARY KEY (`id`) USING BTREE, KEY `IDX_SINGER_MID` (`signer_mid`)) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;DROP TABLE IF EXISTS `song`;CREATE TABLE `song` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `song_id` int(11) DEFAULT NULL COMMENT 'qq音乐id', `song_mid` varchar(50) DEFAULT NULL COMMENT 'qq音乐mid', `song_name` varchar(50) DEFAULT NULL COMMENT '歌曲名称', `song_type` varchar(5) DEFAULT NULL COMMENT '歌曲类型', `album_id` varchar(50) DEFAULT NULL COMMENT '所属专辑', `singer_mid` varchar(255) DEFAULT NULL COMMENT '所属歌手' PRIMARY KEY (`id`) USING BTREE, KEY `IDX_NAME` (`song_name`)) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
场景如下:查找指定歌曲所属歌手,singer表中的singer_mid和song表中的name为普通索引
exists
SELECT si.full_name FROM singer siWHERE EXISTS (SELECT 1 FROM song WHERE si.signer_mid = song.`singer_mid` AND song_name = "画");
执行顺序:
对该语句进行explain分析如下:先全表扫描singer表,在将扫描结果带入自查询进行song表的比较。
-- explain extended + show warnings 显示真正执行的sqlselect `utopia`.`si`.`full_name` AS `full_name` from `utopia`.`singer` `si` where exists( select 1 from `utopia`.`song` where (( `utopia`.`si`.`signer_mid` = `utopia`.`song`.`singer_mid`) and (`utopia`.`song`.`song_name` = '画')))
从真实的执行sql来看,mysql并未对该sql语句进行优化。exists不会被mysql优化。
singer_mid
AND song_name = “画”。in语句
SELECT si.full_name FROM singer si WHERE si.signer_mid IN (SELECT singer_mid FROM song so WHERE so.song_name = "画")
explain 分析
select `utopia`.`si`.`full_name` AS `full_name` from `utopia`.`singer` `si` semi join (`utopia`.`song` `so`) where ((`utopia`.`so`.`song_name` = '画') and (`utopia`.`si`.`signer_mid` = ``.`singer_mid`))
Semi join 半连接查询: 检查一个结果集(外表)的记录是否在另外一个结果集(字表)中存在匹配记录,半连接仅关注”子表是否存在匹配记录”,而并不考虑”子表存在多少条匹配记录”,半连接的返回结果集仅使用外表的数据集,查询语句中IN或EXISTS语句常使用半连接来。
可能需要临时表存储数据,但是比上面的exists 性能要好很多。
join
SELECT si.full_nameFROM singer si INNER JOIN song so ON si.signer_mid = so.`singer_mid` WHERE so.song_name = "画"
简单的关联查询,首先执行song 使用IDX_NAME索引,再执行singer表 使用singer_mid 索引查找,查找性能比上边的更好
在union语句中mysql无法将外部的限制条件作用到每一条子句内,所以如果想两张表汇取出前20条,需要按照相同的排序条件和limit作用到每一个字据中
-- 耗性能的写法-- 会将A表和B表所有排序的数据汇总到临时表中再取出其中20条(SELECT A.name from A order by nameUNION ALLSELECT B.name from B order by name)limit 20;-- 正确的写法(SELECT A.name from A order by name limit 20 UNION ALLSELECT B.name from B order by name limit 20)limit 20;
同一张表的 where中多个条件(或者join)进行AND/OR操作可能会使用到 index merge 技术。index merge:对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union) ,参考本人好友的文章索引合并优化
索引合并并交集访问算法(Index Merge Intersection Access Algorithm):对所有的索引同时执行扫描,扫描的结果取交集。
索引合并联合访问算法(Index Merge Union Access Algorithm):对所有的索引同时执行扫描,扫描的结果取并集。
select * from song where song_name ="潜力" OR singer_mid = "000P8peU0HhORi"
**索引合并排序联合访问算法(Index Merge Sort-Union Access Algorithm):**排序联合算法和联合算法之间的区别在于,排序联合算法必须首先获取所有行的行ID,然后对它们进行排序,然后再返回这些行。
EXPLAINselect * from song where song_name >"潜力" OR singer_mid = "000P8peU0HhORi"
mysql不持索引的松散(跳跃式)扫描,索引扫描需要先确定起点和终点,即使需要的数据很少也要扫描索引中每一条。且不支持hash索引和并行执行。
-- 如下字句会扫描全表 并获取最小主键值,其实如果使用主见扫描 查询第一条即为最小值select min(primaryKeCloumn) from table where noramCloumn = ${query} -- 调整如下 强制走主键扫描select min(primaryKeCloumn) from table USE INDEX(PRIMARY) where noramCloumn = ${query} limit
mysql不支持对一张表同时进行查找和更新操作
UPDATE tb_testSET NAME = ( SELECT NAME FROM tb_test WHERE id= 2)WHERE `status` = 1
-- 报错如下:-- You can't specify target table 'tb_test' for update in FROM clause,-- 可以将查询作为临时表进行操作
UPDATE tb_testSET NAME = (select name from ( SELECT NAME FROM tb_test WHERE id = 2) as t)WHERE `status` = 1
mysql的优化器并非万能,他不一定按照我们想要的结果来进行优化执行,所以mysql提供了**查询优化提示(Query Optimizer Hints)**让我们去引导优化器进行更好的优化。
查询优化提示 | 详细说明 | 使用场景 |
---|---|---|
HIGH_PRIORITY | 优先操作,多条语句访问某个表时,使用该hint 的语句可以优先执行,简单的控制访问表的优先顺序 | 作用在insert/update/select语句中,只适用于表锁引擎。 |
LOW_PRIORITY | 滞后操作,条语句访问某个表时,使用该hint 的语句最后执行,简单的控制访问表的优先顺序 | 同HIGH_PRIORITY |
DELAYED | 使用该提示立即返回数据并将插入或修改的数据行放在缓存中,在数据库闲时写入磁盘 | 适用于 insert 和 replace, |
straight_join | 强制连接顺序 SELECT TABLE1.FIELD1, TABLE2.FIELD2 FROM TABLE1 STRAIGHT_JOIN TABLE2 WHERE…; 让关联表按照出现的顺序进行关联 | 放在select关键字之后,或者两个关联表之间 |
SQL_SMALL_RESUL TSQL_BIG_RESULT |
sql_small_result 告诉优化器结果集会很小,可以将结果集放在内存中的索引临时表,以避免排序操作;sql_big_result 则告诉优化器结果集会很大,建议使用磁盘临时表做排序操作; | 这两个提示只对select语句有效,它们告诉优化器对 group by 或者 distinct 查询如何使用临时表及排序。 |
sql_buffer_result | 告诉优化器将查询放入到一个临时表,然后尽可能地释放锁 | |
SQL_NO_CACHE | 获取实时数据,每次不获取缓存区,每次执行sql | |
SQL_CACHE | 必须要开启缓存,才能使用该关键字进行缓存查询 | |
for update lock in share mode |
这两个提示主要控制select 语句的锁机制。但只对实现了行级锁的存储引擎有效。使用该提示会对符合查询条件的数据加锁。 | |
USE INDEX IGNORE INDEX FORCE INDEX |
使用关键字 force index 来强制使用某个索引 使用 use index 的索引提示来使用某个索引 使用关键字 ignore index 来忽略某个索引 |