高性能MySQL调优实战

一,数据库应该如何优化

数据库优化有很多层面。

1,SQL与索引

因为 SQL 语句是在我们的应用端编写的,所以第一步,我们可以在程序中对 SQL 语句进行优化,最终的目标是用到索引。这个是容易的也是最常用的优化手段。

2,表与存储引擎

数据是存放在表里面的,表又是以不同的格式存放在存储引擎中的,所以我们可以选用特定的存储引擎,或者对表进行分区,对表结构进行拆分或者冗余处理,或者对表结构比如字段的定义进行优化。

3,架构

对于数据库的服务,我们可以对它的架构进行优化。

如果只有一台数据库的服务器,我们可以运行多个实例,做集群的方案,做负载均衡。

或者基于主从复制实现读写分离,让写的服务都访问 master 服务器,读的请求都访问从服务器,slave 服务器自动 master 主服务器同步数据。

或者在数据库前面加一层缓存,达到减少数据库的压力,提升访问速度的目的。

为了分散数据库服务的存储压力和访问压力,我们也可以把不同的数据分布到不同的服务节点,这个就是分库分表(scale out)。

注意主从(replicate)和分片(shard)的区别:

主从通过数据冗余实现高可用,和实现读写分离。

分片通过拆分数据分散存储和访问压力。

4,配置

数据库配置的优化,比如连接数,缓冲区大小等等,优化配置的目的都是为了更高效地利用硬件。

5,操作系统与硬件

从上往下,成本收益比慢慢地在增加。所以肯定不是查询一慢就堆硬件,堆硬件叫做向上的扩展(scale up)。

二,慢日志查询

1,概述

MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。

long_query_time的默认值为10,意思是运行10秒以上的语句。

由他来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合explain进行全面分析。

2,实操

默认情况下,MySQL数据库没有开启慢查询日志,需要我们手动来设置这个参数。

当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。

1)查看及开启

①日志

SHOW VARIABLES LIKE '%slow_query_log%'; 

默认情况下slow_query_log的值为OFF,表示慢查询日志是禁用的。

set global slow_query_log=1;

只对窗口生效,重启服务失效。

②时间

SHOW VARIABLES LIKE '%long_query_time%';
SET GLOBAL long_query_time=0.1;

全局变量设置,对所有客户端有效。但,必须是设置后进行登录的客户端。

SET SESSION long_query_time=0.1; #session可省略

对当前会话连接立即生效,对其他客户端无效。

假如运行时间正好等于long_query_time的情况,并不会被记录下来。也就是说,在mysql源码里是判断大于long_query_time,而非大于等于。

③永久生效

修改配置文件my.cnf(其它系统变量也是如此)

[mysqld]下增加或修改参数

slow_query_log 和slow_query_log_file后,然后重启MySQL服务器。也即将如下两行配置进my.cnf文件 1

slow_query_log =1
slow_query_log_file=/var/lib/mysql/yhd-slow.log 
long_query_time=3
log_output=FILE

2)Case

记录慢SQL并后续分析

查询当前系统中有多少条慢查询记录

SHOW GLOBAL STATUS LIKE '%Slow_queries%'; 

3,日志分析工具-mysqldumpslow

在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow。

查看mysqldumpslow的帮助信息

mysqldumpslow --help

高性能MySQL调优实战_第1张图片

-a: 不将数字抽象成N,字符串抽象成S
-s: 是表示按照何种方式排序;
  c: 访问次数
  l: 锁定时间
  r: 返回记录
  t: 查询时间
  al:平均锁定时间
  ar:平均返回记录数
  at:平均查询时间
-t: 即为返回前面多少条的数据;
-g: 后边搭配一个正则匹配模式,大小写不敏感的;

1)常用SQL

得到返回记录集最多的10个SQL
mysqldumpslow -s r -t 10 /var/lib/mysql/yhd-slow.log
得到访问次数最多的10个SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/yhd-slow.log
得到按照时间排序的前10条里面含有左连接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/yhd-slow.log
另外建议在使用这些命令时结合 | 和more 使用 ,否则有可能出现爆屏情况
mysqldumpslow -s r -t 10 /var/lib/mysql/yhd-slow.log | more

4,SHOW PROCESSLIST

作用:查询所有用户正在干什么。

如果出现不顺眼的:kill [id]

三,EXPLAIN

1,查看执行计划

使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是 如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。

1)作用

表的读取顺序

数据读取操作的操作类型

哪些索引可以使用

哪些索引被实际使用

表之间的引用

每张表有多少行被优化器查询

2)操作

Explain + SQL语句

执行计划包含的信息:

MYSQL 5.6.3以前只能EXPLAIN SELECT;

MYSQL 5.6.3以后就可以EXPLAIN SELECT,UPDATE,DELETE

注意:在5.7以前的版本中,想要显示partitions需要使用explain partitions命令;想要显示filtered需要使用explain extended命令。

在5.7版本后,默认explain直接显示partitions和filtered中的信息。

Column JSON Name Meaning
id select_id The SELECT identifier
select_type None The SELECT type
table table_name The table for the output row
partitions partitions The matching partitions
type access_type The join type
possible_keys possible_keys The possible indexes to choose
key key The index actually chosen
key_len key_length The length of the chosen key
ref ref The columns compared to the index
rows rows Estimate of rows to be examined
filtered filtered Percentage of rows filtered by table condition
Extra None Additional information

3)建表语句

 CREATE TABLE t1(id INT(10) AUTO_INCREMENT,content  VARCHAR(100) NULL ,  PRIMARY KEY (id));
 CREATE TABLE t2(id INT(10) AUTO_INCREMENT,content  VARCHAR(100) NULL ,  PRIMARY KEY (id));
 CREATE TABLE t3(id INT(10) AUTO_INCREMENT,content  VARCHAR(100) NULL ,  PRIMARY KEY (id));
 CREATE TABLE t4(id INT(10) AUTO_INCREMENT,content  VARCHAR(100) NULL ,  PRIMARY KEY (id));
 
 INSERT INTO t1(content) VALUES(CONCAT('t1_',FLOOR(1+RAND()*1000))); 
 INSERT INTO t2(content) VALUES(CONCAT('t2_',FLOOR(1+RAND()*1000)));  
 INSERT INTO t3(content) VALUES(CONCAT('t3_',FLOOR(1+RAND()*1000)));    
 INSERT INTO t4(content) VALUES(CONCAT('t4_',FLOOR(1+RAND()*1000)));

2,explain字段解释

1)id

select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。

三种情况

1)id相同,执行顺序由上至下

EXPLAIN SELECT * FROM t1 ,t2 ,t3;

在这里插入图片描述

2)id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行

mysql> explain select t1.id from t1 where t1.id=(select t2.id from t2 where t2.id = (select t3.id from t3 where t3.content='t3_839'));

在这里插入图片描述

3)id相同,不同,同时存在

explain select t2.* from t2,(select * from t3 where t3.content='') s3 where s3.id = t2.id;

在这里插入图片描述

id如果相同,可以认为是一组,从上往下顺序执行;

在所有组中,id值越大,优先级越高,越先执行

衍生 = DERIVED

关注点:id号每个号码,表示一趟独立的查询。一个sql的查询趟数越少越好。

2)select_type

查询类型主要用于区分普通查询。联合查询,子查询等复杂的查询。

1)SIMPLE

简单的 select 查询,查询中不包含子查询或者UNION

在这里插入图片描述

2)PRIMARY

查询中若包含任何复杂的子部分,最外层查询则被标记为primary

在这里插入图片描述

3)SUBQUERY

在SELECT或WHERE列表中包含了子查询

在这里插入图片描述

4)DEPENDENT SUBQUERY

在SELECT或WHERE列表中包含了子查询,子查询基于外层

在这里插入图片描述

5.5版本会显示,但是5.7变成了SIMPLE。

5)UNCACHEABLE SUBQUREY

与dependent union类似,表示这个subquery的查询要受到外部表查询的影响

在这里插入图片描述

6)UNION

若第二个SELECT出现在UNION之后,则被标记为UNION; 若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED

在这里插入图片描述

7)UNION RESULT

从UNION表获取结果的SELECT

3)table

显示这一行的数据是关于哪张表的

4)partitions

代表分区表中的命中情况,非分区表,该项为null

5)type

type显示的是访问类型,是较为重要的一个指标,结果值从最好到最坏依次是

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

一般来说,得保证查询至少达到range级别,最好能达到ref

〇system

表仅有一行记录,这是const类型的特例,查询起来非常迅速。

在这里插入图片描述

①const

表示通过索引一次就找到了,const用于primary key或者unique索引。

因为只匹配一行数据,所以很快 如将主键置于where列表中,MySQL就能将该查询转换为一个常量

②eq_ref

唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见主键或唯一索引扫描.

高性能MySQL调优实战_第2张图片

③ref

非唯一性索引扫描,返回匹配某个单独值的所有行. 本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而, 它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体

④range

只检索给定范围的行,使用一个索引来选择行。

key 列显示使用了哪个索引 一般就是在你的where语句中出现了between、<、>、in等的查询 这种范围索引扫描比全表扫描要好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。

在这里插入图片描述

⑤index

出现index是sql使用了索引但是没用通过索引进行过滤,一般是使用了覆盖索引或者是利用索引进行了排序分组

Full Index Scan,index与ALL区别为index类型只遍历索引树。

这通常比ALL快,因为索引文件通常比数据文件小。

也就是说虽然all和Index都是读全表,但index是从索引中读取的,而all是从硬盘中读的

在这里插入图片描述

⑥all

Full Table Scan,将遍历全表以找到匹配的行.

在这里插入图片描述

⑦index_merge

在查询过程中需要多个索引组合使用,通常出现在有 or 的关键字的sql中

⑧ref_or_null

对于某个字段既需要关联条件,也需要null值得情况下。查询优化器会选择用ref_or_null连接查询。

⑨index_subquery

利用索引来关联子查询,不再全表扫描。

⑩unique_subquery

该联接类型类似于index_subquery。 子查询中的唯一索引。

6)possible_keys

显示可能应用在这张表中的索引,一个或多个。 查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用.

7)key

实际使用的索引。如果为NULL,则没有使用索引

查询中若使用了覆盖索引,则该索引和查询的select字段重叠

8)key_len

key_len表示索引使用的字节数,根据这个值可以判断索引的使用情况,特别是在组合索引的时候,判断该索引有多少部分被使用到非常重要。值越大越好。

在这里插入图片描述

如何计算

1、先看索引上字段的类型+长度比如 int=4 ; varchar(20) =20 ; char(20) =20

2、如果是varchar或者char这种字符串字段,视字符集要乘不同的值,比如utf-8要乘 3,GBK要乘2

3、varchar这种动态字符串要加2个字节

4、允许为空的字段要加1个字节

key_len的长度计算公式:

varchr(10)变长字段且允许NULL  = 10 \* ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)

varchr(10)变长字段且不允许NULL = 10 \* ( character set:utf8=3,gbk=2,latin1=1)+2(变长字段)

char(10)固定字段且允许NULL    = 10 \* ( character set:utf8=3,gbk=2,latin1=1)+1(NULL)

char(10)固定字段且不允许NULL  = 10 \* ( character set:utf8=3,gbk=2,latin1=1)

9)ref

显示将哪些列或常量与键列中命名的索引进行比较,以从表中选择行。

10)rows

rows列显示MySQL认为它执行查询时必须检查的行数。值越小越好

11)filtered

这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数

12)Extra

包含不适合在其他列中显示但十分重要的额外信息

①Using filesort

order by 没有用上索引。手工排序。

查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度

说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。 MySQL中无法利用索引完成的排序操作称为“文件排序”

②Using temporary

出现Using temporary情况:分组没有用上索引。产生临时表。注意:分组操作是需要先排序后分组的。所以,也会出现Using filesort。

MySQL在对查询结果排序时使用临时表。常见于排序order by 和分组查询group by。

③USING index

如果同时出现using where,表明索引被用来执行索引键值的查找;

如果没有同时出现using where,表明索引用来读取数据而非执行查找动作。

④Using where

表明使用了where过滤

⑤Using join buffer

使用了连接缓存

⑥impossible where

where子句的值总是false,不能用来获取任何元组

⑦select tables optimized away

在没有GROUPBY子句的情况下,基于索引优化MIN/MAX操作或者 对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算, 查询执行计划生成的阶段即完成优化。

3,数据准备-保存100w条数据

1)员工表插入50w条数据,部门表插入1w条数据

CREATE TABLE `dept` (
 `id` INT(11) NOT NULL AUTO_INCREMENT,
 `deptName` VARCHAR(30) DEFAULT NULL,
 `address` VARCHAR(40) DEFAULT NULL,
 `ceo` INT NULL ,
 PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 
CREATE TABLE `emp` (
 `id` INT(11) NOT NULL AUTO_INCREMENT,
 `empno` INT NOT NULL ,
 `name` VARCHAR(20) DEFAULT NULL,
 `age` INT(3) DEFAULT NULL,
 `deptId` INT(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
 #CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

#生成随机字符串
DELIMITER $$
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN    
DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
DECLARE return_str VARCHAR(255) DEFAULT '';
DECLARE i INT DEFAULT 0;
WHILE i < n DO  
SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));  
SET i = i + 1;
END WHILE;
RETURN return_str;
END $$

#假如要删除
DELIMITER ;
drop function rand_string;

#用于随机产生多少到多少的编号
DELIMITER $$
CREATE FUNCTION  rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN   
DECLARE i INT DEFAULT 0;  
SET i = FLOOR(from_num +RAND()*(to_num - from_num+1))   ;
RETURN i;  
END$$ 
 
#假如要删除
DELIMITER ;
drop function rand_num;


#创建往emp表中插入数据的存储过程
DELIMITER $$
CREATE PROCEDURE  insert_emp(  START INT ,  max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
 SET autocommit = 0;    #设置手动提交事务
 REPEAT  #循环
 SET i = i + 1;  #赋值
 INSERT INTO emp (empno, NAME ,age ,deptid ) VALUES ((START+i),rand_string(6),rand_num(30,50),rand_num(1,10000));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT;  #提交事务
END$$

#删除
DELIMITER ;
drop PROCEDURE insert_emp;

#创建往dept表中插入数据的存储过程
DELIMITER $$
CREATE PROCEDURE `insert_dept`(  max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
 SET autocommit = 0;    
 REPEAT  
 SET i = i + 1;  
 INSERT INTO dept ( deptname,address,ceo ) VALUES (rand_string(8),rand_string(10),rand_num(1,500000));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT; 
END$$

#删除
DELIMITER ;
drop PROCEDURE insert_dept;

#执行存储过程,往dept表添加1万条数据
CALL insert_dept(10000); 
#执行存储过程,往emp表添加50万条数据
CALL insert_emp(100000,500000); 

设置参数:log_bin_trust_function_creators

创建函数,假如报错:This function has none of DETERMINISTIC......
# 由于开启过慢查询日志bin-log, 我们就必须为我们的function指定一个参数。
#主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。
#如果使用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设置
#查看mysql是否允许创建函数:
show variables like 'log_bin_trust_function_creators';
#命令开启:允许创建函数设置:
set global log_bin_trust_function_creators=1; 
# 不加global只是当前窗口有效。mysqld重启,上述参数又会消失。
#永久方法:
windows下:my.ini[mysqld]加上:log_bin_trust_function_creators=1  
linux下:/etc/my.cnf下my.cnf[mysqld]加上:log_bin_trust_function_creators=1

2)批量删除某个表上的所有索引

建立好的索引在哪里?

SHOW INDEX FROM t_emp ; 只能查看索引,但不能删除。
information_schema.STATISTICS 存储索引的表(元数据库,统计表),我们可以对表数据进行删除操作。

知识点

1)删除某一个索引

DROP INDEX idx_xxx ON emp

2)查出该表有哪些索引,索引名–>集合

SHOW INDEX FROM t_emp
元数据:meta DATA  描述数据的数据
SELECT index_name  FROM information_schema.STATISTICS WHERE table_name='t_emp' AND table_schema='mydb'
AND index_name <>'PRIMARY' AND seq_in_index = 1

4,单表使用索引及常见索引失效

1)全值匹配

查询语句

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30;
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30 and deptid=4;
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30 and deptid=4 AND emp.name = 'abcd';

建立索引

CREATE INDEX idx_age_deptid_name ON emp(age,deptid,NAME);

2)左前缀匹配

CREATE INDEX idx_age_deptid_name ON emp(age,deptid,NAME);

EXPLAIN SELECT * FROM emp WHERE age=1 AND deptid=1 AND NAME='aaa';
#使用上索引
EXPLAIN SELECT * FROM emp WHERE age=1 AND deptid=1;
#使用上索引
EXPLAIN SELECT * FROM emp WHERE age=1 AND NAME='aaa' AND deptid=1
#只用上了age
EXPLAIN SELECT * FROM emp WHERE deptid=1 AND NAME ='aaa';
#用不上索引

过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。

3)计算、函数导致索引失效

CREATE INDEX idx_name ON emp(NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name  LIKE 'abc%';
#用上了索引
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE LEFT(emp.name,3) = 'abc'; 
#没用上索引

4)范围条件右边的列索引失效

CREATE INDEX idx_age_deptid_name ON emp(age,deptid,NAME);

EXPLAIN 
SELECT 
  SQL_NO_CACHE * 
FROM
  emp 
WHERE emp.name = 'abc'
  AND emp.deptId > 20 
  AND  emp.age = 30 ;

应用开发中范围查询,例如: 金额查询,日期查询往往都是范围查询。应将查询条件放置where语句最后。

5)不等于(!= 或者<>)索引失效

CREATE INDEX idx_name ON emp(NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name <>  'abc' ;

6)is not null无法使用索引,is null可使用索引

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE age IS NULL;
#用到索引  
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE age IS NOT NULL;
#未用到索引

7)like以通配符%开头索引失效

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE NAME LIKE '%aaa';

8)类型转换导致索引失效

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE NAME=123; 

设计实体类属性时,一定要与数据库字段类型相对应,否则会出现类型转换的情况,导致索引失效。

5,关联查询优化

建表

#分类
CREATE TABLE IF NOT EXISTS `class` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
#图书
CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);
 
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
 
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));

1)左连接

EXPLAIN SELECT SQL_NO_CACHE * FROM class LEFT JOIN book ON class.card = book.card;
#结论:type 有All 
# 添加索引优化
ALTER TABLE book ADD INDEX Y ( card); 
#【被驱动表】,可以避免全表扫描 ALL REF
ALTER TABLE class ADD INDEX X (card); 
#【驱动表】,无法避免全表扫描  INDEX REF

2)内连接

换成inner join(MySQL自动选择驱动表)

EXPLAIN 
SELECT 
  SQL_NO_CACHE * 
FROM
  class 
  INNER JOIN book 
    ON class.card = book.card ;

保证被驱动表的join字段被索引

left join 时,选择小表作为驱动表,大表作为被驱动表

inner join 时,mysql会自动将小结果集的表选为驱动表。选择相信mysql优化策略。

子查询尽量不要放在被驱动表,衍生表建不了索引。

能够直接多表关联的尽量直接关联,不用子查询。

两张表的连接查询,比方说 left join right、inner join 等,他们的连表方式是什么?

连表查询一共三种算法:nlj bnl bka 算法 。

right join 底层,会给你转化为left join。

6,子查询优化

#①不推荐
SELECT 
  * 
FROM
  t_emp a 
WHERE a.id NOT IN 
  (SELECT 
    b.ceo 
  FROM
    t_dept b 
  WHERE b.ceo IS NOT NULL) ;

#②推荐
SELECT 
  a.* 
FROM
  t_emp a 
  LEFT JOIN t_dept b 
    ON a.id = b.ceo 
WHERE b.id IS NULL ;

尽量不要使用not in 或者 not exists,会使索引失效。

MySQL自动做出的子查询优化,物化子查询,转为半连接

物化子查询:把子查询的结果查出来后,建立一个临时表,“物化”->变成一张内存临时表

半连接:把子查询转化为类似连接查询的方式,但又不是真正的连接查询,所以叫 半 连接优化

7,排序,分组优化

1)无过滤 不索引

EXPLAIN SELECT SQL_NO_CACHE * FROM emp ORDER BY age,deptid; 
#没用上索引,Using filesort 
EXPLAIN SELECT SQL_NO_CACHE * FROM emp ORDER BY age,deptid LIMIT 10; 
#使用上索引 null

order后面的字段想要使用索引,必须要有过滤条件,limit也行。

2)顺序错,必排序

CREATE INDEX idx_age_deptid_empno ON emp (age,deptid,empno);

EXPLAIN SELECT * FROM emp WHERE age=45 ORDER BY deptid;
# Using index condition
EXPLAIN SELECT * FROM emp WHERE age=45 ORDER BY deptid,NAME; 
# Using index condition
EXPLAIN SELECT * FROM emp WHERE age=45 ORDER BY deptid,empno;
# Using index condition; Using filesort
EXPLAIN SELECT * FROM emp WHERE age=45 ORDER BY NAME,deptid;
# Using index condition; Using filesort
EXPLAIN SELECT * FROM emp WHERE deptid=45 ORDER BY age;
# Using where; Using filesort

在SQL语句中的顺序一定要和定义索引中的字段顺序完全一致。

3)方向反,必排序

EXPLAIN SELECT * FROM emp WHERE age=45 ORDER BY deptid DESC, NAME DESC ;
#Using where
EXPLAIN SELECT * FROM emp WHERE age=45 ORDER BY deptid ASC, NAME DESC ;
#Using index condition; Using filesort

ORDER BY子句,尽量使用Index方式排序,避免使用FileSort方式排序

要么全升序、要么全降序。有升有降无法使用索引。

4)索引的选择

查询 年龄为30岁的,且员工编号小于101000的用户,按用户名称排序

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE age =30 AND empno <101000 ORDER BY NAME ; 

在这里插入图片描述

很显然,type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。

尽量让where的过滤条件和排序使用上索引

CREATE INDEX idx_age_empno_name ON emp (age,empno,NAME);

在这里插入图片描述

using filesort 依然存在,所以name 并没有用到索引。

因为empno是一个范围过滤,所以索引后面的字段不会再使用索引了。

先删掉这个索引

DROP INDEX idx_age_empno_name ON emp

为了去掉filesort我们可以把索引建成:

CREATE INDEX idx_age_name ON emp(age,NAME);

在这里插入图片描述

也就是说empno 和name这个两个字段我只能****二选其一****。

这样我们优化掉了 using filesort。性能提升了。

如果选择那个范围过滤,而放弃排序上的索引呢?

DROP INDEX idx_age_name ON emp;
CREATE INDEX idx_age_empno ON emp(age,empno);

在这里插入图片描述

出现了filesort,而且type还是range光看字面其实并不美好。

但是相比rows,却是提升了千倍效率。

两个索引同时存在,mysql自动选择最优的方案:(对于这个例子,mysql选择idx_age_empno),但是,随着数据量的变化,选择的索引也会随之变化的。

原因

所有的排序都是在条件过滤之后才执行的,所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。

结论

当【范围条件】和【group by 或者 order by】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。

扫描行数的多少,就是explain里的rows,可以说明一个需要扫描的行数多,一个扫描行数少,扫描行数多,代表成本高,扫描行数少代表成本少。优化器最终是对比成本值的大小来选取索引的。准确的说,是MySQL基于成本,优化器是在server层。

有时候优化器会选择错索引为什么?

主要是出在优化器预估行数上,这个涉及到了一条sql的执行流程,语法分析,词法分析之后,进入优化阶段,由优化器进行优化,在优化阶段,会尽可能的生成全部的执行计划,然后对比一下哪一个成本值最低,就选它,所以优化器有一个选择索引,选择表的连接顺序的过程,索引不同,成本不同,读表顺序不同,成本不同,索引的选取,需要存储引擎提供统计信息,innodb中,统计信息是随机采样,随机选取8个索引页,取平均值,当做该索引的全部情况,也就是部分代表整体,也就是最终导致rows那里是个预估值,而不是准确的。所以有时候MySQL选错了索引,有一定概率,是由于这个随机采样造成的。而随机采样的不准确,是由于索引页的分裂,导致有些页内数据较少。

解决方案就是执行一下alter table +表名 就可以使统计信息稍微准确点,他会重新构建索引,使索引页保持紧凑,这个就是B+树的分裂。

5)双路排序和单路排序

如果不在索引列上,filesort有两种算法: mysql就要启动双路排序和单路排序。

双路排序

MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和order by列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出

从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。

取一批数据,要对磁盘进行两次扫描,众所周知,I\O是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。

单路排序

从磁盘读取查询需要的所有列,按照order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。

结论

由于单路是后出的,总体而言好过双路。

但是用单路有问题:

在sort_buffer中,单路比多路要多占用很多空间,因为单路是把所有字段都取出, 所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排……从而多次I/O。

单路本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。

优化策略

增大sort_buffer_size参数的设置

增大max_length_for_sort_data参数的设置

减少select 后面的查询的字段。

提高order by的速度

1.Order by时select * 是一个大忌。只Query需要的字段, 这点非常重要。

当Query的字段大小总和小于max_length_for_sort_data 而且排序字段不是 TEXT|BLOB 类型时,会用改进后的算法——单路排序, 否则用老算法——多路排序。

两种算法的数据都有可能超出sort_buffer的容量,超出之后,会创建tmp文件进行合并排序,导致多次I/O,但是用单路排序算法的风险会更大一些,所以要提高sort_buffer_size。

2.尝试提高 sort_buffer_size

不管用哪种算法,提高这个参数都会提高效率,当然,要根据系统的能力去提高,因为这个参数是针对每个进程(connection)的 1M-8M之间调整。 MySQL5.7,InnoDB存储引擎默认值是1048576字节,1MB。

SHOW VARIABLES LIKE '%sort_buffer_size%';

3.尝试提高 max_length_for_sort_data

提高这个参数, 会增加用改进算法的概率。

SHOW VARIABLES LIKE '%max_length_for_sort_data%'; #默认1024字节

但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是高的磁盘I/O活动和低的处理器使用率。如果需要返回的列的总长度大于max_length_for_sort_data,使用双路算法,否则使用单路算法。1024-8192字节之间调整

6)GROUP BY关键字优化

group by 使用索引的原则几乎跟order by一致 ,唯一区别:

group by 先排序再分组,遵照索引建的最佳左前缀法则

当无法使用索引列,增大max_length_for_sort_data和sort_buffer_size参数的设置

where高于having,能写在where限定的条件就不要写在having中了

group by没有过滤条件,也可以用上索引。Order By 必须有过滤条件才能使用上索引。

8,覆盖索引优化

*禁止使用select ,禁止查询与业务无关字段,尽量使用覆盖索引,防止回表。

回表
非主键索引,我们先通过索引找到主键索引的键值,再通过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。

select * from user_innodb where name = '青山';

高性能MySQL调优实战_第3张图片

在辅助索引里面,不管是单列索引还是联合索引,如果 select 的数据列只用从索引中就能够取得,不必从数据区中读取,这时候使用的索引就叫做覆盖索引,这样就避免了回表。

比方说没有主键,有索引 index(a,b,c)
select * from table where a=? and b=? and c=?; 可以走索引,但需要回表,因为不仅需要返回
a b c还要返回其他的列
select a,b,c,d from table where a=? and b=? and c=?; 可以走索引,但需要回表,因为不仅
需要返回a b c还要返回其他的列
select a,b,c from table where a=? and b=? and c=?; 可以走索引,不需要回表,因为索引
index(a,b,c)包含了所有需要返回的列
select a,c from table where a=? and b=? and c=?; 可以走索引,不需要回表,因为索引
index(a,b,c)包含了所有需要返回的列
select a,c from table where a=? and b=? 呢?可以走索引,不需要回表

覆盖索引减少了 IO 次数,减少了数据的访问量,可以大大地提升查询效率。

四,追踪优化器

执行计划往往只给我们带来了最基础的分析信息,比如是否有使用索引,还有一些其他供我们分析的信息,比如使用了临时表、排序等等。我们能从这些信息里面找一些优化点,这样就足够了吗?

很多问题并不能非常直观地从执行计划中看出来更多的信息,这个时候,我们可以开启OPTIMIZER_TRACE,基于OPTIMIZER_TRACE捕获的信息,去做更细致的追踪分析。

1,OPTIMIZER_TRACE是什么

它是一个跟踪功能,跟踪执行的语句的解析优化执行的过程,并将跟踪到的信息记录到INFORMATION_SCHEMA.OPTIMIZER_TRACE表中。

可以通过optimizer_trace系统变量启停跟踪功能,MySQL从5.6开始提供了相关的功能,但是MySQL默认是关闭它的,我们在需要使用的时候才会手动去开启。

optimizer_trace可以是会话或者是全局开启,但是每个会话都只能跟踪它自己执行的语句,表中默认只记录最后一个查询的跟踪结果(表中记录的跟踪结果数可以通过optimizer_trace的参数设置)

可跟踪语句对象:

  • SELECT/INSERT/REPLACE/UPDATE/DELETE
  • EXPLAIN
  • SET
  • DO
  • DECLARE/CASE/IF/RETURN
  • CALL

2,变量解析

show variables like '%optimizer_trace%';
optimizer_trace enabled=off,one_line=off
optimizer_trace_features greedy_search=on,range_optimizer=on,dynamic_range=on,repeated_subselect=on
optimizer_trace_limit 1
optimizer_trace_max_mem_size 16384
optimizer_trace_offset -1

optimizer_trace

  • enabled:启用/禁用optimizer_trace功能
  • one_line:决定了跟踪信息的存储方式,为on表示使用单行存储,否则以JSON树的标准展示形式存储。单行存储中跟踪结果中没有空格,造成可读性极差,但对于JSON解析器来说是可以解析的,将该参数打开唯一的优势就是节省空间,一般不建议开启

optimizer_trace_features:该变量中存储了跟踪信息中可控的打印项,可以通过调整该变量,控制在INFORMATION_SCHEMA.OPTIMIZER_TRACE表中的trace列需要打印的JSON项和不需要打印的JSON项。默认打开该参数下的所有项。

optimizer_trace_max_mem_size :optimizer_trace内存的大小,如果跟踪信息超过这个大小,信息将会被截断

optimizer_trace_limit & optimizer_trace_offset

  • 这两个参数神似于SELECT语句中的“LIMIT offset, row_count”,optimizer_trace_limit 约束的是跟踪信息存储的个数,optimizer_trace_offset 则是约束偏移量。和 LIMIT 一样,optimizer_trace_offset 从0开始计算(最老的一个查询记录的偏移量为0)
  • optimizer_trace_offset 的正负值,不需要太过于去纠结,如下表所示,其实offset 0 = offset -5 ,它们是一个等价的关系,仅仅是表述方式不同。这样的表述方式和python中的切片的表述是一致的,了解python的童鞋们都知道,切片的时候经常用到-1取列表中最后一个数值或者是反向取值。
query optimizer_trace_offset正数值 optimizer_trace_offset负数值
select1 0 -5
select2 1 -4
select3 2 -3
select4 3 -2
select5 4 -1

结合下MySQL给出的默认值进行解读,MySQL的默认值: optimizer_trace_limit = 1,optimizer_trace_offset = -1。optimizer_trace_limit = 1表示只存储一个查询信息,optimizer_trace_offset = -1 则是指向最近的一个查询,即,在INFORMATION_SCHEMA.OPTIMIZER_TRACE表中只存储最近最后执行的一行结果数据。2

3,如何跟踪分析

1、打开optimizer_trace参数

2、执行要分析的查询

3、查看INFORMATION_SCHEMA.OPTIMIZER_TRACE表中跟踪结果

4、循环2、3步骤

5、当不再需要分析的时候,关闭参数

# Turn tracing on (it's off by default):  
SET optimizer_trace="enabled=on"; 
SELECT ...; # your query here 
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE; 
# possibly more queries... 
# When done with tracing, disable it: 
SET optimizer_trace="enabled=off";

4,OPTIMIZER_TRACE表

1)表结构

CREATE TEMPORARY TABLE `OPTIMIZER_TRACE` (
  `QUERY` longtext NOT NULL,
  `TRACE` longtext NOT NULL,
  `MISSING_BYTES_BEYOND_MAX_MEM_SIZE` int(20) NOT NULL DEFAULT '0',
  `INSUFFICIENT_PRIVILEGES` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2)字段含义

QUERY:跟踪的SQL语句

TRACE:跟踪信息(JSON格式)

MISSING_BYTES_BEYOND_MAX_MEM_SIZE:跟踪信息过长时,被截断的跟踪信息的字节数。

INSUFFICIENT_PRIVILEGES:执行跟踪语句的用户是否有查看对象的权限。当不具有权限时,该列信息为1且TRACE字段为空。一般出现在调用带有SQL SECURITY DEFINER的视图或者是存储过程的情况下。

5,跟踪结果解析

案例数据来自官网

SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=ON; # be readable
SET OPTIMIZER_TRACE_MAX_MEM_SIZE=1000000; # avoid small default
CREATE TABLE t1 (
pk INT, col_int_key INT,
col_varchar_key VARCHAR(1), col_varchar_nokey VARCHAR(1)
);
INSERT INTO t1 VALUES
(10,7,'v','v'),(11,0,'s','s'),(12,9,'l','l'),(13,3,'y','y'),(14,4,'c','c'),
(15,2,'i','i'),(16,5,'h','h'),(17,3,'q','q'),(18,1,'a','a'),(19,3,'v','v'),
(20,6,'u','u'),(21,7,'s','s'),(22,5,'y','y'),(23,1,'z','z'),(24,204,'h','h'),
(25,224,'p','p'),(26,9,'e','e'),(27,5,'i','i'),(28,0,'y','y'),(29,3,'w','w');
CREATE TABLE t2 (
pk INT, col_int_key INT,
col_varchar_key VARCHAR(1), col_varchar_nokey VARCHAR(1),
PRIMARY KEY (pk)
);
INSERT INTO t2 VALUES
(1,4,'b','b'),(2,8,'y','y'),(3,0,'p','p'),(4,0,'f','f'),(5,0,'p','p'),
(6,7,'d','d'),(7,7,'f','f'),(8,5,'j','j'),(9,3,'e','e'),(10,188,'u','u'),
(11,4,'v','v'),(12,9,'u','u'),(13,6,'i','i'),(14,1,'x','x'),(15,5,'l','l'),
(16,6,'q','q'),(17,2,'n','n'),(18,4,'r','r'),(19,231,'c','c'),(20,4,'h','h'),
(21,3,'k','k'),(22,3,'t','t'),(23,7,'t','t'),(24,6,'k','k'),(25,7,'g','g'),
(26,9,'z','z'),(27,4,'n','n'),(28,4,'j','j'),(29,2,'l','l'),(30,1,'d','d'),
(31,2,'t','t'),(32,194,'y','y'),(33,2,'i','i'),(34,3,'j','j'),(35,8,'r','r'),
(36,4,'b','b'),(37,9,'o','o'),(38,4,'k','k'),(39,5,'a','a'),(40,5,'f','f'),
(41,9,'t','t'),(42,3,'c','c'),(43,8,'c','c'),(44,0,'r','r'),(45,98,'k','k'),
(46,3,'l','l'),(47,1,'o','o'),(48,0,'t','t'),(49,189,'v','v'),(50,8,'x','x'),
(51,3,'j','j'),(52,3,'x','x'),(53,9,'k','k'),(54,6,'o','o'),(55,8,'z','z'),
(56,3,'n','n'),(57,9,'c','c'),(58,5,'d','d'),(59,9,'s','s'),(60,2,'j','j'),
(61,2,'w','w'),(62,5,'f','f'),(63,8,'p','p'),(64,6,'o','o'),(65,9,'f','f'),
(66,0,'x','x'),(67,3,'q','q'),(68,6,'g','g'),(69,5,'x','x'),(70,8,'p','p'),
(71,2,'q','q'),(72,120,'q','q'),(73,25,'v','v'),(74,1,'g','g'),(75,3,'l','l'),
(76,1,'w','w'),(77,3,'h','h'),(78,153,'c','c'),(79,5,'o','o'),(80,9,'o','o'),
(81,1,'v','v'),(82,8,'y','y'),(83,7,'d','d'),(84,6,'p','p'),(85,2,'z','z'),
(86,4,'t','t'),(87,7,'b','b'),(88,3,'y','y'),(89,8,'k','k'),(90,4,'c','c'),
(91,6,'z','z'),(92,1,'t','t'),(93,7,'o','o'),(94,1,'u','u'),(95,0,'t','t'),
(96,2,'k','k'),(97,7,'u','u'),(98,2,'b','b'),(99,1,'m','m'),(100,5,'o','o');

SELECT 
  SUM(alias2.col_varchar_nokey),
  alias2.pk AS field2 
FROM
  t1 AS alias1 
  STRAIGHT_JOIN t2 AS alias2 
    ON alias2.pk = alias1.col_int_key 
WHERE alias1.pk 
GROUP BY field2 
ORDER BY alias1.col_int_key,
  alias2.pk ;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
query trace
SELECT
SUM(t1.col_varchar_nokey),
t2.pk AS field2
FROM
t1
STRAIGHT_JOIN t2
ON t2.pk = t1.col_int_key
WHERE t1.pk
GROUP BY field2
ORDER BY t1.col_int_key,
t2.pk
{
“steps”: [
] /* steps */
}
0 0
EXPLAIN SELECT 
  SUM(t1.col_varchar_nokey),
  t2.pk AS field2 
FROM
  t1 
  STRAIGHT_JOIN t2 
    ON t2.pk = t1.col_int_key 
WHERE t1.pk 
GROUP BY field2 
ORDER BY t1.col_int_key,
  t2.pk ;

在这里插入图片描述

6,TRACE

整个OPTIMIZER_TRACE的重点就是TRACE的JSON树。TRACE中的JSON树大部分都又臭又长,个人更建议使用带有收缩代码格式的编辑器去围观这棵树,能更清晰地理顺这棵树,先来看看TRACE的大框架。在TRACE的JSON中有三个步骤构成: join_preparation(准备阶段)、join_optimization(优化阶段)、join_execution(执行阶段)。

高性能MySQL调优实战_第4张图片

该树是在基础层级上的展开,每个阶段下面都可以看到有“select# : N”,它表示当前的结构体是在跟踪分析第几个SELECT,因为语句仅仅只有一个SELECT语句,所以示例中的JSON树只有“select# : 1”。如果有多个SELECT就会出现“select# : 2”的情况。

高性能MySQL调优实战_第5张图片

1)join_preparation(准备阶段)

高性能MySQL调优实战_第6张图片

看看在这个示例中准备阶段时都做了什么事情。

expanded_query:这里的语句进行了格式化,补充了原有语句中隐式的库、表、列名。

transformations_to_nested_joins:这里进行了转换,将JOIN的ON条件句转换成了WHERE条件句(JOIN_condition_to_WHERE),并输出了新的expanded_query,新的expanded_query中将原有的ON条件句改写成了WHERE,这就是JOIN_condition_to_WHERE操作导致的。

2)join_optimization(优化阶段)

优化阶段实际上是我们分析OPTIMIZER_TRACE的一个重点阶段,它的步骤非常多也相对复杂,先粗略看看steps下的每个步骤,大致都在做些什么

steps[0].condition_processing :条件句处理。该步骤对WHERE条件句进行优化处理。

steps[1].substitute_generated_columns :替换虚拟生成列。

steps[2].table_dependencies :梳理表之间的依赖关系。

steps[3].ref_optimizer_key_uses :如果优化器认为查询可以使用ref的话,在这里列出可以使用的索引

steps[4].rows_estimation :估算表行数和扫描的代价。如果查询中存在range扫描的话,对range扫描进行计划分析及代价估算。

steps[5].considered_execution_plans :对比各可行计划的代价,选择相对最优的执行计划

steps[6].attaching_conditions_to_tables :添加附加条件,使得条件尽可能筛选单表数据。

steps[7&8].clause_processing :对DISTINCT、GROUP BY、ORDER BY等语句进行优化(每一种语句会产生一个clause_processing 结构体,示例语句中既有排序又有分组,所以会有7、8两个步骤)

steps[9].refine_plan : 优化后的执行计划

3)join_execution (执行阶段)

join_execution是语句的执行阶段,我们前面提到trace有三大部分构成,其中join_preparation、join_optimization可以分析explain的过程,但是在join_execution阶段,如果分析的语句是explain的话,该阶段的代码是空结构体,只有在真正执行语句之后,该阶段的代码才会有具体的步骤过程。

在执行阶段中,本示例主要经历了图示中的这些步骤:creating_tmp_table(创建临时表)、filesort_information(文件排序信息)、filesort_priority_queue_optimization(文件排序-优先队列优化)、filesort_execution(文件排序执行)、filesort_summary(文件排序汇总信息)。

0、creating_tmp_table

该阶段根据语句创建临时表,虽然在图示中没有展示,但如果临时表的大小超过了设置的max_heap_table_size或者是tmp_table_size参数的话,会在此步骤后引发多一个步骤:converting_tmp_table_to_ondisk(将临时表落地到磁盘中)

tmp_table_info:临时表信息

  • table:临时表的名称
  • row_length:临时表的单行长度
  • key_length:临时表索引长度
  • unique_constraint:是否有使用唯一约束
  • location:表存储位置,比如内存表memory (heap),或者是转换到磁盘的物理表disk (InnoDB)
  • row_limit_estimate:该临时表中能存储的最大行数

1、filesort

join_execution 阶段在创建临时表之后如果临时表不需要转换为磁盘表的话,即开始对文件排序进行处理
filesort_information:文件排序信息(如果有多列,filesort_information下会有多个结构体)

  • 排序方式:排序列是升序还是降序
  • table:排序的表对象名
  • field:排序列
    filesort_priority_queue_optimization:优先队列优化排序,一般在使用limit子句的时候会使用优先队列
  • usable:是否有使用
  • cause:没有使用的原因
    filesort_execution:执行文件排序
    filesort_summary:文件排序汇总信息
  • rows:预计扫描行数
  • examined_rows:参与排序的行数
  • number_of_tmp_files:使用临时文件的个数,这个值为0代表全部使用sort_buffer内存排序,否则表示使用了磁盘文件排序
  • sort_buffer_size:使用的sort_buffer的大小
  • sort_mode:排序方式

在整个optimizer_trace中我们重点其实就是在跟踪记录TRACE的JSON树,我们通过这棵树中的内容可以具体去分析优化器究竟做了什么事情,进行了哪些选择,是基于什么原因做的选择,选择的结果及依据。这一系列都可以辅助验证我们的一些观点及优化,更好的帮助我们对我们的数据库的实例进行调整。3

五,分库分表

从维度来说分成两种,一种是垂直,一种是水平。

垂直切分:基于表或字段划分,表结构不同。我们有单库的分表,也有多库的分库。

水平切分:基于数据划分,表结构相同,数据不同,也有同库的水平切分和多库的切分。

1,垂直切分

垂直分表有两种,一种是单库的,一种是多库的。

1)单库垂直分表

单库分表,比如:商户信息表,拆分成基本信息表,联系方式表,结算信息表,附件表等等。

可以考虑根据冷热点字段拆分,是否经常发生修改操作拆分,根据字段功能拆分。

2)多库垂直分表

多库垂直分表就是把原来存储在一个库的不同的表,拆分到不同的数据库。

比如电商平台的消费系统:一开始,商品表,商品详情表,订单表,用户表,支付记录表,库存表,风控表都在一个库里面,随着数据的增长和业务的扩张,可以考虑将商品和商品详情表单独放到一个库,订单表单独放到一个库,支付记录单独放到一个库,库存表单独放到一个库,风控表单独放到一个库。

当我们对原来的一张表做了分库的处理,如果某些业务系统的数据还是有一个非常快的增长速度,比如说订单数据库的订单表,数据量达到了几个亿,这个时候硬件限制导致的性能问题还是会出现,所以从这个角度来说垂直切分并没有从根本上解决单库单表数据量过大的问题。在这个时候,我们还需要对我们的数据做一个水平的切分。

2,水平拆分

当我们的客户表数量已经到达数千万甚至上亿的时候,单表的存储容量和查询效率都会出现问题,我们需要进一步对单张表的数据进行水平切分。水平切分的每个数据库的表结构都是一样的,只是存储的数据不一样,比如每个库存储 1000 万的数据。

水平切分也可以分成两种,一种是单库的,一种是多库的。

1)单库水平分表

银行的交易流水表,所有进出的交易都需要登记这张表,因为绝大部分时候客户都是查询当天的交易和一个月以内的交易数据,所以我们根据使用频率把这张表拆分成三张表:

当天表:只存储当天的数据。

当月表:在夜间运行一个定时任务,前一天的数据,全部迁移到当月表。用的是 insert into select,然后 delete。

历史表:同样是通过定时任务,把登记时间超过 30 天的数据,迁移到 history历史表(历史表的数据非常大,我们按照月度,每个月建立分区)。

跟分区一样,这种方式虽然可以一定程度解决单表查询性能的问题,但是并不能解决单机存储瓶颈的问题。

2)多库水平分表

比如客户表,我们拆分到多个库存储,表结构是完全一样的。

高性能MySQL调优实战_第7张图片

一般我们说的分库分表都是跨库的分表。

3,分库分表带来的问题

1)跨库关联查询

比如查询合同信息的时候要关联客户数据,由于是合同数据和客户数据是在不同的数据库,那么我们肯定不能直接使用 join 的这种方式去做关联查询。

解决方案

①字段冗余

比如我们查询合同库的合同表的时候需要关联客户库的客户表,我们可以直接把一些经常关联查询的客户字段放到合同表,通过这种方式避免跨库关联查询的问题。

②数据同步

比如商户系统要查询产品系统的产品表,我们干脆在商户系统创建一张产品表,通过 ETL 或者其他方式定时同步产品数据。

③全局表(广播表)

比如行名行号信息被很多业务系统用到,如果我们放在核心系统,每个系统都要去关联查询,这个时候我们可以在所有的数据库都存储相同的基础数据。

④ER表

我们有些表的数据是存在逻辑的主外键关系的,比如订单表 order_info,存的是汇总的商品数,商品金额;订单明细表 order_detail,是每个商品的价格,个数等等。或者叫做从属关系,父表和子表的关系。他们之间会经常有关联查询的操作,如果父表的数据和子表的数据分别存储在不同的数据库,跨库关联查询也比较麻烦。所以我们能不能把父表的数据和从属于父表的数据落到一个节点上呢?

比如 order_id=1001 的数据在 node1,它所有的明细数据也放到 node1;order_id=1002 的数据在 node2,它所有的明细数据都放到 node2,这样在关联查询的时候依然是在一个数据库。

上面的思路都是通过合理的数据分布避免跨库关联查询,实际上在我们的业务中,也是尽量不要用跨库关联查询,如果出现了这种情况,就要分析一下业务或者数据拆分是不是合理。如果还是出现了需要跨库关联的情况,那我们就只能用最后一种办法。

⑤系统层组装

在不同的数据库节点把符合条件数据的数据查询出来,然后重新组装,返回给客户端。

2)分布式事务

具体分布式事务会单独写一篇文章

3)排序,翻页,函数计算问题

跨节点多库进行查询时,会出现 limit 分页,order by 排序的问题。比如有两个节点,节点 1 存的是奇数 id=1,3,5,7,9……;节点 2 存的是偶数 id=2,4,6,8,10……

执行 select * from user_info order by id limit 0,10

需要在两个节点上各取出 10 条,然后合并数据,重新排序。

max、min、sum、count 之类的函数在进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。

4)全局主键避重

MySQL 的数据库里面字段有一个自增的属性,Oracle 也有 Sequence 序列。如果是一个数据库,那么可以保证 ID 是不重复的,但是水平分表以后,每个表都按照自己的规律自增,肯定会出现 ID 重复的问题,这个时候我们就不能用本地自增的方式了。

解决方案

①UUID

UUID 标准形式包含 32 个 16 进制数字,分为 5 段,形式为 8-4-4-4-12 的 36 个字符,例如:c4e7956c-03e7-472c-8909-d733803e79a9。

UUID 是主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于 UUID 非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在 InnoDB 中,UUID 的无序性会引起数据位置频繁变动,导致分页。

②数据库

把序号维护在数据库的一张表中。这张表记录了全局主键的类型、位数、起始值,当前值。当其他应用需要获得全局 ID 时,先 for update 锁行,取到值+1 后并且更新后返回。并发性比较差。

③redis

基于 Redis 的 INT 自增的特性,使用批量的方式降低数据库的写压力,每次获取一段区间的 ID 号段,用完之后再去数据库获取,可以大大减轻数据库的压力。

④雪花算法

优点:毫秒数在高位,生成的 ID 整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高,理论上 QPS 约为 409.6w/s(1000*2^12),并且整个分布式系统内不会产生 ID 碰撞;可根据自身业务灵活分配 bit 位。

不足就在于:强依赖机器时钟,如果时钟回拨,则可能导致生成 ID 重复。

4,多数据源/读写数据源的解决方案

分析一下 SQL 执行经过的流程:

DAO——Mapper(ORM)——JDBC——代理——数据库服务

1)客户端DAO 层

在我们连接到某一个数据源之前,我们先根据配置的分片规则,判断需要连接到哪些节点,再建立连接。

Spring 中提供了一个抽象类 AbstractRoutingDataSource,可以实现数据源的动态切换。

1)aplication.properties 定义多个数据源
2)创建@TargetDataSource 注解
3)创建 DynamicDataSource 继承 AbstractRoutingDataSource
4)多数据源配置类 DynamicDataSourceConfig
5)创建切面类 DataSourceAspect,对添加了@TargetDataSource 注解的
类进行拦截设置数据源。
6)在 启 动 类 上 自 动 装 配 数 据 源 配 置
@Import({DynamicDataSourceConfig.class})
7)在 实 现 类 上 加 上 注 解 , 如 @TargetDataSource(name =
DataSourceNames.SECOND),调用

在 DAO 层实现的优势:不需要依赖 ORM 框架,即使替换了 ORM 框架也不受影响。实现简单(不需要解析 SQL 和路由规则),可以灵活地定制。

缺点:不能复用,不能跨语言。

2)ORM框架层

比如我们用 MyBatis 连接数据库,也可以指定数据源。我们可以基于 MyBatis 插件的拦截机制(拦截 query 和 update 方法),实现数据源的选择。

3)驱动层

不管是MyBatis还是Hibernate,还是Spring的JdbcTemplate,本质上都是对JDBC的封装,所以第三层就是驱动层。比如 Sharding-JDBC,就是对 JDBC 的对象进行了封装。JDBC 的核心对象:

DataSource:数据源

Connection:数据库连接

Statement:语句对象

ResultSet:结果集

那我们只要对这几个对象进行封装或者拦截或者代理,就可以实现分片的操作。

4)代理层

前面三种都是在客户端实现的,也就是说不同的项目都要做同样的改动,不同的编程语言也有不同的实现,所以我们能不能把这种选择数据源和实现路由的逻辑提取出来,做成一个公共的服务给所有的客户端使用呢?

这个就是第四层,代理层。比如 Mycat 和 Sharding-Proxy,都是属于这一层。

5)数据库服务

某些特定的数据库或者数据库的特定版本可以实现这个功能。

六,主从复制

1.基本原理

高性能MySQL调优实战_第8张图片

MySQL复制过程分成三步:
1 master将改变记录到二进制日志(binary log)。这些记录过程叫做二进制日志事件,binary log events;
2 slave将master的binary log events拷贝到它的中继日志(relay log);
3 slave重做中继日志中的事件,将改变应用到自己的数据库中。 MySQL复制是异步的且串行化的

slave会从master读取binlog来进行数据同步

2.与redis主从复制的差异

1.redis主从复制是将主机的所有数据都拷贝给从机,并且是近乎实时的。

2.mysql主从复制不会将建立连接以前的数据发送给从机,并且是异步,且串行化的。

3.复制的基本原则

每个slave只有一个master

每个slave只能有一个唯一的服务器ID

每个master可以有多个salve

4.复制的最大问题

延时

全同步可以避免,但性能会极差,正常情况下半同步,且容忍一部分数据不一致。如果不容忍数据不一致,只有强制读主。

5.一主一从常见配置

1)mysql版本一致且后台以服务运行

2)主从都配置在[mysqld]节点下,都是小写

3)主机修改my.ini配置文件

1)主服务器唯一ID server-id=1

2)启用二进制日志

log-bin=自己本地的路径/data/mysqlbin

log-bin=D:/devSoft/MySQLServer5.5/data/mysqlbin

3)设置不要复制的数据库

binlog-ignore-db=mysql

4)设置需要复制的数据库

binlog-do-db=需要复制德主数据库名字

5)设置logbin格式

binlog_format=STATEMENT(默认)

七,系统及硬件层面调优

1,选择合适的CPU

数据库分为两大类,在线事务处理和在线分析处理。

InnoDB储存引擎一般应用于OLTP的数据库应用,这种应用的特点如下:

1.用户操作的并发量大

2,事务处理时间一般比较短

3,查询的语句较为简单,一般都走索引

4,复杂查询比较少

在当前的MySQL数据库版本中,一条SQL语句只能在一个CPU工作,并不支持多CPU。若cpu支持多核,innodb版本应该选择1.1或者更高。另外如果是多核cpu,可以通过修改参数innodb_read_io_threads和innodb_write_io_threads来增大IO的线程,这样也可以更充分的利用cpu的多核性能。

2,内存的重要性

内存大小直接反映数据库的性能。

Innodb存储引擎既缓存数据,又缓存索引,并且将它们缓存于一个很大的缓冲池中,即InnoDB Buffer Pool。因此,内存的大小直接影像数据库的性能。

3,磁盘对数据库性能的影响

4,合理设置RAID类型

5,操作系统的选择

6,文件系统的选择


  1. 关于慢查询的参数slow_query_log_file,它指定慢查询日志文件的存放路径,如果不设置,系统默认文件:[host_name]-slow.log ↩︎

  2. 修改optimizer_trace参数后INFORMATION_SCHEMA.OPTIMIZER_TRACE表会被清空 ↩︎

  3. 此部分参考文章:http://blog.itpub.net/28218939/viewspace-2658978/ ↩︎

你可能感兴趣的:(MySQL,数据库)