- 数据库调优中,目标就是
响应时间更快,吞吐量更大。利用宏观的监控工具和微观的日志分析可以帮我们快速找到调优的思路和方式。
数据库服务器的优化步骤
- 整个流程划分为观察(Show status)和行动(Action)两部分。观察就需要使用相应的分析工具,行动就是对应分析可以采取的行动。

SQL调优的3个步骤:慢查询、EXPLAIN、SHOW PROFILING
数据库调优的效果和成本
查看系统性能参数
在MySQL中,可以使用
SHOW STATUS语句查询一些MySQL数据库服务器的 性能参数 、 执行频率 。查询语句如下:
SHOW [GLOBAL|SESSION] STATUS LIKE '参数';参数:
• Connections:连接MySQL服务器的次数。
• Uptime:MySQL服务器的上线时间。
• Slow_queries:慢查询的次数。
• Innodb_rows_read:Select查询返回的行数
• Innodb_rows_inserted:执行INSERT操作插入的行数
• Innodb_rows_updated:执行UPDATE操作更新的行数
• Innodb_rows_deleted:执行DELETE操作删除的行数
• Com_select:查询操作的次数。
• Com_insert:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次。
• Com_update:更新操作的次数。
• Com_delete:删除操作的次数。
统计SQL查询成本
一条SQL在交给存储引擎执行之前,需要生成执行计划,如果有多条执行计划,MySQL会计算每个执行计划所需要的成本,从中选择成本最小的一个作为最终执行的执行计划。
如果我们想要查看某条SQL语句的查询成本,可以在执行完这条SQL语句之后,通过查看当前会话中的
last_query_cost变量值得到当前查询的成本。它是io_cost和cpu_cost的开销总和通常作为评价
一个查询的执行效率的指标。可以对应到SQL语句需要读取的页的数量。如下两条SQL语句的执行,对应的查询成本分别为20.29和40.32
既然是io_cost和cpu_cost,成本应该大多在io上。肯定和读取页的数量正相关。
如果页在数据库缓冲池中,读取的效率比从磁盘读取更高。成本也就更低
如果页的读取是顺序读取而非随机读取,那么效率也会提升。成本也就更低
慢查询日志
- 用于记录
响应时间超过阈值的SQL语句,也即具体运行时间超过long_query_time的SQL,会被记录到慢查询日志中。 - 默认值是10s,不包含10s
- 主要作用是帮助我们发现执行时间特别长的SQL查询,并且针对性地进行优化,从而提升系统的整体效率。
- 默认情况下,此功能是关闭状态。需要手动开启,只有在调优的时候需要开启,一般运行时候不开启,多少对性能来说会有影响。
- 慢查询日志支持将日志记录写入文件。
开启慢查询日志
- 查看慢查询日志相关:
show variables like '%slow_query_log%';
开启慢查询日志:
set global slow_query_log='ON';查询运行时间阈值:
show variables like 'long_query_time%';默认10s
设置
long_query_time:set global long_query_time = 1;,设置时间为1s查询当前系统中的慢查询记录:
SHOW GLOBAL STATUS LIKE '%Slow_queries%';
慢查询演示
- 数据准备
```mysql
创建student表
CREATE TABLEstudent(idINT(11) NOT NULL AUTO_INCREMENT,stunoINT NOT NULL,nameVARCHAR(20) DEFAULT NULL,ageINT(3) DEFAULT NULL,classIdINT(11) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
如果报错log_bin_trust_function_creators
set global log_bin_trust_function_creators = 1;
产生随机字符串
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 ;
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 ;
添加数据的存储过程
DELIMITER // CREATE PROCEDURE insert_stu1(START INT, max_num INT) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; #设置手动提交事务 REPEAT
#循环SET i = i + 1; #赋值INSERT INTO student (stuno, NAME, age, classId)VALUES ((START + i), rand_string(6), rand_num(10, 100), rand_num(10, 1000));UNTIL i = max_num END REPEAT;COMMIT; #提交事务
END // DELIMITER ;
插入400,0000条数据,从100001号开始
CALL insert_stu1(100001, 4000000);
-查看一条记录并查看慢查询条数> <a name="08da11bc"></a>### 慢查询日志分析:mysqldumpslow-使用mysqldumpslow查看慢查询的语句,后续使用EXPLAIN进行分析> <a name="561b9764"></a>### 慢查询日志关闭- 临时性关闭:`SET GLOBAL slow_query_log=off;`- 永久性关闭可以修改配置文件<a name="abe8c44a"></a>## 查看SQL执行成本:Show Profile-查看开启状态:`show variables like 'profiling';`-开启:`set profiling = 'ON';`-查看已经执行的SQL语句:`show profiles`-查看对应的SQL语句的执行成本:`show profile for query 编号;`> -**此命令将被弃用,可以从information_schema.PROFILING表查看**<a name="afac4737"></a>## 分析查询语句:EXPLAIN<a name="a4d3b02a"></a>### 概述- [官网](https://dev.mysql.com/doc/refman/8.0/en/explain-output.html)- MySQL优化器会对执行SQL给出他认为最优的执行计划。其中展示了具体执行计划的方式。**而`EXPLAIN`就是用来查看执行计划的工具。**<a name="f2b0b493"></a>### 语法-`explain + 需要执行的SQL语句`> -列的含义> <a name="9195cc17"></a>### 数据准备-sql语句```mysql# 两个表字段相同 索引也相同CREATE TABLE s1(id INT AUTO_INCREMENT,key1 VARCHAR(100),key2 INT,key3 VARCHAR(100),key_part1 VARCHAR(100),key_part2 VARCHAR(100),key_part3 VARCHAR(100),common_field VARCHAR(100),PRIMARY KEY (id),INDEX idx_key1 (key1),UNIQUE INDEX idx_key2 (key2),INDEX idx_key3 (key3),INDEX idx_key_part (key_part1, key_part2, key_part3)) ENGINE = INNODBCHARSET = utf8;CREATE TABLE s2(id INT AUTO_INCREMENT,key1 VARCHAR(100),key2 INT,key3 VARCHAR(100),key_part1 VARCHAR(100),key_part2 VARCHAR(100),key_part3 VARCHAR(100),common_field VARCHAR(100),PRIMARY KEY (id),INDEX idx_key1 (key1),UNIQUE INDEX idx_key2 (key2),INDEX idx_key3 (key3),INDEX idx_key_part (key_part1, key_part2, key_part3)) ENGINE = INNODBCHARSET = utf8;
- 表结构

- 添加数据
```mysql
set global log_bin_trust_function_creators = 1; # 不加global只是当前窗口有效
随机字符串函数
DELIMITER // CREATE FUNCTION rand_string1(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
RETURN return_str; END // DELIMITER ;DOSET return_str = CONCAT(return_str, SUBSTRING(chars_str, FLOOR(1 + RAND() * 52), 1)); SET i = i + 1;END WHILE;
表1添加数据
DELIMITER // CREATE PROCEDURE insert_s1(IN min_num INT(10), IN max_num INT(10)) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO s1 VALUES ((min_num + i), rand_string1(6), (min_num + 30 * i + 5), rand_string1(6), rand_string1(10), rand_string1(5), rand_string1(10), rand_string1(10)); UNTIL i = max_num END REPEAT; COMMIT; END // DELIMITER ;
表s2添加数据
DELIMITER // CREATE PROCEDURE insert_s2(IN min_num INT(10), IN max_num INT(10)) BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; REPEAT SET i = i + 1; INSERT INTO s2 VALUES ((min_num + i), rand_string1(6), (min_num + 30 * i + 5), rand_string1(6), rand_string1(10), rand_string1(5), rand_string1(10), rand_string1(10)); UNTIL i = max_num END REPEAT; COMMIT; END // DELIMITER ;
调用存储过程
CALL insert_s1(10001, 10000);
CALL insert_s2(10001, 10000);
<a name="7d64ccdb"></a>### EXPLAIN字段<a name="89cbf0e6"></a>#### table:表名-查询的**每一行记录都对应着一个单表**> <a name="48bd060c"></a>#### id:标识符-在一个大的查询语句中**每个SELECT关键字都对应一个唯一的id**> > > > 而对于UNION ALL不需要去重,那么就有两个id> -id小结- **id如果相同,可以认为是一组,从上往下顺序执行**- **在所有组中,id值越大,优先级越高,越先执行**- **关注点:每一个id表示一趟独立的查询, 一个sql的查询趟数越少越好**<a name="af815777"></a>#### select_type:SELECT类型-select_type包含的类型> -UNION和DEPENDENT UNION> > -DERIVED> 派生表:将查询结果作为表> -MATERIALIZED(物化)> 官网对于物化的介绍:[Optimizing Subqueries with Materialization](https://dev.mysql.com/doc/refman/8.0/en/subquery-materialization.html)> 为了加速子查询,物化将子查询的结果存在内存中。> > <a name="a5c48833"></a>#### partitions:分区-表示分区表中的命中情况。对于非分区表,该值为NULL-[ ALTER TABLE Partition Operations](https://dev.mysql.com/doc/refman/8.0/en/alter-table-partition-operations.html)> <a name="c5fdc841"></a>#### type:访问类型-[EXPLAIN Join Types](https://dev.mysql.com/doc/refman/8.0/en/explain-output.html#explain-join-types):The `type` column of [`EXPLAIN`](https://dev.mysql.com/doc/refman/8.0/en/explain.html) output **describes how tables are joined**.-表示MySQL**对某个表的执行查询时的访问方法**。最好到最坏的方法如下: `system` , `const` , `eq_ref` , `ref` , `fulltext` , `ref_or_null` , `index_merge` , `unique_subquery` , `index_subquery` , `range` , `index` , `ALL` 。-**system**:当表中只有一条记录并且存储引擎的统计数据是精确的> MyISAM存储引擎中每一个表都有额外的字段记录总的记录条数> 只有一条记录并且存储引擎是MyISAM,结果type是`system`> > 只有一条记录并且存储引擎是InnoDB,结果type是`ALL`> -`const`:**根据主键或者唯一二级索引列与常数进行等值匹配**时,对单表的访问方法就是`const`> > 而对于普通索引就是ALL> > **因为对于唯一索引,找到某个叶子结点中的记录的主键之后就可以进行回表操作。不再需要查看其他记录。**-`eq_ref`:在连接查询时,如果**被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的**,则对该被驱动表的访问方法就是`eq_ref`> 如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较> > 其实这里就是遍历s1表,**拿到每一个记录的id(这里为主键),此时就是常量了,去s2表中找id相同的记录。**-`ref`:**通过普通的二级索引列与常量进行等值匹配时来查询某个表**,那么对该表的访问方法就可能是`ref`> -`ref_or_null`:当对**普通二级索引进行等值匹配查询和NULL值匹配查询时**> -`index_merge`:索引合并使用> -`unique_subquery`:如果查询**优化器决定将`IN`子查询转换为`EXISTS`子查询,而且子查询可以使用到主键进行等值匹配的话**> -`range`:使用**索引获取某些`范围区间`的记录**> -`index`:当我们**可以使用索引覆盖,但需要扫描全部的索引记录时**> -`ALL`:全表扫描> ☆小结:结果值从最好到最坏依次是: system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL<br />SQL 性能优化的目标:**至少要达到range级别,要求是ref级别,最好是const级别。**(阿里巴巴开发手册要求)<a name="c1149e21"></a>#### key和possible_keys-possible_keys表示执行计划中**可能用到的索引**-key表示**实际使用到的索引。**> <a name="a6389def"></a>#### key_len:实际使用到的索引长度-单位:字节数-是否充分利用了索引,值越大越好。**主要针对于联合索引**,有一定的参考意义。> > 长度计算:> 允许为NULL,需要1个字节记录> 可变长度的字段(比如varchar),需要2个字节记录长度> 字符转换为字节:character set:utf8=3字节,gbk=2字节,latin1=1字节<a name="81836de8"></a>#### ref:与索隐列进行匹配的对象信息-当使用索引列进行等值查询的时候,与索隐列进行匹配的对象信息> > <a name="b7028e13"></a>#### rows:预计需要读取的行数-值越小越好> <a name="faf6213e"></a>#### filtered:过滤百分比-某个表**经过搜索条件过滤后剩余记录条数的百分比**> > 更常用在连接查询中驱动表对应的执行计划的filtered值> <a name="18971b60"></a>#### extra:额外信息-额外信息很重要。我们可以通过此信息**更准确地理解MySQL到底将如何执行给定的SQL语句**。-常见的信息如下:-No tables used> 当查询语句的没有`FROM`子句时将会提示该额外信息> -Impossible WHERE> **查询语句条件永远不可能成立**的时候提示的额外信息> -Using where> 当我们使用**全表扫描来执行对某个表的查询,并且该语句的`WHERE`子句中有针对该表的搜索条件时(除了索引字段)**,在`Extra`列中会提示上述额外信息。> -No matching min/max row> 当表中有MIN或MAX聚合函数,并且符合条件的记录不存在的时候> -Using Index(覆盖索引,建议)> 使用索引覆盖,不需要回表操作。**查询列表以及搜索条件中只包含属于某个索引的列**> > 由于非聚簇索引中有主键的值,所以查询列表中有主键和索引列的值的时候,也会显示此信息> -Using index condition(**使用了索引条件下推**)> 对于**条件中出现了索引字段,但是并未用上索引**> > 分析:`SELECT * FROM s1 WHERE key1 > 'z' AND key1 LIKE '%a';`> 1. 首先,根据索引`key1`查找到满足`key1>'z'`的记录。拿到记录对应的主键值> 2. 然后再去聚簇索引中根据主键值找到记录的key1值满足`key1 LIKE '%a'`的记录(回表),放入到结果集中> 3. 然后将结果集返回>事实上,**回表操作是很耗费时间的(随机IO)**。可能导致上面391条记录,经过`key1 LIKE '%a'`过滤后满足的很少,但是还是需要每个都去聚簇索引中查找。> **优化**:既然使用了key1索引,并且两个条件都是key1相关的字段。由于key1索引中已经包含了key1字段的值,那么在满足`key1>'z'`的基础之上,先**不进行回表操作,而是通过条件`key1 LIKE '%a'`过滤出最后的结果之后,再去聚簇索引拿到其他字段的信息。**-[索引条件下推(Index Condition Pushdown (ICP))](https://dev.mysql.com/doc/refman/8.0/en/index-condition-pushdown-optimization.html):一句话来说就是:**索引条件下推`ICP`就是尽可量利用二级索引筛除不符合`where`条件的记录,如此一来减少需要回表继续判断的次数**-索引条件下推**默认开启**,可以调整> 查看:`show variables like '%optimizer_switch%'`> 开启:`SET optimizer_switch = 'index_condition_pushdown=on';`> 关闭:`SET optimizer_switch = 'index_condition_pushdown=off';`> 关闭后再次执行,只显示Using where> -Using join buffer (hash join)> 在连接查询执行过程中,当**被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫`join buffer`的内存块来加快查询速度**,也就是我们所讲的`基于块的嵌套循环算法`> -Not exists> 当我们使用左(外)连接时,**如果`WHERE`子句中包含要求被驱动表的某个列等于`NULL`值的搜索条件,而且那个列又是不允许存储`NULL`值的**,那么在该表的执行计划的Extra列就会提示`Not exists`额外信息> -Using union> 表示使用**索引合并的方式执行查询**> -Zero limit> limit 0> -Using filesort(文件排序,尽量规避)> **很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序**(英文名:`filesort`)。> -Using temporary(使用临时表,尽量避免)> 在许多查询的执行过程中,MySQL可能会**借助临时表来完成一些功能**,比如去重、排序之类的,比如我们在执行许多**包含`DISTINCT`、`GROUP BY`、`UNION`等子句的查询过程中**,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的`Extra`列将会显示`Using temporary`提示> 非索引字段去重需要使用临时表将数据加载进内存中,然后再进行去重> > 而对于索引字段去重就不需要临时表,索引本身就是排好序的,直接在索引字段上进行去重即可> <a name="eb1e3038"></a>### EXPLAIN输出格式-EXPLAIN可以输出四种格式: `传统格式` , `JSON格式` , `TREE格式` 以及 `可视化输出` 。用户可以根据需要选择适用于自己的格式。-传统格式> 以表格的形式呈现> -JSON格式:`EXPLAIN FORMAT=JSON SQL语句`> 4种里面信息最详尽的格式,包含了**执行的成本信息**> ```json{"query_block": {"select_id": 1,"cost_info": {"query_cost": "1360.07" //执行成本},"nested_loop": [{"table": {"table_name": "s1","access_type": "ALL","possible_keys": ["idx_key1"],"rows_examined_per_scan": 9895,"rows_produced_per_join": 989,"filtered": "10.00","cost_info": {"read_cost": "914.80","eval_cost": "98.95","prefix_cost": "1013.75","data_read_per_join": "1M"},"used_columns": ["id","key1","key2","key3","key_part1","key_part2","key_part3","common_field"],"attached_condition": "((`test_db`.`s1`.`common_field` = 'a') and (`test_db`.`s1`.`key1` is not null))"}},{"table": {"table_name": "s2","access_type": "eq_ref","possible_keys": ["idx_key2"],"key": "idx_key2","used_key_parts": ["key2"],"key_length": "5","ref": ["test_db.s1.key1"],"rows_examined_per_scan": 1,"rows_produced_per_join": 989,"filtered": "100.00","index_condition": "(cast(`test_db`.`s1`.`key1` as double) = cast(`test_db`.`s2`.`key2` as double))","cost_info": {"read_cost": "247.38","eval_cost": "98.95","prefix_cost": "1360.08","data_read_per_join": "1M"},"used_columns": ["id","key1","key2","key3","key_part1","key_part2","key_part3","common_field"]}}]}}
- TREE格式

- 可视化输出:MySQL Workbench
Show warnings
- 在执行Explain之后,可通过
show warnings查看与执行的EXPLAIN的相关的一些扩展信息可以看到优化器优化后的语句
比如之前子查询的语句
show warnings中显示的信息
小结
- EXPLAIN不会考虑缓存,所以不存在缓存影响多次运行分析的结果不同
- EXPLAIN不能显示MySQL在执行查询时所做的优化工作,比如,IN转化为EXISTS
- EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
- 部分统计信息是估算的,并非精确值
分析优化器执行计划:trace
OPTIMIZER_TRACE可以跟踪优化器做出的各种决策(比如访问表的方法、各种开销计算、各种转换等),并将跟踪结果记录到information_schema.OPTIMIZER_TRACE表中默认关闭,开启trace,并设置格式为JSON,同时设置trace最大能够使用的内存大小,避免音内存过小导致信息不完整 ```mysql SET optimizer_trace=”enabled=on”,end_markers_in_json=on;
set optimizer_trace_max_mem_size=1000000; ```
MySQL监控分析视图:sys schema
- MySQL5.7.7中新增,将
information_schema和performance_schema中的数据以更容易理解的方式总结归纳为视图,目的就是为了降低查询performance_schema数据库的复杂度






