笔记整理自 【宋红康】MySQL数据库(mysql安装/基础/高级/优化),还参考网上的其他技术文章作为补充,具体参考链接在文末

⏰数据库服务器的优化步骤

🗺️流程图

把思考的流程整理成下面这张图

整个流程划分成了 观察(Show status)行动(Action) 两个部分。字母 S 的部分代表观察(会使 用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)。

优化流程1

总结:

image-20220120185447468

📈查看系统性能参数

在MySQL中,可以使用 SHOW STATUS 语句查询一些MySQL数据库服务器的 性能参数 、 执行频率 。

SHOW STATUS语句语法如下:

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的查询成本:last_query_cost

show status like 'last_query_cost'

一条 SQL 查询语句在执行前需要确定查询计划,如果存在多种查询计划的话,MySQL 会计算每个查询计划所需要的成本,从中选择成本最小的一个作为最终执行的查询计划。

如果查看某条 SQL 语句的查询成本,可以在执行完这条 SQL 语句之后,通过查看当前会话中的 last_query_cost 变量值来得到当前查询的成本。这个查询成本对应的是 SQL 语句所需要读取的页(page)的数量。

注意查询成本越高,查询效率并不一定低

比如两个查询语句,他们查询成本相差20倍,但如果mysql采用了顺序读取的方式将页面一次性加载到缓冲池中,然后再进行查找,虽然 页 数量(last_query_cost)增加了不少 ,但是通过缓冲池的机制,并 没有增加多少查询时间 。

📸定位执行慢的 SQL:慢查询日志

1、开启慢查询日志参数

  • 开启slow_query_log
mysql > set global slow_query_log='ON';
  • 修改long_query_time阈值

这里如果我们想把时间缩短,比如设置为 1 秒,可以这样设置:

#测试发现:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。所以可以一并
执行下述语句
mysql > set global long_query_time = 1;
mysql> show global variables like '%long_query_time%';

mysql> set long_query_time=1;
mysql> show variables like '%long_query_time%';

2、 查看慢查询数目

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

SHOW GLOBAL STATUS LIKE '%Slow_queries%';

3、慢查询日志分析工具:mysqldumpslow

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

mysqldumpslow 命令的具体参数如下:

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

工作常用参考:

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

4、关闭慢查询日志

  • 方式1:永久性方式
[mysqld]
slow_query_log=OFF

或者,把slow_query_log一项注释掉 或 删除

[mysqld]
#slow_query_log =OFF

重启MySQL服务,执行如下语句查询慢日志功能。

SHOW VARIABLES LIKE '%slow%'; #查询慢查询日志所在目录
SHOW VARIABLES LIKE '%long_query_time%'; #查询超时时长
  • 方式2:临时性方式

使用SET语句来设置

(1)停止MySQL慢查询日志功能,具体SQL语句如下。

SET GLOBAL slow_query_log=off

(2)重启MySQL服务,使用SHOW语句查询慢查询日志功能信息

SHOW VARIABLES LIKE '%slow%';
#以及
SHOW VARIABLES LIKE '%long_query_time%';

🎬查看 SQL 执行成本:SHOW PROFILE

通过设置 profiling='ON’ 来开启 show profile:

mysql > set profiling = 'ON';

然后执行相关的查询语句。接着看下当前会话都有哪些 profiles,使用下面这条命令:

mysql > show profiles;

image-20220123071352646

你能看到当前会话一共有 2 个查询。如果我们想要查看最近一次查询的开销,可以使用:

mysql > show profile

image-20220123071441110

mysql> show profile cpu,block io for query 2

image-20220123071504470

show profile的常用查询参数:

① ALL:显示所有的开销信息。

② BLOCK IO:显示块IO开销。

③ CONTEXT SWITCHES:上下文切换开 销。

④ CPU:显示CPU开销信息。

⑤ IPC:显示发送和接收开销信息。

⑥ MEMORY:显示内存开销信 息。

⑦ PAGE FAULTS:显示页面错误开销信息。

⑧ SOURCE:显示和Source_function,Source_file, Source_line相关的开销信息。 ⑨ SWAPS:显示交换次数开销信息。

💻分析查询语句:EXPLAIN

💡版本情况

  • 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中的信息。

基本语法

EXPLAIN 或 DESCRIBE语句的语法形式如下:

EXPLAIN SELECT select_options
或者
DESCRIBE SELECT select_options

如果我们想看看某个查询的执行计划的话,可以在具体的查询语句前边加一个 EXPLAIN ,就像这样:

mysql> EXPLAIN SELECT 1

EXPLAIN 语句输出的各个列的作用如下:

image-20220123082710327

EXPLAIN各列作用

以下分析所用到的表结构

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=INNODB CHARSET=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=INNODB CHARSET=utf8;

1. table

EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该 表的表名(有时不是真实的表名字,可能是简称)。

2、id

image-20220123082855741

在一个大的查询语句中每个SELECT关键字都对应一个唯一的id

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

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

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

3、select_type

(1) simple

查询语句中不包含UNION或者子查询的查询都算作是SIMPLE类型,无论是单表查询还是联合查询这些查询的级别都是 simple。顾名思义,这些查询都被 MySQL 认为是比较简单的查询模式。

单表查询:
image-20220123084323830

连接查询:

使用子查询:

💡这里需要说明的一点是,其实子查询严格意义上不是 simple 级别的,但是我这里举得这个例子比较特殊,原理是因为这个 SQL 在MySQL 的查询优化器上被优化成了连接查询,所以才会出现这种情况。换句话说,这个 SQL 最终执行的内容和上面的这个 SQL 是一种类型。

(2) primary

对于包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type值就是PRIMARY

image-20220123084052415

image-20220123084059624

(3) union

包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的嘛。除了第一个是 PRIMARY,其他的都是 UNION

(4) union result

如果 MySQL 中的 UNION 需要用到临时表进行去重的话,那么这个小查询的级别就是 UNION RESULT

(5) subquery

如果我们的子查询不能转换对应 semi-join的形式,而且这个查询不是相关子查询的话,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,这个时候该子查询的第一个 SELECT 的级别就是 SUBQUERY

💡相关子查询

内部查询的执行依赖于外部查询的数据,外部查询每执行一次,内部查询也会执行一次。

image-20220123083903733

(6) dependent subquery

如果包含子查询的查询语句不能够转为对应的semi-join的形式,并且该子查询是相关子查询,则该子查询的第一个SELECT关键字代表的那个查询的select_type就是DEPENDENT SUBQUERY

image-20220123083833443

(7) dependent union

在包含UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的select_type的值就是DEPENDENT UNION

image-20220123083749939

4、type

完整的访问方法如下: systemconsteq_refreffulltextref_or_nullindex_mergeunique_subqueryindex_subqueryrangeindexALL

  • system

    当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM、Memory,那么对该表的访问方法就是system

 CREATE TABLE t(i INT) ENGINE=MYISAM;
 INSERT INTO t VALUES(1);

 EXPLAIN SELECT * FROM t;

image-20220123084801476

  • const

    当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const

    EXPLAIN SELECT * FROM s1 WHERE id = 10005;
    

    image-20220123084936108

  • eq_ref

    在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref

    EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;
    

    image-20220123085026304

    从执行计划的结果中可以看出,MySQL打算将s2作为驱动表,s1作为被驱动表,重点关注s1的访问 方法是 eq_ref ,表明在访问s1表的时候可以 通过主键的等值匹配 来进行访问。

  • ref

    当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref

    EXPLAIN SELECT * FROM s1 WHERE key1 = 'a';
    

    image-20220123085245449

  • fulltext

    全文索引

  • ref_or_null

    当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是ref_or_null

    EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key1 IS NULL;
    

    image-20220123085700314

  • index_merge

    单表访问方法时在某些场景下可以使用IntersectionUnionSort-Union这三种索引合并的方式来执行查询

    EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
    

    image-20220123085753198

    从执行计划的 type 列的值是 index_merge 就可以看出,MySQL 打算使用索引合并的方式来执行 对 s1 表的查询。

  • unique_subquery

    unique_subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery

    EXPLAIN SELECT * FROM s1 
    WHERE key2 IN (SELECT id FROM s2 WHERE s1.key1 = s2.key1) OR key3 = 'a';
    

    image-20220123085935795

  • index_subquery

    子查询使用了普通索引

    该联接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引

    mysql> EXPLAIN SELECT * FROM s1 WHERE common_field IN (SELECT key3 FROM s2 where
    s1.key1 = s2.key1) OR key3 = 'a';
    

    image-20220123090221833

  • range

    如果使用索引获取某些范围区间的记录,那么就可能使用到range访问方法

    EXPLAIN SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c');
    

    image-20220123090256511

  • index

    当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index

    EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a';
    

    image-20220123090406169

    💡索引覆盖针对于select条件;索引覆盖不像索引最左匹配那么严格要求,索引覆盖认定的字段上只要存在索引就行。

    注意跟二级索引对比,可以发现出现index时,此时的并不遵循最左匹配原则

  • ALL

    EXPLAIN SELECT * FROM s1;
    

    image-20220123092812611

小结: 结果值从最好到最坏依次是:

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

其中比较重要的几个提取出来(见加粗部分)。

SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。(阿里巴巴 开发手册要求)

5、possible_keys和key

可能用到的索引 和 实际上使用的索引

image-20220123092941636

6、key_len

实际使用到的索引长度(即:字节数)

帮你检查是否充分的利用上了索引值越大越好,主要针对于联合索引,有一定的参考意义。

image-20220123093112911

key_len的长度计算公式:

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

varchar(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)

6、ref

当使用索引列等值查询时,与索引列进行等值匹配的对象信息。比如只是一个常数或者是某个列。

image-20220123093726652

7、row

预估的需要读取的记录条数, 值越小越好

image-20220123093810088

8、filtered

某个表经过搜索条件过滤后剩余记录条数的百分比

  • 如果使用的是索引执行的单表扫描,那么计算时需要估计出满足除使用到对应索引的搜索条件外的其他搜索条件的记录有多少条。

    💡假如type=all,表示以全表扫描的方式得到1000条记录,且filtered=0.1%,表示只有1条记录是符合搜索条件的。此时如果加一个索引可以直接搜出来1条数据,那么filtered就可以提升到100%。所以filtered越大越好

image-20220123093911362

  • 对于单表查询来说,这个filtered列的值没什么意义,我们更关注在连接查询中驱动表对应的执行计划记录的filtered值,它决定了被驱动表要执行的次数(即:rows * filtered)

image-20220123094627564

9、Extra

一些额外的信息,更准确的理解MySQL到底将如何执行给定的查询语句

  • No tables used

    当查询语句的没有FROM子句时将会提示该额外信息

    image-20220123095731466

  • Impossible WHERE

    查询语句的WHERE子句永远为FALSE时将会提示该额外信息

    image-20220123095812291

  • Using where

  • 当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息。

    EXPLAIN SELECT * FROM s1 WHERE common_field = 'a';
    

    image-20220123095934446

  • 当使用索引访问来执行对某个表的查询,并且该语句的WHERE子句中有除了该索引包含的列之外的其他搜索条件时,在Extra列中也会提示上述额外信息。

    image-20220123095858979

  • No matching min/max row

    当查询列表处有MIN或者MAX聚合函数,但是并没有符合WHERE子句中的搜索条件的记录时,将会提示该额外信息

    image-20220123100154757

  • Using index

    当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,在Extra列将会提示该额外信息。比方说下边这个查询中只需要用到idx_key1而不需要回表操作

    image-20220123100246278

  • Using index condition

    使用了索引下推优化

    如果可以只使用索引中的列来计算WHERE条件的一部分,MySQL服务器就会将WHERE条件的这一部分推到存储引擎中。然后,存储引擎通过使用索引条目来评估推入的索引条件,只有当满足该条件时,才从表中读取行。

    image-20220123100331479

  • Using join buffer (Block Nested Loop)

    在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为

    其分配一块名叫join buffer的内存块来加快查询速度,也就是我们所讲的基于块的嵌套循环算法

    image-20220123100651246

  • Not exists

    我们使用左(外)连接时,如果WHERE子句中包含要求被驱动表的某个列等于NULL值的搜索条件,而且那个列又是不允许存储NULL值的,那么在该表的执行计划的Extra列就会提示Not exists额外信息

    EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;
    

    image-20220123100752231

  • Using intersect(…) 、 Using union(…) 和 Using sort_union(…)

  • 如果执行计划的Extra列出现了Using intersect(...)提示,说明准备使用Intersect索引合并的方式执行查询,括号中的...表示需要进行索引合并的索引名称;

  • 如果出现了Using union(...)提示,说明准备使用Union索引合并的方式执行查询;

  • 出现了Using sort_union(...)提示,说明准备使用Sort-Union索引合并的方式执行查询

    image-20220123100843605

  • Zero limit

    当我们的LIMIT子句的参数为0时,表示压根儿不打算从表中读出任何记录,将会提示该额外信息

    image-20220123100912343

  • Using filesort

    很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort)。

    image-20220123100950283

  • Using temporary

  • 在许多查询的执行过程中,MySQL可能会借助临时表来完成一些功能,比如去重、排序之类的,比如我们

  • 在执行许多包含DISTINCTGROUP BYUNION等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。

  • 如果查询中使用到了内部的临时表,在执行计划的Extra列将会显示Using temporary提示

    image-20220123101036072

💡执行计划中出现Using temporary并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以我们最好能使用索引来替代掉使用临时表。比如:扫描指定的索引idx_key1即可

小结:

  • EXPLAIN不考虑各种Cache

  • EXPLAIN不能显示MySQL在执行查询时所作的优化工作

  • EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况

  • 部分统计信息是估算的,并非精确值

🔎EXPLAIN的进一步使用

EXPLAIN三种输出格式

1、传统格式

image-20220123101401856

2、JSON格式

JSON格式:在EXPLAIN单词和真正的查询语句中间加上 FORMAT=JSON 。

EXPLAIN FORMAT=JSON SELECT ....

image-20220123101649512

const_info:执行成本

"cost_info": {
    "read_cost": "1840.84",
    "eval_cost": "193.76",
    "prefix_cost": "2034.60",
    "data_read_per_join": "1M"
}
  • read_cost 是由下边这两部分组成的:
  • IO 成本
  • 检测 rows × (1 - filter) 条记录的 CPU 成本
  • eval_cost 是这样计算的: 检测 rows × filter 条记录的成本。
  • prefix_cost 就是单独查询 s1 表的成本,也就是: read_cost + eval_cost
  • data_read_per_join 表示在此次查询中需要读取的数据量。

3、TREE格式

TREE格式是8.0.16版本之后引入的新格式,主要根据查询的 各个部分之间的关系 和 各部分的执行顺序 来描 述如何查询。

image-20220123101930283

SHOW WARNINGS的使用

显示上一个命令的警告信息

image-20220123102012925

📺分析优化器执行计划:trace

SET optimizer_trace="enabled=on",end_markers_in_json=on;

set optimizer_trace_max_mem_size=1000000;

开启后,可分析如下语句:

  • SELECT

  • INSERT

  • REPLACE

  • UPDATE

  • DELETE

  • EXPLAIN

  • SET

  • DECLARE

  • CASE

  • IF

  • RETURN

  • CALL

🔗参考链接:

详细解释MySQL explain 中的 select_type 是什么