高性能mysql读书笔记三性能查询优化

文章目录

    • 1. sql执行生命周期
      • 1.1 客户端/服务端连接
      • 1.2 查询优化处理
        • 优化器作用
        • 优化策略
      • 1.3 统计数据
      • 1.4 mysql关联查询
      • 1.5 执行计划
      • 1.6 排序优化
    • 2. 优化数据访问
      • 2.1 查询数据过多
      • 2.2 扫描数据过多
      • 2.3 重构查询条件
    • 3. mysql相关不足
      • 3.1 EXISTS语法以及改进
      • 3.2 UNION 合并
      • 3.3 索引合并优化
      • 3.4 松散索引扫描
      • 3.5 最大最小值优化
      • 3.6 同时更新查找
    • 4. 查询优化器提示hint

1. sql执行生命周期

​ 想要对sql进行一些相关优化,我们需要了解查询sql的整个生命周期,sql的执行流程分成了如下图多个子任务,只要通过降低或者避免其中子任务的耗时,则整个sql性能就能得到提高。
高性能mysql读书笔记三性能查询优化_第1张图片

  1. 客户端连接服务端并发送请求,执行之前还会做权限的判断。

  2. 在查询缓存中,看该SQL有没有执行过,如果有查询过,则把缓存结果返回。在MySQL内部一个查询缓存,查询缓存默认是不开启的。

    因为查询缓存要求SQL和参数都要一样,所以这个命中率是非常低的。

  3. 没有开启查询缓存或没有命中,那么就到了解析器,解析器主要对SQL语法进行解析生成解析树。

    • 验证sql的语法错误 关键字顺序,别名等sql语法规范。
    • 检查数据表和数据列是否存在
    • 验证权限
  4. 得到没有经过优化解析树之后,需要对这棵树进行预处理,得到优化后的解析数。比如常量放在什么地方,如果有计算的东西,把计算的结果算出来等等…

  5. 查询优化器:这一步是MySQL性能中,最关键的核心点,也是我们的优化原则.我们平时所讲的优化SQL。该优化器基于最低成本的执行计划。

    SELECT * FROM USER WHERE USERNAME = toby AND PASSWORD = 1是先执行username = toby还是password = 1?
    每一条SQL的执行,查询优化器就是根据MySQL对数据统计表的一些信息,比如索引,数据量,会根据统计数据,进行一个综合的判定选择一个运行最快查询计划。
    
    最低成本:由于mysql自身的统计信息可能不准确的,MySQL根据这些信息选了一个它自认为最优的方案,但这个方案可能和我们想象的不一样。
    
  6. 执行计划会传给查询执行引擎,执行引擎选择存储引擎来执行查询计划(涉及到磁盘IO读取),获取对应的结果并返回。

  7. 如果开了查询缓存,则返回结果给客户端,并且查询缓存也放一份。

1.1 客户端/服务端连接

​ mysql客户端和服务端的连接是”半双工“通信,则每次客户端请求后,需要等待服务端将所有的数据返回给客户端才能结束。

同时每一个客户端与服务端的连接都有如下状态:

  • sleep: 服务端线程正在等待客户端发送请求。
  • Query: 服务端线程正在执行查询/给客户端发送结果。
  • locked: 服务端线程正在等待表锁。
  • Analying and Statistics: 正在收集存储引擎的统计信息,并生成查询计划。
  • Copying to tmp table [on disk]: 线程在执行查询并将结果复制到临时表中,如果含on disk 则说明在将临时表复制到磁盘中。
  • Sorting result: 正在对结果集进行排序。
  • Sending Data : 线程在多个状态传输数据、生成结果集、向客户端返回数据。

1.2 查询优化处理

优化器作用

对于一条查询有多种执行方式,优化器作用会基于成本(成本最低)选择一条‘最好’的执行计划。

-- 每当执行一个查询 可以通过查询当前会话的Last_query_cost获取其查询成本
selecr * from #{tableName} where #{query}

-- 获取当前查询成本
SHOW STATUS LIKE "Last_query_cost"

高性能mysql读书笔记三性能查询优化_第2张图片

​ 上述图显示 查询大概需要做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的 多个等值查找 适合列的取值重复。

1.3 统计数据

统计信息由存储引擎实现,mysql生成查询执行计划时候从存储引擎获取统计信息,统计信息包括

  • 表/索引占用的页数(页 存储引擎基本单元)

  • 表的索引基数

    基数又叫索引基数,是数据列所包含的不同值的数量。

    例如,某个数据列包含值1、3、7、4、7、3,那么它的基数就是4

  • 数据行/索引长度

    数据行长度:每行数据长度限制

    索引行长度:索引列长度限制

  • 索引的分布情况

1.4 mysql关联查询

关联在mysql中并非指两个表以上的查询,而是每一次查询都是关联查询

Mysql关联执行的策略,任何关联查询均为嵌套循环关联操作,所谓嵌套循环为:现在一张表中取出符合条件的数据,循环取出数据查找下一个表匹配的行,以此类推最终找到所有匹配的行,根据行获取到对应的查找列。

for(int i =0 i<num;i++){
   for(int j = 0;j<num2;j++){
     for(int k = 0;k<num3;k++){
        //...以此往复
     }
   }
}

高性能mysql读书笔记三性能查询优化_第3张图片

  • union查询 一系列的单个查询结果都放到一个临时表中,最终再从临时表获取完整数据。
  • 子查询 有子查询先执行子查询将结果放入临时表,在将其当作普通表,在进行关联查询。

对于n表关联,则嵌套循环会有 n阶乘或者n-1阶乘关联顺序,关联顺序种类被称为执行计划的‘’搜索空间“,当搜索空间超过optimizer_search_depth的时候,优化器不会评估每一个关联顺序成本,而是使用贪婪搜索的方式获取最优关联顺序。

1.5 执行计划

mysql的执行计划是一棵包含全部查询的指令树,存储引擎执行这棵指令树并返回结果

-- 查看执行计划
EXPLAIN EXTENDED ${querySql};

SHOW WARNINGS;
# 强制不改变 查询条件为内连接的表关联
select STRAIGHT_JOIN {querySql} 

STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取。这可用于联接优化器以次优顺序处理表的那些(少数)情况。
STRAIGHT_JOIN只适用于内连接,因为left join、right join已经知道了哪个表作为驱动表,哪个表作为被驱动表

1.6 排序优化

排序是一个成本很高的操作,文件排序会使用很大的临时存储空间,如果可能避免排序或者使用索引排序。 索引排序参考之前博文高性能mysql学习笔记二构建高性能索引

当不能使用索引排序,mysql需要自行进行排序,排序方式数据量是否超过“排序缓冲区”大小不同,如果不超过该阈值内存排序,否则磁盘排序,两者都统称为文件排序

磁盘排序:先将数据分开,对每个块使用快速排序,最终各个排好序的块进行合并。

排序算法

  • 两次传输排序:第一次读取行指针和排序字段进行排序,再次根据排好序的结果再次读取记录第二次可能会产生大量随机IO),因为只读行指针和排序字段,可以让缓存区容纳更多数据进行排序。
  • 单次传输排序:读取所需要数据,根据字段进行排序最后返回排序结果,减少IO操作,缺点会占用大量的空间。

同时mysql排序需要使用大量的临时存储空间。

关联表的排序

  • 所有排序字段均来自第一个表:extra字段为“using filesort”

  • 非上述情况,mysql会先将关联的结果存放临时表,再进行文件排序,extra字段"using temporary;Using filesort"。

2. 优化数据访问

2.1 查询数据过多

  • 查询数据不带分页但是只获取其中前n条数据
  • 关联查询或者查询了过多的列
  • 重复相同请求。

这个产生的性能问题属于低级错误,大概率开发不会犯这种问题。

2.2 扫描数据过多

是否扫描了过多的数据性能指标

  1. 响应时间:需要区分是排队时间(等待锁,等待资源) + 执行sql时间。

  2. 扫描的行数返回的行数: 通过EXPLAIN分析sql。主要看type字段extra字段

    mysql性能分析 第二章节。

2.3 重构查询条件

  1. 复杂查询简化成多个简单查询(具体问题具体分析,衡量多个查询和一个复杂查询成本优劣)。

  2. 数据分治处理,比如批量更新、删除不必一次性全部执行,可以分成多个部分执行,减轻对服务器的压力,不必锁太多的数据。

  3. 分解关联查询

    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)
    
    • 缓存,转换后的第一条sql 如果开启了缓存,则重复查询会直接获取到缓存。
    • 等值查询 后面的两个sql使用in可以使用多行等值查询。
    • 单表查询减少了锁的竞争。
    • 关联查询会造成同一条记录重复扫描。
    • 便于以后数据表拆分。

3. mysql相关不足

-- 语法示例
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>

3.1 EXISTS语法以及改进

创建两张表,一张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优化。

在这里插入图片描述

  1. 首先执行一次外部查询,并缓存结果集,如 SELECT * FROM singer
  2. 遍历外部查询结果集的每一行记录R,代入子查询中作为条件进行查询, SELECT 1
    FROM. song WHERE si.signer_mid = song.singer_mid AND song_name = “画”。
  3. 如果子查询有返回结果,则EXISTS子句返回TRUE(NOT EXISTS 相反返回FALSE),这一行R可作为外部查询的结果行,否则不能作为结果。

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 索引查找,查找性能比上边的更好

3.2 UNION 合并

在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;

3.3 索引合并优化

同一张表的 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"  

在这里插入图片描述

3.4 松散索引扫描

mysql不持索引的松散(跳跃式)扫描,索引扫描需要先确定起点和终点,即使需要的数据很少也要扫描索引中每一条。且不支持hash索引并行执行

3.5 最大最小值优化

-- 如下字句会扫描全表 并获取最小主键值,其实如果使用主见扫描 查询第一条即为最小值select min(primaryKeCloumn) from table where noramCloumn = ${query} -- 调整如下 强制走主键扫描select min(primaryKeCloumn) from table USE INDEX(PRIMARY) where noramCloumn = ${query}  limit

3.6 同时更新查找

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

4. 查询优化器提示hint

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 来忽略某个索引

你可能感兴趣的:(mysql,mysql)