mysql优化篇(基于索引)

Posted HZhuizai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mysql优化篇(基于索引)相关的知识,希望对你有一定的参考价值。

在上一篇文章:Mysql索引(一篇就够le) 中介绍了索引的基本使用,分类和原理,也强烈建议先读Mysql索引(一篇就够le),然后继续本文的阅读

我们也知道mysql的优化可以从很多的方面进行,比如硬件,换个ssd的磁盘也很爽,环境的搭建,比如读写分离等,参数的配置,表结构的设计,字段类型的合理选用,sql的正确书写方式,索引的构建等,这里我们主要从sql的查询做优化,主要是基于走索引的方式去学习。

一、优化能够带来什么

优化能带来的肯定是爽!!!当你作为一个用户去体验某个网页正在加载中。。。内心肯定是崩溃的,秒出的网页肯定是爽。做开发测试的是每次查询几分钟甚至十几分钟,内心不会崩溃,只想砸电脑,所以速度快就是爽。速度快了,用户爽了,你公司爽了,你老板爽了,你也就爽了,薪资说不定也爽了。上面我们提到了优化可以从很多个方面进行:最常见的就是sql和索引的优化了,因为写CURD的小伙伴都免不了写个查询语句,然后语句走不走索引呢,这也会决定你爽不爽了,这种层次的优化相对成本低,效果好,主要还是根据业务场景来,不像msyql环境,配置啊等,因为会有很多个业务呀,不可能让环境是合适所有的业务场景吧,顶多有个合理居中值。(瞎扯,编不下去了。。。不过确实能带来爽和成就感)

二、优化思路

既然要做优化,首先要知道哪些需要优化吧,然后做分析这个sql慢是因为什么,然后才是怎么解决呢,在实际的环境中可能先要定位慢的语句,然后观察一段时间是不是一直都慢呢,还是有时候慢有时候又很快呢,这些都和实际环境中的并发,msyql环境当时的健康程度有关吧,所以要先锁定目标然后观察,然后把那些都慢的sql都弄出来进行分析,最后做优化。这里会说一下怎么找到慢的sql查询和分析,优化交给下一节。

2.1、慢查询语句(mysqldumpslow

mysql中有很多的日志文件,binlog日志,慢日志,查询日志,错误日志。这里我们要说的是慢日志,默认情况下是没有开始慢日志查询日志的,因为会影响一些些性能。(惨遭打脸,自己玩的竟突开启了)

show variables like \'%slow_query_log%\';

  虽然是开启了慢日志查询的功能,还得要有个阈值判断执行多久是慢日志查询吧(默认10s): show variables like \'%long_query_time%\';

   太长了,我们改改,10s一点都不爽;set global long_query_time=3;改完配置后记得重启session。来测试:

   上图是弄了一个超过3s的执行语句,查看慢日志的文件,确实被记录在里头了,还包括了很多其他的值,比如时间,用户,主机,执行时间等。

那如果这个慢日志的文件很大很大,很多的查询,不可能这么手动的去找吧。所有就要有慢日志的分析工具了:mysqldumpslow

[root@lgh mysql]# ./bin/mysqldumpslow --help
Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]

Parse and summarize the MySQL slow query log. Options are

  --verbose    verbose
  --debug      debug
  --help       write this text to standard output

  -v           verbose
  -d           debug
  -s ORDER     what to sort by (al, at, ar, c, l, r, t), \'at\' is default  #按照某种方式排序,默认at
                al: average lock time #平均锁定时间
                ar: average rows sent #平均返回记录数
                at: average query time #平均查询时间
                 c: count #访问次数
                 l: lock time #锁定时间
                 r: rows sent #返回记录
                 t: query time #查询时间
  -r           reverse the sort order (largest last instead of first) #倒序
  -t NUM       just show the top n queries  #topN
  -a           don\'t abstract all numbers to N and strings to \'S\'
  -n NUM       abstract numbers with at least n digits within names
  -g PATTERN   grep: only consider stmts that include this string  #正则
  -h HOSTNAME  hostname of db server for *-slow.log filename (can be wildcard),
               default is \'*\', i.e. match all
  -i NAME      name of server instance (if using mysql.server startup script)
  -l           don\'t subtract lock time from total time #总时间(包括锁定时间)

./bin/mysqldumpslow -s t -t 3 /apps/mysql8/logs/mysql/slow3306.log

./bin/mysqldumpslow -s t -t 2 -r /apps/mysql8/logs/mysql/slow3306.log

  用起来不是很难,当然还有很多其他的慢日志的一些工具,比如:mysqlsla,pt-query-digest等,这些都需要自己去安装的。

2.2、语句分析

当我们找到了我们的哪些慢查询后怎么去分析呢,当然是借助explain啦,当然还有个帅气的小伙伴,show profile,接下来我们就一一介绍。

2.2.1、explain

1、explain可以用来干嘛呢?

  1. 可以知道表的执行顺序
  2. 在sql中哪些索引可以使用
  3. 在sql中哪些索引实际上被用
  4. 数据读取操作的的操作类型
  5. sql中的每个表有多少行数据被优化器查询
  6. 表之间的引用

看完上面可能一脸懵逼吧,那就再看一遍,然后继续往下看就懂了

2、explain怎么玩呢?

语法:explain +  sql查询

explain select * from item_description td inner join (select * from item_general where item_id in (select item_id from item where item_id > 332604504321036693 and item_id < 332604504321036710)) tt on tt.item_id=td.item_id;

  如上我们先看一个explain+sql;

下面看下explain的具体说明:

1、id:select查询的序号列,表示查询select语句中表的执行顺序

  • 当id相同,则从上往下执行
  • 如果id不同,则从大到小的顺序执行
  • 如果id有相同的也有不同的,按照上面两个规则执行,先大到小,先上到下

2、select_type:表示SELECT语句的类型。它可以是以下几种取值:

  • SIMPLE:表示简单查询,其中不包括连接查询和子查询;
  • PRIMARY:表示主查询,或者是最外层的查询语句,最外层查询为PRIMARY,也就是最后加载的就是PRIMARY;
  • UNION:表示连接查询的第2个或后面的查询语句, 不依赖于外部查询的结果集
  • DEPENDENT UNION:连接查询中的第2个或后面的SELECT语句,依赖于外面的查询;
  • UNION RESULT:连接查询的结果;
  • SUBQUERY:子查询中的第1个SELECT语句;不依赖于外部查询的结果集
  • DEPENDENT SUBQUERY:子查询中的第1个SELECT,依赖于外面的查询;
  • DERIVED:导出表的SELECT(FROM子句的子查询),MySQL会递归执行这些子查询,把结果放在临时表里。
  • DEPENDENT DERIVED:派生表依赖于另一个表
  • MATERIALIZED:物化子查询
  • UNCACHEABLE SUBQUERY:子查询,其结果无法缓存,必须针对外部查询的每一行重新进行评估
  • UNCACHEABLE UNION:UNION中的第二个或随后的 select 查询,属于不可缓存的子查询

3、table:表示查询的表

4、partitions:查询将从中匹配记录的分区。该值适用NULL于未分区的表

5、type:表示表的连接类型

  • system:该表是仅有一行的系统表。这是const连接类型的一个特例
  • const: 数据表最多只有一个匹配行,它将在查询开始时被读取,并在余下的查询优化中作为常量对待。const表查询速度很快,因为只读取一次,const用于使用常数值比较PRIMARY KEY或UNIQUE索引的所有部分的场合。
  • eq_ref:对于每个来自前面的表的行组合,从该表中读取一行,可以用于使用=运算符进行比较的索引列 。比较值可以是常量,也可以是使用在此表之前读取的表中列的表达式
  • ref:对于来自前面的表的任意行组合,将从该表中读取所有匹配的行,ref可以用于使用“=”或“<=>”操作符的带索引的列。
  • fulltext:使用FULLTEXT 索引执行联接
  • ref_or_null:这种连接类型类似于ref,但是除了MySQL还会额外搜索包含NULL值的行。此联接类型优化最常用于解析子查询
  • index_merge:此联接类型指示使用索引合并优化。在这种情况下,key输出行中的列包含使用的索引列表,并key_len包含使用的索引 的最长键部分的列表
  • unique_subquery:类型替换 以下形式的eq_ref某些 IN子查询,unique_subquery 只是一个索引查找函数,它完全替代了子查询以提高效率。
  • index_subquery:连接类型类似于 unique_subquery。它代替IN子查询,但只适合子查询中的非唯一索引
  • range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引。key_len包含所使用索引的最长关键元素。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符用常量比较关键字列时,类型为range
  • index:该index联接类型是一样的 ALL,只是索引树被扫描。这发生两种方式:1、如果索引是查询的覆盖索引,并且可用于满足表中所需的所有数据,则仅扫描索引树。在这种情况下,Extra列显示为 Using index,2、使用对索引的读取执行全表扫描,以按索引顺序查找数据行。 Uses index没有出现在 Extra列中。
  • ALL:对于前面的表的任意行组合进行完整的表扫描
  • system>const>eq_ref>ref>index>ALL(这些比较常见,)

6、possible_keys:指出MySQL能使用哪个索引在该表中找到行。若该列是NULL,则没有相关的索引。在这种情况下,可以通过检查WHERE子句看它是否引用某些列或适合索引的列来提高查询性能。如果是这样,可以创建适合的索引来提高查询的性能。

7、kye:表示查询实际使用的索引,如果没有选择索引,该列的值是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX

8、key_len:表示MySQL选择的索引字段按字节计算的长度,若键是NULL,则长度为NULL。注意,通过key_len值可以确定MySQL将实际使用一个多列索引中的几个字段

9、ref:表示使用哪个列或常数与索引一起来查询记录。

10、rows:显示MySQL在表中进行查询时必须检查的行数。

11、filtered:按表条件筛选的行的百分比

12、Extra:表示MySQL在处理查询时的详细信息

  • using filesort: 使用了文件排序,很影响性能
  • using temporary: 使用了临时表,很影响性能
  • using index: 使用了覆盖索引
  • using where: 使用了where
  • using MRR: 使用了MRR优化
  • using join buffer: 使用了链接缓存
  • impossible where: where的条件总是false
  • select tbles optimized away: 在没有group by操作的情况下,不必等到执行阶段再计算
  • distnct: 优化distinct操作
  • using index condition: 使用了ICP优化

更多详情见官网:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html

2.2.2、show profile

Show Profile是mysql提供的可以用来分析当前会话中sql语句执行的资源消耗情况的工具,可用于sql调优的测量。默认情况下处于关闭状态,并保存最近15次的运行结果。

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 #显示交换次数开销信息
}

 show variables like \'%profiling%\';

  set global profiling =on;

  然后我们随性的多执行一些查询语句,在通过show profiles查看:

 上面有3列,一个是查询的id,一个是执行的时间,最后一个是执行语句。

然后我们对id=8的语句进行诊断:show profile cpu,block io for query Query_ID;/*Query_ID为#3步骤中show profiles列表中的Query_ID*/

  上表中第一列为该sql查询的状态,也是sql的一个生命周期,从开始,到权限,打开表,初始化,上锁,优化,统计,执行,发送数据,查询结束,关闭表等。

在状态这一列中要注意一些点:

  • converting HEAP to MyISAM:查询结果太大,内存不够,数据往磁盘上搬了。
  • Creating tmp table:创建临时表。先拷贝数据到临时表,用完后再删除临时表。
  • Copying to tmp table on disk:把内存中临时表复制到磁盘上,

从上看来主要的时间花在了数据的传输上面,所以可以针对该项进行优化,更多详情见官网:https://dev.mysql.com/doc/refman/8.0/en/show-profile.html

三、优化具体点

3.1、sql的执行顺序

在去写好sql和优化sql查询之前,知道sql的执行顺序尤为的重要(所以这里要多读几遍)

from ->on ->join ->where ->group by ->having ->select ->distinct ->order by ->limit
1、from 对查询指定的表计算笛卡尔积
2、on 按照 join_condition 过滤数据
3、join 添加关联外部表数据
4、where 按照where_condition过滤数据
5、group by 进行分组操作
6、having 按照having_condition过滤数据
7、select 选择指定的列
8、distinct 指定列去重
9、order by 按照order_by_condition排序
10、limit 取出指定记录量

3.2、创建索引和不该创建索引时机

应该创建索引

  • 主键,唯一,外键约束都会自动创建索引
  • 频繁作为查询条件的字段已经创建索引
  • 查询中与其他表关联的字段
  • 查询中排序的字段
  • 查询中统计或分组的字段

不应该创建索引

  • 频繁更新的字段
  • where条件用不到的字段
  • 表记录太少
  • 经常增删改的表
  • 数据重复且分布平均的表字段

3.3、join优化

制造点实验数据:

CREATE TABLE IF NOT EXISTS department(
id INT(11)  PRIMARY KEY AUTO_INCREMENT COMMENT\'部门编号(主键)\',
d_name VARCHAR(50) COMMENT\'部门名称\',
d_address VARCHAR(50) COMMENT\'部门地址\');
    
    
CREATE TABLE user(
id INT(11) NOT NULL PRIMARY KEY COMMENT\'员工编号\',
u_name VARCHAR(50) COMMENT\'员工姓名\',
sex int(11) COMMENT \'性别\',
d_id int(11) COMMENT \'部门id\',
word VARCHAR(50) COMMENT\'员工工作\',
entryTime datetime COMMENT\'员工入职时间\',
wage INT(11) COMMENT\'员工工资\',
bonus INT(11) COMMENT\'员工奖金\'
);

create table sex(
    id int(11),
    name char(1) not null
)

insert into sex values(\'1\',\'\'),(\'0\',\'\');

INSERT INTO user VALUES 
(\'1\', \'小王\', \'0\', \'2\', \'a写代码1\', \'2017-06-14 14:30:50\', \'4000\', null),
(\'2\', \'小李\', \'1\', \'3\', \'b写代码2\', \'2016-08-16 14:32:08\', \'20800\', \'5000\'),
(\'3\', \'小张\', \'1\', \'3\', \'c写代码2\', \'2016-05-04 14:33:05\', \'22700\', null),
(\'4\', \'小高\', \'1\', \'1\', \'a写代码2\', \'2015-07-08 14:33:54\', \'5000\', null),
(\'5\', \'小刘\', \'0\', \'4\', \'写代码3\', \'2017-11-08 14:35:35\', \'10000\', null),
(\'6\', \'王一\', \'1\', \'5\', \'d写代码1\', \'2016-11-01 14:36:28\', \'20000\', \'5000\'),
(\'7\', \'王二\', \'0\', \'2\', \'写代码5\', \'2018-03-22 14:38:44\', \'5000\', null),
(\'8\', \'李四\', \'1\', \'7\', \'写代码6\', \'2017-04-01 14:39:53\', \'5000\', null),
(\'9\', \'李一\', \'0\', \'4\', \'写代码8\', \'2018-08-01 14:40:43\', \'5000\', null);


INSERT INTO  department(id,d_name,d_address)
(1,\'销售部\',\'销售部地址\'),
(2,\'学业部\',\'学业部地址\'),
(3,\'董事部\',\'董事部地址\'),
(4,\'人力资源部\',\'人力资源部地址\'),
(5,\'产品部\',\'产品部地址\'),
(20,\'研发部\',\'三楼\');
View Code

  MySQL内部采用了一种叫做 nested loop join的算法。Nested Loop Join 实际上就是通过驱动表的结果集作为循环基础数据,然后一条一条的通过该结果集中的数据作为过滤条件到下一个表中查询数据,然后合并结果。如果还有第三个参与 Join,则再通过前两个表的 Join 结果集作为循环基础数据,再一次通过循环查询条件到第三个表中查询数据,如此往复,基本上MySQL采用的是最容易理解的算法来实现join。(一定要用小表驱动大表

看看这些表的索引情况:

  除了两个表的主键自动创建的主键索引外,没有其他的任何索引。

explain select * from department d left join user u on u.d_id = d.id;

  两个表都是全表扫描呢,命名department表在id上有主键索引呢,但是作为左连接,department需要保留全部的数据,所以建立索引是没什么影响的,接下来我们在user表上建立d_id的索引idx_d_id: create index idx_d_id on user (d_id);

  看结果可以得出user表使用了索引,减少了数据的读取,可以得出left join主要的优化在于右表的索引的创建,那right join也是一样在于左表的索引的情况,对于inner join呢?我们看看。

在user表没有建立idx_d_id索引前:

  在user表没有建立idx_d_id索引后:

  好像优化器会选择把小的表来驱动大的表,全表扫描小的表,大的表走索引。

这是两个表的lefter join和right join的情况,那三个表的呢?还是上面的原则,有一个表会全保留,其他的走索引就好了。所以就是小表用来做驱动,大表用来走索引,这样就可以提高left join和right join的速度了。当然索引也要合适。。

3.4、order by和group by优化

1、order by

  order by:就是排序,我们都知道InnoDB存储引擎的存储是根据主键按照顺序存储的,所以这些都是已经排好序的,但是我们又不仅仅是根据主键排序,还要更加其他列进行排序,这样又怎么弄呢,当然我们可以在这些列上建立索引呀(单列,或者组合索引,推荐组合),索引就是有序的,这样就不用额外的排序了,但是不可能每个列都创建好索引吧,还有就是默认的是asc排序,那desc排序呢,又当如何呢,这就会造成filesort,虽然可以排序,但是效率真的低,所以尽量不要使用。既然order by有两种排序,一种是通过索引的默认排序这样的速度好,还有就是filesort,但是filesort如何去优化下呢?

在MySQL中filesort 的实现算法实际上是有两种:

  • 双路排序:是首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行指针信息,然后在sort buffer 中进行排序。
  • 单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序。

  在MySQL4.1版本之前只有第一种排序算法双路排序,第二种算法是从MySQL4.1开始的改进算法,主要目的是为了减少第一次算法中需要两次访问表数据的 IO 操作,将两次变成了一次,但相应也会耗用更多的sortbuffer 空间。当然,MySQL4.1开始的以后所有版本同时也支持第一种算法。

  MySQL主要通过比较我们所设定的系统参数 max_length_for_sort_data的大小和Query 语句所取出的字段类型大小总和来判定需要使用哪一种排序算法。如果 max_length_for_sort_data更大,则使用第二种优化后的算法,反之使用第一种算法。所以如果希望 ORDER BY 操作的效率尽可能的高,一定要注意max_length_for_sort_data 参数的设置。

是有filesort的情况(说白了就是不走索引):

  1. where语句与order by语句,使用了不同的索引
  2. 检查的行数过多,且没有使用覆盖索引
  3. ORDER BY中的列不包含在相同的索引,也就是使用了不同的索引
  4. 对索引列同时使用了ASC和DESC
  5. where语句或者ORDER BY语句中索引列使用了表达式,包括函数表达式
  6. where 语句与ORDER BY语句组合满足最左前缀,但where语句中使用了条件查询。

同样我们使用3.3节中的实验数据(看完下面的结果可以好好体会下上面说的filesort的情况):

   在join条件下的order by情况:order by的子句只引用了联接中的第一个表,MySQL会先对第一个表进行排序,然后进行联接。也就是expain中的Extra的Using Filesort.否则MySQL先把结果保存到临时表(Temporary Table),然后再对临时表的数据进行排序.此时expain中的Extra的显示Using temporary Using Filesort(这里有个坑,就是使用inner join的情况下,默认会使用小表驱动大表,所以就算你把大表写在前面,然后排序只用了大表的字段,但是mysql优化器会把小表放在前面,然后使用了大表的排序,这样就会生成临时表)

    图1:第一种方法用于第一个非常量表中存在ORDER BY所依赖的列的索引,那就可直接使用已经有序的索引来查找关联表的数据,这种方式是性能最优的,因为不需要额外的排序动作

  图2:第二种方式用于ORDER BY所依赖的列全部属于第一张查询表且没有索引,那么我们可以先对第一张表的记录进行filesort(模式可能是模式1也可能是模式2),得到有序行索引,然后再做关联查询,filesort的结果可能是在内存中,也可能在硬盘上,这取决于系统变量sort_buffer_size

  图3:第三种方法用于当ORDER BY的元素不属于第一张表时,需要把关联查询的结果放入临时表,最后对临时表进行filesort

mysql> explain select * from department d left join user u on u.d_id = d.id order by d.d_name; #在没有索引的情况下,除了全表扫描,还有临时表,filesort
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | d     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |   100.00 | Using temporary; Using filesort                    |
|  1 | SIMPLE      | u     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    9 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

mysql> explain select * from department d left join user u on u.d_id = d.id order by d.id;#在没有索引的情况下,除了全表扫描,还有临时表,filsort
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+ | 1 | SIMPLE | d | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 100.00 | Using temporary; Using filesort | | 1 | SIMPLE | u | NULL | ALL | NULL | NULL | NULL | NULL | 9 | 100.00 | Using where; Using join buffer (Block Nested Loop) <

以上是关于mysql优化篇(基于索引)的主要内容,如果未能解决你的问题,请参考以下文章

mysql优化之索引优化

mysql数据库-查询优化之索引篇

MySQL高级篇——索引解决查询相关的优化问题

MySQL高级篇——索引解决查询相关的优化问题

mysql学习第10篇:数据库之索引与慢查询优化

MySQL优化之索引篇: Explain工具