MySQL学习笔记索引篇

索引的声明与使用

索引的分类

MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。

  • 从功能逻辑来说,主要有4中:普通索引、唯一索引、主键索引、全文索引
  • 从物理实现方式来说,主要有2中:聚簇索引和非聚簇索引
  • 按照作用字段个数来说:单列索引和联合索引

普通索引

创建普通索引时,不附加任何限制条件,只是用于提高查询效率,可以创建在任何数据类型中。

唯一性索引

UNIQUE参数设置唯一性约束时,会自动添加唯一性索引。创建唯一性索引时,限制该索引的值必须唯一,但允许有空值。一张表可以有多个唯一索引。

主键索引

主键索引就是一种特殊的唯一性索引,在唯一性索引的基础上增加不为空的约束。一张表最多只有一个主键索引。

单列索引

表中单个字段上创建索引。单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只作用在一个字段上即可。

多列索引

也称组合、联合索引,在表的多个字段组合上创建一个索引。通过这几个字段查询时,只有查询条件使用这些字段中的第一个字段才会被使用,要遵循最左前缀集合

全文索引

也称全文检索,是搜索引擎使用的一种关键技术,利用分词技术分析文本中关键词的频率和重要性。

使用参数FULLTEXT设置全文索引。在定义索引的列上支持值的全文查找,允许这些索引列中插入重复值和空值。全文索引只能创建在CHAR、VARCHAR或TEXT类型及其系列类型的字段上,查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度。

全文索引典型的有两种类型:自然语言的全文索引和布尔全文索引。

空间索引

使用参数SPATIAL可以设置索引为空间索引。空间索引只能建立在空间数据类型上,可以提高系统获取空间数据的效率。目前只有MyISAM支持空间索引,而且索引的字段不能为空值。

小结

InnoDB:支持B-TreeFull-text等索引,不支持Hash索引

MyISAM:支持B-TreeFull-text等索引,不支持Hash索引

Memory:支持B-TreeHash等索引,不支持Full-text索引

NDB:支持Hash索引,不支持B-TreeFull-text等索引

Archive:不支持B-TreeFull-textHash索引

创建索引

MySQL支持多种方法在单个或多个列上创建索引:创建表时指定索引列,或更新表时创建索引。

创建表时创建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-- 隐式创建,创建主键约束、唯一性约束、非空约束、外键约束时,会创建对应索引
-- 第一方式,在列上声明索引
CREATE TABLE index_test1(
-- 创建主键约束,
id INT PRIMARY KEY,
-- 创建唯一索引
`name` VARCHAR(15) UNIQUE
);
-- 第二种方式,在表外部创建索引
CREATE TABLE index_test2(
-- 创建主键索引
id INT PRIMARY KEY,
-- 创建唯一索引
`name` VARCHAR(15) NOT NULL,
test1_id INT,
-- 声明将 index_test1 的id,作为index_test2的外键,作用在列 test1_id 上
CONSTRAINT test1_id_fk FOREIGN KEY(test1_id) REFERENCES index_test1(id)
);

-- 第二种方式,显式创建索引
CREATE TABLE index_test3(
id INT,
`name` VARCHAR(100),
`authors` VARCHAR(15),
`comment` VARCHAR(100),
-- 创建普通索引
INDEX idx_name(`name`),
-- 创建唯一索引,插入数据时需要确保唯一性
UNIQUE INDEX uk_comment(`comment`),
-- 创建主键索引
PRIMARY KEY pk_id(id),
);

-- 创建联合索引
CREATE TABLE index_test4(
id INT,
`name` VARCHAR(100),
info VARCHAR(100),
INDEX mul_id_name_info(id,`name`,info)
);

-- 创建全文索引
CREATE TABLE index_test5(
id INT,
info VARCHAR(50),
-- 将info字段的前20个字符做全文索引
FULLTEXT INDEX full_index_info(info(20))
);

-- 查看索引
-- 方式1
SHOW CREATE TABLE index_test1;
-- 方式2
SHOW INDEX FROM index_test4;

获取索引的信息

1
2
3
4
5
6
7
8
9
SHOW INDEX FROM index_test4;
+-------------+------------+------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------------+------------+------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| index_test4 | 1 | mul_id_name_info | 1 | id | A | 0 | NULL | NULL | YES | BTREE | | | YES | NULL |
| index_test4 | 1 | mul_id_name_info | 2 | name | A | 0 | NULL | NULL | YES | BTREE | | | YES | NULL |
| index_test4 | 1 | mul_id_name_info | 3 | info | A | 0 | NULL | NULL | YES | BTREE | | | YES | NULL |
+-------------+------------+------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
3 rows in set (0.01 sec)
  • Seq_in_index:代表索引的顺序,使用时需要按照顺序使用。

使用全文检索时,查询方式就不是用LIKE,查询速度更快

1
2
-- 使用全文索引检索
SELECT * FROM index_test5 WHERE MATCH(info) against('查询字符串');

创建空间索引

1
2
3
4
5
CREATE TABLE index_table6(
-- 空间索引字段必须为非空
geo GEOMETRY NOT NULL,
SPATIAL INDEX spa_idx_geo(geo)
) ENGINE=MyISAM;

已存在的表添加索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 第一种方式,添加索引
CREATE TABLE index_test11(
id INT,
`name` VARCHAR(100),
info VARCHAR(100)
);
-- 添加普通索引
ALTER TABLE index_test11 ADD INDEX idx_cmt(info);
-- 添加唯一索引
ALTER TABLE index_test11 ADD UNIQUE idx_uik(`name`);
-- 添加联合索引
ALTER TABLE index_test11 ADD INDEX mul_id_name(id,`name`);
-- 第二种方式,创建索引
CREATE TABLE index_test12(
id INT,
`name` VARCHAR(100),
info VARCHAR(100)
);
-- 创建普通索引
CREATE INDEX idx_cmt ON index_test12(info);
-- 创建唯一索引
CREATE UNIQUE INDEX idx_uik ON index_test12(`name`);
-- 创建联合索引
CREATE INDEX mul_id_name ON index_test12(id,`name`);

删除索引

1
2
3
4
5
6
7
8
9
10
-- 删除索引
-- 方式1、修改的方式删除索引
-- 删除主键索引
ALTER TABLE index_test2 DROP PRIMARY KEY;
-- 删除其他索引
ALTER TABLE index_test3 DROP INDEX uk_comment;
-- 方式2、drop的方式删除索引
DROP INDEX idx_cmt ON index_test12;
-- 删除联合索引中的某一个字段,删除之后,索引会重建
ALTER TABLE index_test4 DROP COLUMN `name`;

删除表中的列时,如果要删除的列为索引的组成部分,该列也会从索引中删除。如果组成索引的列全部被删除,则整个索引将被删除。

MySQL 8.0索引新特性

支持降序索引

仅限于InnoDB存储引擎。

8.0版本之前创建的任然是升序索引,使用时进行反向扫描,大大降低了数据库的效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
CREATE TABLE index_test21(
a INT,
b INT,
-- 创建索引,先按照a升序,然后按照b降序
INDEX idx_a_b(a ASC,b DESC)
);
SHOW INDEX FROM index_test21;

SHOW INDEX FROM index_test21\G
*************************** 1. row ***************************
Table: index_test21
Non_unique: 1
Key_name: idx_a_b
Seq_in_index: 1
Column_name: a
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
*************************** 2. row ***************************
Table: index_test21
Non_unique: 1
Key_name: idx_a_b
Seq_in_index: 2
Column_name: b
Collation: D
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
2 rows in set (0.01 sec)

show create table index_test21\G
*************************** 1. row ***************************
Table: index_test21
Create Table: CREATE TABLE `index_test21` (
`a` int DEFAULT NULL,
`b` int DEFAULT NULL,
KEY `idx_a_b` (`a`,`b` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

-- 使用时,需要按照a升序,b降序
SELECT * FROM index_test21 ORDER BY a,b DESC;
-- 而不能用a降序
SELECT * FROM index_test21 ORDER BY a DESC,b DESC;

隐藏索引

将需要待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引。这种通过先将索引设置为隐藏索引,再删除索引的方式,就是软删除。

隐藏索引还可以用来验证删除之后的查询性能。

主键不能设置为隐藏索引,当表中没有显示主键时,表中第一个唯一非空索引会成为隐式主键,也不能设置为隐藏索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- 隐藏索引
-- 创建表时,隐藏索引
CREATE TABLE index_test22(
id INT,
`name` VARCHAR(100),
info VARCHAR(100),
-- 创建不可见索引
INDEX idx_info(info) INVISIBLE
);

show index from index_test22\G
*************************** 1. row ***************************
Table: index_test22
Non_unique: 1
Key_name: idx_info
Seq_in_index: 1
Column_name: info
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
Visible: NO -- 不可见
Expression: NULL
1 row in set (0.01 sec)

-- 创建表之后
ALTER TABLE index_test22 ADD UNIQUE INDEX uk_idx_test22(`name`) INVISIBLE;
CREATE INDEX uk_idx_test22_1 ON index_test22(id) INVISIBLE;
-- 修改可见性
-- 修改成不可见
ALTER TABLE index_test22 ALTER INDEX uk_idx_test22 INVISIBLE;
-- 修改成可见
ALTER TABLE index_test22 ALTER INDEX uk_idx_test22 VISIBLE;

当索引被隐藏时,也是会实时更新。

使隐藏索引对优化器可见

1
2
3
4
5
6
7
-- 获取隐藏索引对优化器的可见状态
SELECT @@optimizer_switch\G
*************************** 1. row ***************************
@@optimizer_switch: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,use_invisible_indexes=off,skip_scan=on,hash_join=on,subquery_to_derived=off,prefer_ordering_index=on,hypergraph_optimizer=off,derived_condition_pushdown=on
1 row in set (0.01 sec)
-- 修改
SET SESSION optimizer_switch="use_invisible_indexes=on";

索引设计原则

索引可以加速查询,但是也会占用存储,而且降低插入和修改性能。

适合创建索引的情况

  1. 字段的数值有唯一性约束,唯一性索引可以很快确定某个数据信息,如果不是唯一性,则可能出现往前查询或者往后查询都是链表查询。

  2. 频繁作为WHERE条件的字段

  3. 经常GROUP BYORDER BY的列,分组和排序,使用索引性能会更好,此时需要注意创建多个索引或者联合索引,以及联合索引的顺序,创建联合索引,先 GROUP BY的列,后ORDER BY的列

  4. UPDATEDELETEWHERE条件

  5. DISTINCT字段

  6. 多表JOIN时:

    1. 连接表的数量尽量不要超过3张
    2. WHERE条件创建索引
    3. 对用于链接的字段创建索引
  7. 使用列的类型小的创建索引,这里类型小的,指的是数据范围的大小

  8. 使用字符串前缀创建索引,alter table shop add index(address(12));,长度计算公式 count(distinct left(列名, 索引长度)) / count(**)

    阿里巴巴开发手册:在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。

  9. 区分度高(散列性高)的列适合作为索引

  10. 使用频繁的列放到联合索引的左侧(最左匹配原则)

  11. 在多个字段都要创建索引的情况下,联合索引优于单值索引

限制索引数据

建议单表索引数量不超过6个

  1. 索引越多,占用磁盘空间越大
  2. 索引越多,增删改语句的性能越差
  3. 优化器选择如何优化时,成本更高

不适合创建索引的情况

  1. where中使用不到的字段,不设置索引(group byorder by中一样)
  2. 数据量小的表最好不要使用索引
  3. 有大量重复数据的列上不要创建索引,重复度高于10%时,也不需要创建索引
  4. 避免对经常更新的表创建过多的索引
  5. 不建议用无序的值作为索引,例如身份证,UUID(索引比较时需要转为ASCII,并且插入时可能造成页分裂),MD5HASH,无序长字符串等
  6. 删除不再使用或者很少使用的索引
  7. 不要定义冗余或重复的索引,例如创建a,b,c的联合索引,再创建a的索引就冗余了。

性能分析工具的使用

发现执行SQL时存在不规则延迟或卡顿的情况,就可以采用分析工具定位有问题的SQL,有三种分析工具也可以理解是SQL调优的三个步骤:慢查询EXPLAINSHOW PROFILES

image-20221116095527717

查看系统性能参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 连接MySQL服务器的次数
SHOW STATUS LIKE 'connections';
-- MySQL服务器的上线时间
SHOW STATUS LIKE 'uptime';
-- 慢查询次数
SHOW STATUS LIKE 'slow_queries';
-- SELECT 查询返回的行数,也就是查询操作返回的行数的叠加
SHOW STATUS LIKE 'innodb_rows_read';
-- 执行 INSERT 操作插入的行数
SHOW STATUS LIKE 'innodb_rows_inserted';
-- 执行 UPDATE 操作更新的行数
SHOW STATUS LIKE 'innodb_rows_updated';
-- 执行 DELETE 操作删除的行数
SHOW STATUS LIKE 'innodb_rows_deleted';
-- 查询操作的次数
SHOW STATUS LIKE 'com_select';
-- 插入操作的次数,对于批量插入的insert操作,只累加一次
SHOW STATUS LIKE 'com_insert';
-- 更新操作的次数
SHOW STATUS LIKE 'com_update';
-- 删除操作的次数
SHOW STATUS LIKE 'com_delete';

统计SQL的查询成本

执行SQL语句之后,通过查看当前会话中的last_query_cost变量来得到当前查询的成本,这个通常是评价一个查询的执行效率的常用指标,这个查询成本对应的是SQL语句所需要读取的页的数量

1
2
3
4
5
6
7
8
SELECT * FROM employee;
SHOW STATUS LIKE 'last_query_cost';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| Last_query_cost | 2.049000 | -- 读取的页数是 2.049
+-----------------+----------+
1 row in set (0.03 sec)

SQL采用顺序读取的方式将页面一次性加载到缓冲池中,然后进行查找。虽然页数量可能会增加,但是通过缓冲池的机制,并没有增加多少查询成本。

从页加载的角度看:

  1. 位置决定效率。如果页在数据库的缓冲池中,效率高。
  2. 批量决定效率。如果对磁盘单页随机读,效率会比较差,而采用顺序读,批量对页进行读取,平均一页的效率会提升很多。

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

运行时间超过 long_query_time 值的SQL,则会记录到慢查询日志中。long_query_time默认是10,也就是超过10s(不包括10s)的查询语句,会被认为是慢查询。

默认情况下,MySQL没有开启慢查询日志,如果不是调优需要的话,一般不建议开启改参数,因为会对性能带来一定的影响。

慢查询日志支持将日志记录写入文件。

开启慢查询日志参数

1
2
3
4
5
6
7
8
9
10
11
12
-- 获取慢查询日志参数
SHOW VARIABLES LIKE '%slow_query_log%';
+---------------------+--------------------------------------+
| Variable_name | Value |
+---------------------+--------------------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/lib/mysql/87e36d56e6fe-slow.log |
+---------------------+--------------------------------------+
2 rows in set (0.01 sec)

-- 设置慢查询日志参数
SET GLOBAL slow_query_log=ON;

查看和修改慢查询阈值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 查看慢查询阈值
SHOW VARIABLES LIKE '%long_query_time%';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.02 sec)

-- 修改慢查询阈值
-- 改 session,改成 0.1 s(只有当前session有效)
SET long_query_time = 0.1;
-- 改 GLOBAL,改成 0.1 s
SET GLOBAL long_query_time = 1;

如果永久修改,则需要修改/etc/my.cnf配置文件。

查看慢查询数据

1
2
-- 查询当前系统中有多少条慢查询记录
SHOW GLOBAL STATUS LIKE '%Slow_queries';

测试以及分析

创建测试表

1
2
3
4
5
6
7
8
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` tinyint(4) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

生成测试数据-100万条记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
delimiter //
DROP PROCEDURE IF EXISTS proc_batch_insert;
CREATE PROCEDURE proc_batch_insert()
BEGIN
DECLARE pre_name BIGINT;
DECLARE ageVal INT;
DECLARE i INT;
SET pre_name=187635267;
SET ageVal=100;
SET i=1;
-- 循环 100 0000 次
WHILE i <= 1000000 DO
INSERT INTO t_user(`name`,age,create_time,update_time) VALUES(CONCAT(pre_name,'@qq.com'),(ageVal+1)%30,NOW(),NOW());
SET pre_name=pre_name+100;
SET i=i+1;
SET ageVal=ageVal+1;
END WHILE;
END //

delimiter ;

-- 调用存储过程
call proc_batch_insert();

存储过程执行两次

1
2
3
4
5
6
7
SELECT COUNT(*) FROM `t_user`;
+----------+
| COUNT(*) |
+----------+
| 2000000 |
+----------+
1 row in set (0.06 sec)

检索测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
SELECT * FROM t_user WHERE name = '[email protected]';
+---------+------------------+------+---------------------+---------------------+
| id | name | age | create_time | update_time |
+---------+------------------+------+---------------------+---------------------+
| 12001 | 188835267@qq.com | 11 | 2022-11-16 05:47:36 | 2022-11-16 05:47:36 |
| 1012001 | 188835267@qq.com | 11 | 2022-11-16 06:22:04 | 2022-11-16 06:22:04 |
+---------+------------------+------+---------------------+---------------------+
2 rows in set (0.21 sec)

-- 查看全局慢查询次数
SHOW GLOBAL STATUS LIKE '%Slow_queries';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries | 2 |
+---------------+-------+
1 row in set (0.00 sec)

-- 查看当前会话慢查询次数
SHOW STATUS LIKE '%Slow_queries';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries | 0 |
+---------------+-------+
1 row in set (0.00 sec)

min_examined_row_limit设置查询扫描过的最少记录数,与变量和查询执行时间,共同组成判断别一个查询是否是慢查询的条件。

1
2
3
4
5
6
7
SHOW VARIABLES LIKE 'min%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| min_examined_row_limit | 0 | -- 默认是0,代表超过慢查询阈值,即使没有扫描1条记录,也会记录到慢查询日志中。
+------------------------+-------+
1 row in set (0.00 sec)

慢查询日志分析工具

可以使用命令行工具mysqldumpslow,也可以直接查看日志文件。

1
2
3
4
5
6
7
mysqldumpslow -s c -t 10 /var/run/mysqld/mysqld-slow.log # 取出使用最多的10条慢查询

mysqldumpslow -s t -t 3 /var/run/mysqld/mysqld-slow.log # 取出查询时间最慢的3条慢查询

mysqldumpslow -s t -t 10 -g “left join” /database/mysql/slow-log # 得到按照时间排序的前10条里面含有左连接的查询语句

mysqldumpslow -s r -t 10 -g 'left join' /var/run/mysqld/mysqld-slow.log # 按照扫描行数最多的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cat 87e36d56e6fe-slow.log

/usr/sbin/mysqld, Version: 8.0.31 (MySQL Community Server - GPL). started with:
Tcp port: 3306 Unix socket: /var/run/mysqld/mysqld.sock
Time Id Command Argument

# 第一条,执行存储过程
# Time: 2022-11-16T06:12:04.364900Z
# User@Host: root[root] @ [172.17.0.1] Id: 10
# Query_time: 1488.306346 Lock_time: 0.000002 Rows_sent: 0 Rows_examined: 0
use dbtest;
SET timestamp=1668579124;
call proc_batch_insert();

# 第二条,执行查询语句
# Time: 2022-11-16T06:25:56.937954Z
# User@Host: root[root] @ [172.17.0.1] Id: 11
# Query_time: 0.215468 Lock_time: 0.000002 Rows_sent: 2 Rows_examined: 1174797
SET timestamp=1668579956;
SELECT * FROM t_user WHERE name = '[email protected]';

关闭慢查询日志

永久关闭:修改my.cnf配置文件

临时关闭:

1
SET GLOBAL slow_query_log=off;

删除慢查询日志文件

1
2
3
4
# 删除 
rm -rf 87e36d56e6fe-slow.log
# 重置,再生成人文件
mysqladmin -uroot -p flush-logs slow

查看SQL执行成本

SHOW PROFILE;是MySQL提供的可以用来分析当前会话中SQL都做了什么、执行的资源消耗情况的工具,可用于SQL调优的测量。默认情况下处于关闭状态,并保存最近15次的运行结果。

1
2
3
4
-- 查看回话级开关状态
SHOW VARIABLES LIKE 'profiling';
-- 修改状态
SET profiling=on;

查看相关的查询语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
SHOW PROFILES;
+----------+------------+------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+------------------------------------------------------+
| 1 | 0.00489300 | SHOW VARIABLES LIKE 'profiling' |
| 2 | 0.46527275 | SELECT * FROM t_user WHERE name = '[email protected]' |
+----------+------------+------------------------------------------------------+
2 rows in set, 1 warning (0.02 sec)

-- 获取具体的查询语句
SHOW PROFILE FOR QUERY 2;
+--------------------------------+----------+
| Status | Duration |
+--------------------------------+----------+
| starting | 0.000306 |
| Executing hook on transaction | 0.000015 |
| starting | 0.000025 |
| checking permissions | 0.000018 |
| Opening tables | 0.000538 |
| init | 0.000089 |
| System lock | 0.000123 |
| optimizing | 0.000053 |
| statistics | 0.000278 |
| preparing | 0.000079 |
| executing | 0.463503 |
| end | 0.000026 |
| query end | 0.000008 |
| waiting for handler commit | 0.000054 |
| closing tables | 0.000023 |
| freeing items | 0.000112 |
| cleaning up | 0.000023 |
+--------------------------------+----------+
17 rows in set, 1 warning (0.01 sec)

-- 查看 CPU时间和等待时间
SHOW PROFILE CPU,BLOCK IO FOR QUERY 2;
+--------------------------------+----------+----------+------------+--------------+---------------+
| Status | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out |
+--------------------------------+----------+----------+------------+--------------+---------------+
| starting | 0.000306 | 0.000107 | 0.000193 | 0 | 0 |
| Executing hook on transaction | 0.000015 | 0.000003 | 0.000010 | 0 | 0 |
| starting | 0.000025 | 0.000010 | 0.000016 | 0 | 0 |
| checking permissions | 0.000018 | 0.000008 | 0.000010 | 0 | 0 |
| Opening tables | 0.000538 | 0.000541 | 0.000000 | 0 | 0 |
| init | 0.000089 | 0.000088 | 0.000000 | 0 | 0 |
| System lock | 0.000123 | 0.000123 | 0.000000 | 0 | 0 |
| optimizing | 0.000053 | 0.000053 | 0.000000 | 0 | 0 |
| statistics | 0.000278 | 0.000278 | 0.000000 | 0 | 0 |
| preparing | 0.000079 | 0.000077 | 0.000000 | 0 | 0 |
| executing | 0.463503 | 0.498843 | 0.000000 | 0 | 0 |
| end | 0.000026 | 0.000024 | 0.000000 | 0 | 0 |
| query end | 0.000008 | 0.000009 | 0.000000 | 0 | 0 |
| waiting for handler commit | 0.000054 | 0.000054 | 0.000000 | 0 | 0 |
| closing tables | 0.000023 | 0.000023 | 0.000000 | 0 | 0 |
| freeing items | 0.000112 | 0.000111 | 0.000000 | 0 | 0 |
| cleaning up | 0.000023 | 0.000023 | 0.000000 | 0 | 0 |
+--------------------------------+----------+----------+------------+--------------+---------------+
17 rows in set, 1 warning (0.00 sec)

可查看的查询参数:

ALL:所有开销信息

BLOCK IO:显示块IO开销

CONTEXT SWITCHES:上下文切换开销

CPU:显示CPU开销

IPC:显示发送和接收开销

MEMORY:内存开销

PAGE FAULTS:页面错误开销

SOURCE:显示和Source_funtion,Source_file,Source_line相关的开销

SWAPS:显示交换次数开销信息

日常开发需要注意的结论:

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

如果在 SHOW PROFILE诊断结果中出现了以上4条任意一条,则sql语句需要优化。

注意:

不过 SHOW PROFILE 命令将要被弃用,这些信息可以information_schemaprofiling数据表中查看

分析查询语句:EXPLAIN

定位了慢查询的SQL之后,可以使用EXPLAINDESCRIBE工具做针对性的分析查询语句。EXPLAINDESCRIBE使用方法与分析结果是一样的。

MySQL中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为请求的Query提供它认为最优的执行计划。(查看执行计划,而不是执行语句

EXPLAIN支持SELECTUPDATEDELETE

官方文档:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html

image-20221116221025881

列名 描述
id 一个大的查询语句中每个SELECT关键字都对应一个唯一的id
select_type select关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的访问方法
possible_keys 可能用到的索引
key 实际使用的索引
key_len 实际使用到的索引长度
ref 使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要赌球的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息

数据准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE s1(
id INT PRIMARY KEY auto_increment,
sid INT,
`name` VARCHAR(20)
);

CREATE TABLE s2(
id INT PRIMARY KEY auto_increment,
sid INT,
`name` VARCHAR(20)
);

-- 添加外键约束
ALTER TABLE s2 ADD CONSTRAINT s1_id_fk FOREIGN KEY(sid) REFERENCES s1(id);

table 表名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- table 表名 
-- 查询的每一行记录都对应一个表单
EXPLAIN SELECT * FROM s1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

-- 多表,s1:驱动表,s2:被驱动表
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using join buffer (hash join) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
2 rows in set, 1 warning (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
-- id 序列号
-- 大查询中,一个SELECT对应1个id,这里有一个
EXPLAIN SELECT * FROM s1 WHERE id = 1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
1 row in set, 1 warning (0.01 sec)

-- 这里也只有1个
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using join buffer (hash join) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
2 rows in set, 1 warning (0.01 sec)

-- 这里有两个
EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT sid FROM s2) OR sid = 4;
+----+-------------+-------+------------+-------+-------------------+----------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+-------------------+----------+---------+------+------+----------+-------------+
| 1 | PRIMARY | s1 | NULL | ALL | idx_sid_key2_key3 | NULL | NULL | NULL | 5 | 100.00 | Using where |
| 2 | SUBQUERY | s2 | NULL | index | s1_id_fk | s1_id_fk | 5 | NULL | 1 | 100.00 | Using index |
+----+-------------+-------+------------+-------+-------------------+----------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)

-- 查询优化器可能对涉及子查询的查询语句进行重写,转变为多表查询的操作
EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT sid FROM s2 WHERE name = 'a');
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------------------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | PRIMARY | NULL | NULL | NULL | 5 | 100.00 | NULL |
| 1 | SIMPLE | s2 | NULL | ALL | s1_id_fk | NULL | NULL | NULL | 1 | 100.00 | Using where; FirstMatch(s1); Using join buffer (hash join) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------------------------------------+
2 rows in set, 1 warning (0.01 sec)

-- UNION 去重,因此,需要有一个临时的表,记录重复的数据
EXPLAIN SELECT id,sid,name FROM s1 UNION (SELECT id,sid,name FROM s2);
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 100.00 | NULL |
| 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 3 | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)

-- 此时UNION ALL 获取所有的s2,不需要去重
EXPLAIN SELECT id,sid,name FROM s1 UNION ALL SELECT id,sid,name FROM s2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 100.00 | NULL |
| 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
2 rows in set, 1 warning (0.01 sec)
  • id如果相同,可以认为是一组,从上往下顺序执行
  • 在所有组中,id值越大,优先级越高,越先执行
  • id号每个号码,表示一次独立的查询,一个SQL的查询次数越少越好

select_type 下查询在大查询中的角色

select_type 每个SELECT关键字代表一个小的查询语句,这个小查询在大查询中的角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
-- 不含UNION或者子查询的查询都算作是SIMPLE
EXPLAIN SELECT * FROM s1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

-- 连接查询也算是SIMPLE类型
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using join buffer (hash join) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------------------------+
2 rows in set, 1 warning (0.01 sec)

-- 包含UNION 或者 UNION ALL 或者子查询的大查询来说,时由几个小查询组成,其中左边的,就是PRIMARY
-- 包含UNION 或者 UNION ALL 的大查询来说,它是有几个小查询组成的,其中除了最左边的那个小查询,其余的小查询就是UNION
-- 使用临时表来完成UNION查询的去重,针对临时表的查询就是UNION RESULT
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 3 | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)

EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
2 rows in set, 1 warning (0.01 sec)

-- 如果包含子查询的查询语句不能转为对应的多表连接的形式,并且子查询是不相关子查询
-- 该子查询的第一个SELECT关键字代表的查询是SUBQUERY
-- 例如两个字段没有任何联系
use mitaka;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

EXPLAIN SELECT * FROM employee WHERE emp_id IN (SELECT dept_id FROM department) OR lname = 'a';
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | PRIMARY | employee | NULL | ALL | NULL | NULL | NULL | NULL | 17 | 100.00 | Using where |
| 2 | SUBQUERY | department | NULL | index | PRIMARY | PRIMARY | 2 | NULL | 3 | 100.00 | Using index |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

-- 如果包含子查询的查询语句不能转为对应的多表连接的形式,并且子查询是相关子查询
-- 该子查询的第一个SELECT关键字代表的查询是 DEPENDENT SUBQUERY
EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR name = '12';
+----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+
| 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | s2 | NULL | unique_subquery | PRIMARY | PRIMARY | 4 | func | 1 | 100.00 | Using index |
+----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

-- DEPENDENT SUBQUERY 的查询可能会被执行多次

-- 在包含 UNION 或者 UNION ALL 的大查询中,如果各个小查询都依赖于外层查询的话
-- 除了最左边的小查询之外,其余小查询的类型是 DEPENDENT UNION
EXPLAIN SELECT * FROM s1 WHERE name IN (SELECT `name` FROM s2 WHERE name = 'a' UNION SELECT `name` FROM s1 WHERE `name` = 'a');
+----+--------------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 3 | DEPENDENT UNION | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 4 | UNION RESULT | <union2,3> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
4 rows in set, 1 warning (0.01 sec)

-- 对于包含派生表的查询,该派生表对应的子查询的类型就是 DERIVED,例如 子查询出的零时表就是派生
EXPLAIN SELECT * FROM (SELECT `name`,COUNT(*) AS c FROM s1 GROUP BY `name`) AS derived_s1 WHERE c > 2;
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 2 | DERIVED | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using temporary |
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
2 rows in set, 1 warning (0.01 sec)

-- 当查询优化器在执行包含子查询的语句时,选择将子查询物化之后与外层查询进行连接查询时,属性是 MATERIALIZED
EXPLAIN SELECT * FROM employee WHERE lname IN (SELECT `name` FROM department); -- 子查询转为了物化表
+----+--------------+-------------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+-------------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| 1 | SIMPLE | <subquery2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | 100.00 | NULL |
| 1 | SIMPLE | employee | NULL | ALL | NULL | NULL | NULL | NULL | 17 | 10.00 | Using where; Using join buffer (hash join) |
| 2 | MATERIALIZED | department | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | NULL |
+----+--------------+-------------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
3 rows in set, 1 warning (0.00 sec)

partitions 分区命中情况

可略,代表分区表中的命中情况,非分区表,该项为NULL。(将表创建在不同的区中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 按照id分区,id < 100 p0分区 ,其他 p1 分区
CREATE TABLE user_partitions (
id INT,`name` VARCHAR(25))
PARTITION BY RANGE(id)(
PARTITION p0 VALUES less than(100),
PARTITION p1 VALUES less than MAXVALUE
);

DESC SELECT * FROM user_partitions WHERE id > 200;
+----+-------------+-----------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | user_partitions | p1 | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
+----+-------------+-----------------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

type 访问类型

重要,代表MySQL对某个表的执行查询时的访问方法,又称访问类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
-- 当表中只有一条记录,并且该表使用的存储引擎的统计数据时精确的,比如 MyISAM,Memory
-- 那么对表的访问就是system,性能最高
-- MyISAM 会记录条目数
CREATE TABLE t(i INT) ENGINE = myisam;
INSERT INTO t VALUES(1);
EXPLAIN SELECT * FROM t;
+----+-------------+-------+------------+--------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | t | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+--------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

-- 再插入一条就会变成最差的ALL
-- 如果换成innodb,就会变成ALL,因为InnoDB没有精确统计,需要计算个数
CREATE TABLE t1(i INT) ENGINE = INNODB;
INSERT INTO t1 VALUES(1);
EXPLAIN SELECT * FROM t1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

-- 根据主键或者 唯一二级索引列 与常数进行等值匹配时,对单表的访问方法就是const
INSERT INTO s1(sid,`name`) VALUES(1,'a');
EXPLAIN SELECT * FROM s1 WHERE id = 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

-- 创建唯一二级索引
CREATE UNIQUE INDEX un_sid ON s1(sid);
EXPLAIN SELECT * FROM s1 WHERE sid = 1;
+----+-------------+-------+------------+-------+---------------+--------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | const | un_sid | un_sid | 5 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+--------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

-- 在连接查询中,如果被驱动表时通过主键或者唯一二级索引等值列匹配的方式进行访问的,则对该被驱动表的访问方法就是 eq_ref
-- 如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id;
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ALL | PRIMARY | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | SIMPLE | s2 | NULL | eq_ref | PRIMARY | PRIMARY | 4 | dbtest.s1.id | 1 | 100.00 | NULL |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

-- 当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法可能是ref
CREATE INDEX normal_idx ON s1(`name`);
EXPLAIN SELECT * FROM s1 WHERE `name` = 'a';
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ref | normal_idx | normal_idx | 83 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

-- 如果查询过程,需要通过转换,例如数字到字符串的隐式转换,访问方法会变成 ALL
EXPLAIN SELECT * FROM s1 WHERE `name` = 1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | normal_idx | NULL | NULL | NULL | 1 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 3 warnings (0.00 sec)

-- 当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值是,那么访问方法可能是 ref_or_null
EXPLAIN SELECT * FROM s1 WHERE `name` = 'a' OR `name` IS NULL;
+----+-------------+-------+------------+-------------+---------------+------------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------------+---------------+------------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | s1 | NULL | ref_or_null | normal_idx | normal_idx | 83 | const | 2 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------------+---------------+------------+---------+-------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

-- index_merge 单表访问方法时,某些场景下可以使用 intersection union sort-union 这三种索引合并的方式来执行查询
ALTER TABLE s1 ADD COLUMN key2 VARCHAR(100);
CREATE INDEX idx_key2 ON s1(key2);
ALTER TABLE s1 ADD COLUMN key3 VARCHAR(100);
CREATE INDEX idx_key3 ON s1(key3);
INSERT INTO s1(`name`,key2,key3) VALUES('c','c','c');
-- 此时使用到了 索引合并
EXPLAIN SELECT * FROM s1 WHERE key2 = 'c' OR key3 = 'c';
+----+-------------+-------+------------+------+-------------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-------------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | idx_key2,idx_key3 | NULL | NULL | NULL | 2 | 75.00 | Using where |
+----+-------------+-------+------------+------+-------------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

-- 改成 and 时,就变长 ref
EXPLAIN SELECT * FROM s1 WHERE key2 = 'c' AND key3 = 'c';
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ref | idx_key2,idx_key3 | idx_key2 | 403 | const | 1 | 50.00 | Using where |
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

-- unique_subquery 针对一些包含IN子句的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到的主键进行等值匹配的话,
-- 那么该子查询执行计划的type列的值就是 unique_subquery
EXPLAIN SELECT * FROM s1 WHERE sid IN (SELECT id FROM s2 WHERE s1.sid = s2.sid) OR key3 = 'c';
+----+--------------------+-------+------------+-----------------+------------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------+------------+-----------------+------------------+---------+---------+------+------+----------+-------------+
| 1 | PRIMARY | s1 | NULL | ALL | idx_key3 | NULL | NULL | NULL | 2 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | s2 | NULL | unique_subquery | PRIMARY,s1_id_fk | PRIMARY | 4 | func | 1 | 100.00 | Using where |
+----+--------------------+-------+------------+-----------------+------------------+---------+---------+------+------+----------+-------------+
2 rows in set, 2 warnings (0.01 sec)

-- range 如果使用索引获取某些 范围区间 的记录,那么就可能使用到 range 访问方法
EXPLAIN SELECT * FROM s1 WHERE sid IN (1,3,4,5);
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | s1 | NULL | range | un_sid | un_sid | 5 | NULL | 4 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)

EXPLAIN SELECT * FROM s1 WHERE sid > 3 AND sid < 10;
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | s1 | NULL | range | un_sid | un_sid | 5 | NULL | 1 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

-- index 如果可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是 index
CREATE INDEX idx_sid_key2_key3 ON s1(sid,key2,key3);
DROP INDEX idx_key3 ON s1;
-- 使用了联合索引idx_sid_key2_key3,但是还会扫描全部的索引记录
-- 索引覆盖:使用了联合索引,而且不需要回表操作,例如本次查询过程通过联合索引,但是扫描了全部的索引记录,在索引记录中包含key2,不需要回表再查,这个就是覆盖索引
EXPLAIN SELECT key2 FROM s1 WHERE key3='a';
+----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | s1 | NULL | index | idx_sid_key2_key3 | idx_sid_key2_key3 | 811 | NULL | 2 | 50.00 | Using where; Using index |
+----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)

-- 如果查询 name,则没有用到联合索引,就变成all
EXPLAIN SELECT `name`,key2 FROM s1 WHERE key3='a';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

-- ALL 全表扫描
EXPLAIN SELECT * FROM s1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

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

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

其中比较重要是的 system const eq_ref ref range index ALL

SQL性能优化的目标:至少达到range级别,要求是ref级别,最好是const级别。

possible_keyskey 使用索引情况

可能使用到的索引和真实使用的索引。一般查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。key代表实际用到的索引,如果为NULL,则没有使用索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EXPLAIN SELECT * FROM s1 WHERE `name` > 'z' AND key2 = 's';
+----+-------------+-------+------------+------+---------------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ref | normal_idx,idx_key2 | idx_key2 | 403 | const | 1 | 50.00 | Using where |
+----+-------------+-------+------------+------+---------------------+----------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

-- 查询优化器计算成本时,使用 idx_key2
EXPLAIN SELECT * FROM s1 WHERE `name` > 'z' OR key2 = 's';
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | normal_idx,idx_key2 | NULL | NULL | NULL | 2 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
-- 如果是 OR ,则两个索引都会使用到

key_len 实际使用的索引长度

单位字节数,检查是否充分的利用上了索引,越大越好。主要针对联合索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- 主键索引
EXPLAIN SELECT * FROM s1 WHERE id = 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

-- 联合索引情况下,越大越好,需要数据
EXPLAIN SELECT * FROM s1 WHERE sid = 2 AND key2 = 'c' AND key3 = 'c';
+----+-------------+-------+------------+-------+-----------------------------------+--------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+-----------------------------------+--------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | const | un_sid,idx_key2,idx_sid_key2_key3 | un_sid | 5 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+-----------------------------------+--------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

-- 没有使用上索引 (没有满足最左匹配原则)
EXPLAIN SELECT * FROM s1 WHERE key3 = 'c';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
-- 主键索引
-- key_len = 4,
EXPLAIN SELECT * FROM s1 WHERE id = 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

-- 联合索引情况下,越大越好,
EXPLAIN SELECT * FROM s1 WHERE sid = 1 AND key2 = 'c' AND key3 = 'c';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE noticed after reading const tables |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------+
1 row in set, 1 warning (0.01 sec)

-- 没有使用上索引
EXPLAIN SELECT * FROM s1 WHERE key3 = 'c';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

-- 使用普通二级索引,长度 83 = varchar(20) * utf8mb4 >> 20 * 4 = 80 >> + 2 可变长字段 + 1 允许为NULL
EXPLAIN SELECT * FROM s1 WHERE `name` = 'a';
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ref | normal_idx | normal_idx | 83 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

ref 等值匹配

当使用索引列等值查询时,与索引列进行等值匹配的对象信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 比如使用常数 const 
EXPLAIN SELECT * FROM s1 WHERE `name` = 'a';
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | s1 | NULL | ref | normal_idx | normal_idx | 83 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

-- 或者使用某列 dbtest.s2.sid
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.sid = s2.sid;
+----+-------------+-------+------------+--------+--------------------------+--------+---------+---------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+--------------------------+--------+---------+---------------+------+----------+-------------+
| 1 | SIMPLE | s2 | NULL | ALL | s1_id_fk | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 1 | SIMPLE | s1 | NULL | eq_ref | un_sid,idx_sid_key2_key3 | un_sid | 5 | dbtest.s2.sid | 1 | 100.00 | NULL |
+----+-------------+-------+------------+--------+--------------------------+--------+---------+---------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

-- 或者使用的函数 func
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.sid = UPPER(s2.sid);
+----+-------------+-------+------------+--------+--------------------------+--------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+--------------------------+--------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | SIMPLE | s1 | NULL | eq_ref | un_sid,idx_sid_key2_key3 | un_sid | 5 | func | 1 | 100.00 | Using index condition |
+----+-------------+-------+------------+--------+--------------------------+--------+---------+------+------+----------+-----------------------+
2 rows in set, 1 warning (0.00 sec)

Rowsfiltered 预估条目数和过滤条目数

rows 表示预估的需要读取的记录条数。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- rows,越少越好
EXPLAIN SELECT * FROM s1 WHERE `name` > 'z';
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | s1 | NULL | range | normal_idx | normal_idx | 83 | NULL | 1 | 100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

-- filtered 100.00 代表数据 100% 满足要求,
-- 单表扫描,则计算除使用索引的搜索条件之外,其他搜索条件的记录有多少条
EXPLAIN SELECT * FROM s1 WHERE `name` > 'z' AND key2 = 'a';
+----+-------------+-------+------------+------+---------------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | s1 | NULL | ref | normal_idx,idx_key2 | idx_key2 | 403 | const | 1 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------------+----------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

-- 对于单表filtered意义不大,在多表查询时,更关注驱动表对应的执行计划记录的 filtered ,决定了被驱动表要执行的次数,即 rows * filtered
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.`name` = s2.`name` WHERE s1.key3 = 'a';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | SIMPLE | s1 | NULL | ALL | normal_idx | NULL | NULL | NULL | 3 | 33.33 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)
-- 1 * 100 % = 1 , 将 表s2中的数据给到s1进行查询

extra 额外信息

重要,额外信息可以用于更准确理解MySQL到底如何执行给定的查询语句。MySQL提供的额外信息有好几十个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
-- 没有使用表  No tables used 
EXPLAIN SELECT 1;
-- Impossible WHERE,where子句永远为FALSE
EXPLAIN SELECT * FROM s1 WHERE 1 != 1;

-- Using where 使用全表扫描来执行某个表的查询,并且该语句的where子句中有针对该表的搜索条件时
EXPLAIN SELECT * FROM s1 WHERE key3 = 'a';
-- Using where 使用索引访问来执行某个表的查询,并且语句的 where 子句有针对该表的搜索条件之外,还有其他的搜索条件时
EXPLAIN SELECT * FROM s1 WHERE `name` = 'a' AND key3 = 'a';

-- No matching min/max row 当查询列表有 MIN 或者 MAX 聚合函数,但是并没有符合 WHERE 子句中的搜索条件的记录时
EXPLAIN SELECT MIN(`name`) FROM s1 WHERE name = 'abc';

-- Select tables optimized away 查询时有值
EXPLAIN SELECT MIN(`name`) FROM s1 WHERE name = 'a';

-- Using index 当查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下
EXPLAIN SELECT `name` FROM s1 WHERE `name`='a';
EXPLAIN SELECT `name`,id FROM s1 WHERE `name`='a';

-- Using index condition 条件索引,也就是索引下推,搜索条件中出现了索引列,但是不能使用到索引
EXPLAIN SELECT * FROM s1 WHERE `name` > 'z' AND `name` LIKE '%a';

-- Using where; Using join buffer (hash join) 连接查询时,被驱动表不能有效的利用索引加快访问速度,MySQL会为其分配一块叫 JOIN Buffer 的内存块来加快查询速度,也就是 基于块的嵌套循环算法
DROP INDEX normal_idx ON s1; -- 删除 name 上的索引
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.`name` = s2.`name`;

-- Using where; Not exists; Using join buffer (hash join)
-- 当使用作为连接时,where子句包含要求被驱动表的某个列等于 NULL 值的搜索条件
-- 而且那个列又是不允许存储 NULL 值的,那么在该表的执行计划的EXTRA列会出现 not exist
EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.`name` = s2.`name` WHERE s2.id IS NULL;

-- 如果执行计划的 extra 列出现 using intersct(...) 的提示,说明准备使用 intersect 索引合并的方式执行查询
-- 括号中的 ... 表时需要进行索引合并的索引名称
-- 如果出现了 using union(...) 提示,说明准备使用 union 索引合并的方式执行查询
-- 出现了 Using sort_union(...) 提示,说明准备使用 sort_union 索引合并的方式查询
-- 例如 Using union(idx_key4,idx_key2); Using where ,使用两个索引,也就是说会用索引合并
EXPLAIN SELECT * FROM s1 WHERE key4 = 'a' OR key2 = 'a';

-- LIMIT 子句是0,表示不打算从表中读取任何记录,Zero limit
EXPLAIN SELECT * FROM s1 LIMIT 0;

-- 有一些情况下对结果集中的记录进行排序时可以使用索引的
EXPLAIN SELECT * FROM s1 ORDER BY id LIMIT 10;
-- Using index
EXPLAIN SELECT key4 FROM s1 ORDER BY key4 LIMIT 10;
-- Using filesort 很多情况下操作排序无法使用索引,只能在内存(数据少)或者磁盘(数据多)中排序,这种情况称为文件排序 filesort
-- 如果某个查询需要使用文件排序的方式进行查询,就会在执行计划中出现 Using filesort , 这种情况性能会低
EXPLAIN SELECT id FROM s1 ORDER BY `name` LIMIT 10;

-- 在许多查询的执行过程中,MySQL可能会借助临时表来完成一些功能,比如去重、排之类的
-- 如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询
-- Using temporary 表示查询中使用到了内部的临时表
EXPLAIN SELECT DISTINCT `name` FROM s2;
-- 还例如
EXPLAIN SELECT `name`,COUNT(*) AS amount FROM s1 GROUP BY `name`;
-- Using temporary 不是一个好的用法,建立和维护临时表需要付出很大的代价,索引最好使用索引来替代使用临时表
-- 比如扫描指定的索引列
EXPLAIN SELECT key4,COUNT(*) AS amount FROM s1 GROUP BY key4;

小结

  • EXPLAIN 不考虑各种Cache
  • EXPLAIN 不能显示MySQL执行查询时所做的优化工作
  • EXPLAIN 不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响
  • 部分统计信息是估算的,并非准确值

EXPLAIN进一步使用

输出格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-- 传统格式 
EXPLAIN SELECT 1;
-- JSON 格式,JSON格式可以输出信息最详尽的格式,包含了执行的成本信息
EXPLAIN FORMAT=JSON SELECT `name`,COUNT(*) AS amount FROM s1 GROUP BY `name`;
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "0.75" -- 查询成本
},
"grouping_operation": {
"using_temporary_table": true,
"using_filesort": false,
"table": {
"table_name": "s1",
"access_type": "ALL",
"rows_examined_per_scan": 5, -- rows
"rows_produced_per_join": 5, -- rows * filters
"filtered": "100.00",
"cost_info": {
"read_cost": "0.25", -- 由两部分组成,IO成本和 rows * (1 - filter) 条记录的 CPU 成本
"eval_cost": "0.50", -- rows * filter 条记录的成本
"prefix_cost": "0.75", -- 上面两个成本相加,如果有多表,则是第一张表的时间加上上面两个时间
"data_read_per_join": "6K" -- 读取数据量
},
"used_columns": [ -- 查询的字段
"id",
"name"
]
}
}
}
} |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.01 sec)

-- Tree格式 表示 多表 之间的关系
EXPLAIN FORMAT=TREE SELECT * FROM s1 LEFT JOIN s2 ON s1.`name` = s2.`name` WHERE s2.id IS NULL;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Filter: (s2.id is null) (cost=0.83 rows=5)
-> Hash antijoin (s2.`name` = s1.`name`) (cost=0.83 rows=5)
-> Table scan on s1 (cost=0.75 rows=5)
-> Hash
-> Table scan on s2 (cost=0.07 rows=1)
|
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

-- 可视化输出,需要 MySQL Workbench 可视化工具

查看警告信息

使用EXPLAIN语句查看某个查询的执行计划之后,还可以使用SHOW WARNINGS;查看执行计划有关的一些扩展信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ALTER TABLE s2 ADD COLUMN key4 VARCHAR(100);
EXPLAIN SELECT s1.`name`,s2.`name` FROM s1 LEFT JOIN s2 ON s1.`name` = s2.`name` WHERE s2.key4 IS NOT NULL;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
| 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where | -- 将 s2 作为驱动表,s1作为被驱动表成本低
| 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 20.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
2 rows in set, 1 warning (0.01 sec)

SHOW WARNINGS;
*************************** 1. row ***************************
Level: Note -- 告警等级
Code: 1003 -- 1003是很常见的Code,代表Message字段展示的信息类似于查询优化器将查询语句优化后的执行语句
Message: /* select#1 */ select `dbtest`.`s1`.`name` AS `name`,`dbtest`.`s2`.`name` AS `name` from `dbtest`.`s1` join `dbtest`.`s2` where ((`dbtest`.`s1`.`name` = `dbtest`.`s2`.`name`) and (`dbtest`.`s2`.`key4` is not null))
1 row in set (0.01 sec)

分析优化器执行计划 :trace

optimizer_trace是MySQL 5.6 引入的一项跟踪功能,可以跟踪优化器做出的各种决策(比如访问表的方法、计算开销、各种转换等),并将跟踪结果记录到 information_schema.OPTIMIZER_TRACE 表中。

此功能默认关闭。

1
2
3
4
5
SHOW VARIABLES LIKE '%optimizer_trace%';
-- 打开 optimizer_trace,并将格式设置为json
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
USE mitaka;
SELECT * FROM employee WHERE emp_id < 10 ;
SELECT * FROM information_schema.OPTIMIZER_TRACE\G
*************************** 1. row ***************************
QUERY: SELECT * FROM employee WHERE emp_id < 10 -- 查询语句
TRACE: { -- 跟踪信息
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `employee`.`emp_id` AS `emp_id`,`employee`.`fname` AS `fname`,`employee`.`lname` AS `lname`,`employee`.`start_date` AS `start_date`,`employee`.`end_date` AS `end_date`,`employee`.`superior_emp_id` AS `superior_emp_id`,`employee`.`dept_id` AS `dept_id`,`employee`.`title` AS `title`,`employee`.`assigned_branch_id` AS `assigned_branch_id` from `employee` where (`employee`.`emp_id` < 10)"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`employee`.`emp_id` < 10)",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(`employee`.`emp_id` < 10)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "(`employee`.`emp_id` < 10)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "(`employee`.`emp_id` < 10)"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": {
} /* substitute_generated_columns */
},
{
"table_dependencies": [
{
"table": "`employee`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [
{
"table": "`employee`",
"range_analysis": {
"table_scan": {
"rows": 18,
"cost": 4.15
} /* table_scan */,
"potential_range_indexes": [
{
"index": "PRIMARY",
"usable": true,
"key_parts": [
"emp_id"
] /* key_parts */
},
{
"index": "fk_e_emp_id",
"usable": true,
"key_parts": [
"superior_emp_id",
"emp_id"
] /* key_parts */
},
{
"index": "fk_dept_id",
"usable": true,
"key_parts": [
"dept_id",
"emp_id"
] /* key_parts */
},
{
"index": "fk_e_branch_id",
"usable": true,
"key_parts": [
"assigned_branch_id",
"emp_id"
] /* key_parts */
}
] /* potential_range_indexes */,
"setup_range_conditions": [
] /* setup_range_conditions */,
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
} /* group_index_range */,
"skip_scan_range": {
"potential_skip_scan_indexes": [
{
"index": "PRIMARY",
"usable": false,
"cause": "query_references_nonkey_column"
},
{
"index": "fk_e_emp_id",
"usable": false,
"cause": "query_references_nonkey_column"
},
{
"index": "fk_dept_id",
"usable": false,
"cause": "query_references_nonkey_column"
},
{
"index": "fk_e_branch_id",
"usable": false,
"cause": "query_references_nonkey_column"
}
] /* potential_skip_scan_indexes */
} /* skip_scan_range */,
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "PRIMARY",
"ranges": [
"emp_id < 10"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": true,
"using_mrr": false,
"index_only": false,
"in_memory": 1,
"rows": 9,
"cost": 1.16247,
"chosen": true
},
{
"index": "fk_e_emp_id",
"chosen": false,
"cause": "no_valid_range_for_this_index"
},
{
"index": "fk_dept_id",
"chosen": false,
"cause": "no_valid_range_for_this_index"
},
{
"index": "fk_e_branch_id",
"chosen": false,
"cause": "no_valid_range_for_this_index"
}
] /* range_scan_alternatives */,
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */,
"chosen_range_access_summary": {
"range_access_plan": {
"type": "range_scan",
"index": "PRIMARY",
"rows": 9,
"ranges": [
"emp_id < 10"
] /* ranges */
} /* range_access_plan */,
"rows_for_plan": 9,
"cost_for_plan": 1.16247,
"chosen": true
} /* chosen_range_access_summary */
} /* range_analysis */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`employee`",
"best_access_path": {
"considered_access_paths": [
{
"rows_to_scan": 9,
"access_type": "range",
"range_details": {
"used_index": "PRIMARY"
} /* range_details */,
"resulting_rows": 9,
"cost": 2.06247,
"chosen": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 9,
"cost_for_plan": 2.06247,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {
"original_condition": "(`employee`.`emp_id` < 10)",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`employee`",
"attached": "(`employee`.`emp_id` < 10)"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"finalizing_table_conditions": [
{
"table": "`employee`",
"original_table_condition": "(`employee`.`emp_id` < 10)",
"final_table_condition ": "(`employee`.`emp_id` < 10)"
}
] /* finalizing_table_conditions */
},
{
"refine_plan": [
{
"table": "`employee`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": {
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0 -- 丢失的超出最大容量的字节,如果最大内存过小,可能出现丢失
INSUFFICIENT_PRIVILEGES: 0 -- 缺失权限,当不具备权限时,该列信息为1,且 TRACE字段为空
1 row in set (0.02 sec)

MySQL监控分析图: sys schema

information_schemaperformance_schema中的数据以最容易理解的方式总结归纳为视图,成为sys.schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SHOW TABLES IN sys LIKE '%schema%';
+---------------------------------------+
| Tables_in_sys (%schema%) |
+---------------------------------------+
| innodb_buffer_stats_by_schema |
| schema_auto_increment_columns |
| schema_index_statistics |
| schema_object_overview |
| schema_redundant_indexes |
| schema_table_lock_waits |
| schema_table_statistics |
| schema_table_statistics_with_buffer |
| schema_tables_with_full_table_scans |
| schema_unused_indexes |
| x$innodb_buffer_stats_by_schema |
| x$ps_schema_table_statistics_io |
| x$schema_flattened_keys |
| x$schema_index_statistics |
| x$schema_table_lock_waits |
| x$schema_table_statistics |
| x$schema_table_statistics_with_buffer |
| x$schema_tables_with_full_table_scans |
+---------------------------------------+
18 rows in set (0.01 sec)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 索引信息
-- 查询冗余索引
-- 例如联合索引和单列索引冗余
SELECT * FROM sys.schema_redundant_indexes;
-- 查询未使用过的索引
SELECT * FROM sys.schema_unused_indexes;
-- 查询索引的使用情况
SELECT * FROM sys.schema_index_statistics;

-- 表信息
-- 查询表的访问量
SELECT * FROM sys.schema_index_statistics;
-- 查询占用 buffer pool 较多的表
SELECT * FROM sys.innodb_buffer_stats_by_table;
-- 查询表的全表扫描情况
SELECT * FROM sys.statements_with_full_table_scans;

-- 语句相关
-- 监控 SQL 执行频率 , 监控使用临时表或者磁盘临时表 SQL
SELECT * FROM sys.statement_analysis;
-- 监控使用了排序的SQL
SELECT * FROM sys.statements_with_sorting;

风险提示:通过sys库查询时,MySQL会消耗大量资源收集相关信息,严重可能导致业务请求被阻塞,因此建议不要频繁查询sysinformation_schemaperformance_schema表来完成监控、巡检等工作。

索引优化与查询优化

数据库调优的维度有这么几个:

  • 索引失效、没有充分利用索引 – 建立索引
  • 关联查询太多JOIN – SQL优化
  • 服务器调优各个参数设置(缓冲、线程数等) – 调整 my.cnf
  • 数据过多 – 分库分表

查询优化可以分为两大块:

  • 物理查询优化,通过索引表连接方式进行优化
  • 逻辑查询优化,通过SQL等价变换提升查询效率

索引失效案例

索引提供高效访问数据的方法,并且加快查询的速度。

  • 使用索引可以快速地定位表中的某条记录,从而提高数据库查询的速度,提高数据库的性能
  • 如果查询时没有使用索引,查询语句会扫描表中所有的记录。数据量大的情况下,这样的查询速度会很慢

默认情况下采用B+树构建索引。只是空间列类型的索引适用R-树,并且Memory表支持hash索引。

使用或是不适用索引,取决于优化器。优化器基于cost开销explain json格式下的cost),而不是基于规则,也不是基于语义。SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
 -- 全值匹配
-- 没有使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11;
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11 AND name = '[email protected]';
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11 AND name = '[email protected]' AND create_time = '2022-11-16 05:47:16' ;
-- 创建索引
CREATE INDEX id_age ON t_user(age);
CREATE INDEX id_age_name ON t_user(age,name);
CREATE INDEX id_age_name_ctime ON t_user(age,name,create_time);
-- 使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11;
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11 AND name = '[email protected]';
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11 AND name = '[email protected]' AND create_time = '2022-11-16 05:47:16' ;

-- 最佳左前缀匹配,在联合索引中,优先匹配最左侧的索引列
-- 过滤条件要使用索引必须按照索引建立时的顺序,一次满足,一旦跳过某个字段,索引后面的字段都无法被使用
-- 使用到 id_age_name_ctime
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11 AND name = '[email protected]';
-- 没有匹配上索引,由于没有创建 name 的索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE update_time ='2022-11-16 05:47:16' AND name = '[email protected]';

-- 主键插入顺序
-- InnoDB在没有显示创建索引时,表中的数据实际上存储在聚簇索引的叶子节点。而记录又是存放在数据页中,数据页和记录按照主键值从小到大的顺序排序。
-- 如果插入的记录的主键值一次增大,那么每插满一个数据页就换到下一个数据页继续插。
-- 如果插入的主键值忽大忽小,就可能出现插入到老的页,当老的页数据满了,则需要将当前页分裂成两个页,把本页中的一些记录移动到新创建的页中,会造成新能损耗
-- 针对这种情况,主键可以 auto_increment 属性

-- 计算、函数、类型转换,导致索引失效
-- 创建索引
CREATE INDEX idx_name ON t_user(`name`);
-- 使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` LIKE 'abc%';
-- 没有使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE LEFT(`name`,3) = 'abc';
-- 使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE id = 10000;
-- 没使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE id + 1 = 10000;

-- 类型转换
-- 这种查询语句会将 整形 进行类型转换,使用到了函数,因此没有使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` = 123;
-- 使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` = '123';

-- 范围条件右边的列索引失效
-- 范围右侧的列不能使用,< <= > >= 和 BETWEEN .
-- 因此,创建联合索引,务必把范围(时间、金额)放在索引后面
-- 删除除了主键之外的索引
-- 创建索引
CREATE INDEX idx_age_ctime_name ON t_user(age,create_time,name);
-- 使用到索引 ,使用到了 age字段 和 create_time 字段 ,没有使用到 `name` 字段,这是由于使用了范围字段
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 11 AND create_time > '2022-11-16 05:47:16' AND `name` = '[email protected]';
-- 要使用到索引,就需要将等值关系的索引放到前面
CREATE INDEX idx_age_name_ctime ON t_user(age,name,create_time);
-- 使用到idx_age_name_ctime的全部字段
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 20 AND create_time > '2022-11-16' AND `name` = 'abc';

-- 不等于 索引失效
CREATE INDEX idx_name ON t_user(`name`);
-- 没有使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` <> 'abc';
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` != 'abc';

-- IS NULL 可以使用索引
-- IS NOT NULL 无法使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` IS NULL;
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` IS NOT NULL;
-- 因此,最好在设计数据表时,就将字段设置为 NOT NULL 约束。INT类型默认值0,字符串类型默认值空字符串''
-- 同理,NOT LIKE 也一样无法使用索引,会全表扫描

-- LIKE 以 %开头索引失效
-- 用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` LIKE 'abc%';
-- 不用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE `name` LIKE '%abc';

-- OR 前后存在非索引的列,索引失效
-- 删除除主键索引其他的索引
-- 创建索引
CREATE INDEX idx_age ON t_user(age);
-- 索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 10 OR `name` = 'abc';
-- 如果要使用索引,则要将另外一个字段也创建索引
CREATE INDEX idx_name ON t_user(name);
-- 此时会使用索引集合 Using union(idx_age,idx_name); Using where
EXPLAIN SELECT SQL_NO_CACHE * FROM t_user WHERE age = 10 OR `name` = 'abc';

-- 数据库和表的字符集统一使用utf8mb4
-- 不同的字符集在进行比较前,会进行隐式转换,使用到函数就会造成索引时效

Index(a,b,c)

image-20221120150048266

一般性建议

  • 单列索引中,尽量选择针对当前query过滤性更好的索引
  • 联合索引中,当前query中过滤性最好的字段越靠前越好
  • 联合索引中,尽量选择能够包含当前query中的where子句中更多字段的索引
  • 联合索引中,如果某个字段可能出现范围查询,尽量把这个字段放在后面

关联查询优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-- 外连接  
-- 没有使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM employee LEFT JOIN department ON employee.dept_id = department.dept_id;
-- 添加索引
CREATE INDEX idx_dept ON department(dept_id);
-- 被驱动表有索引之后,使用被驱动表的索引。(驱动表不需要索引,因为要全表扫描)(字段类型如果不一样,会有隐式转换,会导致索引失效)
EXPLAIN SELECT SQL_NO_CACHE * FROM employee LEFT JOIN department ON employee.dept_id = department.dept_id;

-- 内连接
EXPLAIN SELECT SQL_NO_CACHE * FROM employee INNER JOIN department ON employee.dept_id = department.dept_id;
-- 添加索引
CREATE INDEX idx_dept ON department(dept_id);
CREATE INDEX idx_dept ON employee(dept_id);
-- 此时两个表的权重一样,可能驱动表和被驱动表的角色会互换
-- 查询优化器会根据表中的数据量,会选择数据量少的表作为驱动表,数据量大的座位被驱动表
EXPLAIN SELECT SQL_NO_CACHE * FROM employee INNER JOIN department ON employee.dept_id = department.dept_id;
-- 删除 department 的索引
DROP INDEX idx_dept ON department;
EXPLAIN SELECT SQL_NO_CACHE * FROM employee INNER JOIN department ON employee.dept_id = department.dept_id;
-- 对于内连接,如果表中的连接条件只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表



-- JOIN 语句原理
-- JOIN 方式连接多表,本质是各个表之间的数据的循环匹配。如果关联表的数据量很大,JOIN 关联的执行时间会很长。
-- 在MySQL 5.5之前,只支持嵌套循环算法 (nested loop join)
-- 在MySQL 通过引入 BNLJ 算法来优化嵌套执行
-- 所谓驱动表,是指优化器优化之后的选择
-- 驱动表就是主表,被驱动表就是外表
-- 内连接可能会换角色,外连接也会
CREATE TABLE a(f1 INT,f2 INT, INDEX(f1)) ENGINE = INNODB;
CREATE TABLE b(f1 INT,f2 INT)ENGINE=INNODB;
INSERT INTO a VALUES(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
INSERT INTO b VALUES(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);
-- 此时 b 是驱动表,a是被驱动表,尽管是 a LEFT JOIN b ,实际上,查询优化器是将外连接改成了一个内连接
EXPLAIN SELECT * FROM a LEFT JOIN b ON a.f1=b.f1 WHERE a.f2=b.f2;
-- 此时还是一个外连接
EXPLAIN SELECT * FROM a LEFT JOIN b ON a.f1=b.f1 AND a.f2=b.f2;
-- 内连接,b是驱动表,a是被驱动表
EXPLAIN SELECT * FROM a INNER JOIN b ON a.f1=b.f1 WHERE a.f2=b.f2;

Simple Nested-Loop Join (简单嵌套循环连接)

从表A去除一条数据1,遍历表B,将匹配到的数据放到result,以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断。性能最差。

image-20221120192228914

两个表都没有索引,开销如下:

开销统计 SNLJ
外表扫描次数 1
内表扫描次数 A
读取记录数 A+B*A
JOIN比较次数 B*A
回表读取记录数 0

Index Nested-Loop Join (索引嵌套循环连接)

Index Nested-Loop Join优化的思路主要是为了减少内存表数据的匹配次数,所以要求被驱动表上必须有索引。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极少的减少了匹配次数。

image-20221120192606329

驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,所以MySQL优化器都倾向于使用记录数少的表作为驱动表(外表)。

开销统计 SNLJ INLJ
外表扫描次数 1 1
内表扫描次数 A 0
读取记录数 A+B*A A+B(match)
JOIN比较次数 B*A A*Index(Height)
回表读取记录数 0 B(match)(if possible)

被驱动表加索引,效率会非常高,但如果索引不是主键索引,那么还需要一次回表查询。相比,被驱动表的索引是主键索引,效率会更高。

Block Nested-Loop Join (块嵌套循环连接)

如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数就会跟多,退化成SNLJ。相对于SNLJ每次查询都要将被驱动表的数据加载到内存一次,然后从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录再加载到内存匹配,周而复始,大大增加了IO的次数。为了减少被驱动表IO次数,就出现了Block Nested-Loop Join

不再是逐条获取驱动表的数据,而是一块一块的获取,引入了 join buffer 缓冲区,将驱动表join相关的部分数据列(大小受join buffer限制)缓存到 join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录记录一次性和 join buffer中的所有驱动表匹配记录进行匹配(内存中操作),将简单嵌套循环中的偶次比较合并成一次,降低了被驱动表的访问频率。

这里缓存的不只是关联表的列,select后面的列也会缓存起来。

在一个有N个join关联的sql中会分配N-1个 join buffer。所以查询的时候尽量减少不必要的字段,可以让 join buffer中可以存放更多的列。

image-20221120193819265

开销统计 SNLJ INLJ BNLJ
外表扫描次数 1 1 1
内表扫描次数 A 0 A * used colume size / join buffer size +1
读取记录数 A+B*A A+B(match) A + B * (A * used colume size / join buffer size )
JOIN比较次数 B*A A*Index(Height) B * A
回表读取记录数 0 B(match)(if possible) 0

参数设置

1
2
3
4
-- 开关
SHOW VARIABLES LIKE '%optimizer_switch%';
-- buffer size ,默认 256k
SHOW VARIABLES LIKE '%join_buffer%';

Join小结

  1. 整体效率比较,INLJ > NMLJ > SNLJ

  2. 用小结果集驱动大结果集(本质就是减少外层循环的数据数量)

    1
    2
    3
     -- STRAIGHT_JOIN 让查询优化器不变更位置 
    SELECT t1.b,t2.* FROM t1 STRAIGHT_JOIN t2 ON (t1.b=t2.b) WHERE t2.id <= 100; -- 推荐
    SELECT t1.b,t2.* FROM t2 STRAIGHT_JOIN t1 ON (t1.b=t2.b) WHERE t2.id <= 100; -- 不推荐
  3. 为被驱动表匹配的条件增加索引(减少内层循环次数)

  4. 增加 join buffer 的大小(一次缓存的数据越多,内存包的扫表次数就越少)

  5. 减少驱动表不必要的字段查询(字段越少,join buffer 所缓存的数据就越多)

Hash Join

MySQL的8.0.20版本开始废弃BNLJ,因为从MySQL 8.0.18版本开始,加入了hash join,默认都会使用hash join

  • Nested Loop:对于被连接的数据子集较小的情况,Nested Loop是个较好的选择。
  • Hash Join:是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立 散列表,然后扫描较大的表并探测散列,找出与hash表匹配的行。

子查询优化

子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作,但是执行效率不高,这是由于:

  1. 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询从临时表中查询记录。查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
  2. 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会收到一定的影响
  3. 对于返回结果集比较大的子查询,对查询性能的印象也就越大。

MySQL中,可以使用JOIN查询来替代子查询。连接查询不需要建立临时表,比子查询要快,如果查询中用到索引,性能会更好。

1
2
3
4
 -- 子查询
EXPLAIN SELECT * FROM employee emp WHERE emp.dept_id IN (SELECT dept_id FROM department dep WHERE `name` IS NOT NULL);
-- 改造成多表查询
EXPLAIN SELECT emp.* FROM employee emp JOIN department dep ON emp.dept_id = dep.dept_id WHERE dep.`name` IS NOT NULL;

尽量不要使用 NOT IN 或者NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL 替代。

排序优化

MySQL中,支持两种排序方式,分别是FileSortIndex排序。

  • Index排序,索引可以保证数据的有序性,不需要再进行排序,效率更高
  • FileSort排序一般在内存中进行排序,占用CPU较多。如果待排结果较大,会产生临时IO到磁盘进行排序的情况,效率较低

优化建议:

  • SQL中,在WHERE子句和ORDER BY子句中使用索引,目的是WHERE子句中避免全表扫描,在ORDER BY子句避免使用FileSort排序。某些情况下全表扫描或者FileSort排序不一定比索引慢,但是绝大部分索引更快
  • 尽量使用Index完成ORDER BY排序。如果WHEREORDER BY后面是相同的列就是用单索引列,如果不同,就是用联合索引
  • 无法使用Index时,需要对FileSort方式进行调优
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
-- ORDER BY 没有 LIMIT 索引失效 
EXPLAIN SELECT SQL_NO_CACHE * FROM employee ORDER BY fname,lname;
EXPLAIN SELECT SQL_NO_CACHE * FROM employee ORDER BY fname,lname LIMIT 10;
-- 创建索引
CREATE INDEX idx_fname_lname ON employee(fname,lname,title);
-- ORDER BY 时不 limit ,索引失效
-- Using filesort,由于需要获取全部字段,要回表,因此不会使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM employee ORDER BY fname,lname;
-- Using index 此时不回表,这种情况下使用索引
EXPLAIN SELECT SQL_NO_CACHE fname,lname FROM employee ORDER BY fname,lname;
-- 数据量大时,limit 会使用到索引,数据量不大,不会使用到索引
EXPLAIN SELECT SQL_NO_CACHE * FROM employee ORDER BY fname,lname LIMIT 2;

-- ORDER BY 顺序错误,索引失效
-- 创建索引
CREATE INDEX idx_dept_branch_id ON employee(dept_id,assigned_branch_id,emp_id);
CREATE INDEX idx_dept_branch_fname ON employee(dept_id,assigned_branch_id,fname);
-- 索引失效
EXPLAIN SELECT * FROM employee ORDER BY assigned_branch_id LIMIT 10;
EXPLAIN SELECT * FROM employee ORDER BY assigned_branch_id,fname LIMIT 10;
-- 使用索引
EXPLAIN SELECT * FROM employee ORDER BY dept_id,assigned_branch_id,emp_id LIMIT 10;
EXPLAIN SELECT * FROM employee ORDER BY dept_id,assigned_branch_id LIMIT 10;
EXPLAIN SELECT * FROM employee ORDER BY dept_id LIMIT 10;

-- ORDER BY时,规则不一致,索引失效(顺序错,不索引;方向反,不索引)
EXPLAIN SELECT * FROM employee ORDER BY dept_id DESC,assigned_branch_id ASC LIMIT 10;
EXPLAIN SELECT * FROM employee ORDER BY assigned_branch_id DESC,fname DESC LIMIT 10;
-- 使用 dept_id 正排序,assigned_branch_id降序,性能可能还不如直接内存排序
EXPLAIN SELECT * FROM employee ORDER BY dept_id ASC,assigned_branch_id DESC LIMIT 10;
-- 使用逆序索引,反着索引反着获取
EXPLAIN SELECT * FROM employee ORDER BY dept_id DESC,assigned_branch_id DESC LIMIT 10;

-- 无过滤,不索引
-- 数据量多的时候,才会使用索引;过滤之后数据不多,还是会使用内存排序
EXPLAIN SELECT * FROM employee WHERE dept_id = 10 ORDER BY assigned_branch_id;
EXPLAIN SELECT * FROM employee WHERE dept_id = 10 ORDER BY assigned_branch_id,fname;
-- 索引失效
EXPLAIN SELECT * FROM employee WHERE assigned_branch_id = 10 ORDER BY dept_id;
-- LIMIT 使用索引 ,这里会先使用 dept_id 排序,然后获取 assigned_branch_id 为10 的,取前 5个,数据量不大,会使用索引
EXPLAIN SELECT * FROM employee WHERE assigned_branch_id = 10 ORDER BY dept_id LIMIT 5;

使用FileSortindex排序的选择,会根据实际需要排序的数据量,如果数据量很大,会使用FileSort,如果此时使用limit,则会使用index排序。如果排序的数据量很少,也会使用FileSort而不用index

  1. 两个索引同时存在,MySQL会自动选择最优方案,随着数据量的变化,选择的索引也会随之变化
  2. 范围条件group by或者order by的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上,反之亦然。

FileSort算法:双路排序和单路排序

双路排序(慢):MySQL 4.1之前的使用方式;两次扫描磁盘,第一次获取需要排序的列,内存中排序好之后,再扫描磁盘获取具体的详细信息。

单路排序(快):一次扫描磁盘查询需要的所有列,在内存中进行排序。

单路排序虽然快,但是对内存占用更多,当内存空间少时,可能会造成多次随机IO

1
2
3
4
5
-- 方案1、提高sort_buffer_size
-- 获取 sort_buffer_size 大小
SHOW VARIABLES LIKE '%sort_buffer_size%';
-- 方案二、提高
SHOW VARIABLES LIKE '%max_length_for_sort_data%';

优化策略除了修改上述两个变量,还可以在语法上。order byselect * 是一个大忌,加载更多内容会消耗buffer,可能导致多次随机IO

GROUP BY优化

  • GROUP BY是用索引的原则几乎跟 ORDER BY一直,GROUP BY即使没有过滤条件用到索引,也可以直接使用索引。
  • GROUP BY先排序,再分组,按照索引建的最佳左前缀法则
  • 当无法使用索引列,增大max_length_for_sort_datasort_buffer_size参数
  • where效率高于having,能写在where限定的条件就不要写在having
  • 减少使用order by,能不排序就不排序,或将排序放在程序端去做。order by,group by,distinct这些语句较为耗费CPU
  • 包含了order by,group by,distinct这些查询的语句,where条件过滤出来的结果集最好保持在1000行以内,否则SQL会很慢

优化分页查询

一般分页查询时,通过创建覆盖索引可以比较好的提高性能。

1
2
3
4
5
6
7
-- 这种情况性能比较差,需要排序前 2000000记录,但是仅仅返回 2000000-2000010的记录,其他记录丢弃,
-- 将2000000条记录都放在内存中进行排序,排序之后,只取 10 条
EXPLAIN SELECT * FROM employee LIMIT 2000000,10;
-- 优化思路1、使用主键索引
EXPLAIN SELECT * FROM employee e,(SELECT emp_id FROM employee ORDER BY emp_id LIMIT 2000000,10) a WHERE e.emp_id = a.emp_id;
-- 优化思路2、适用于主键自增的表,将limit查询转换成某个位置的查询
EXPLAIN SELECT * FROM employee WHERE emp_id > 20000000 LIMIT 10;

优先考虑覆盖索引

覆盖索引:一个索引包含了满足查询结果的数据就叫做覆盖索引。或者说是非聚簇索引的一种形式,包括在查询里的SELECTJOINWHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。简单来说,就是索引列+主键包含**SELECTFROM之间查询的列**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 例如创建联合索引 
CREATE INDEX idx_fname_title_branch ON employee(fname,title,assigned_branch_id);
-- 查询时,获取索引包含的字段
EXPLAIN SELECT SQL_NO_CACHE fname,title,assigned_branch_id FROM employee ORDER BY fname,title;
-- 或者再加上主键id
EXPLAIN SELECT SQL_NO_CACHE fname,title,assigned_branch_id,emp_id FROM employee ORDER BY fname,title;
-- 但是如果加上不在索引中的字段,则无法使用索引,还是会回表
EXPLAIN SELECT SQL_NO_CACHE fname,title,assigned_branch_id,emp_id,lname FROM employee ORDER BY fname,title;
DROP INDEX idx_fname_title_branch ON employee;

-- 当 != 不使用索引
CREATE INDEX idx_branch_fname ON employee(assigned_branch_id,fname);
-- 不使用索引,这是由于使用索引然后回表比不使用索引的开销多
EXPLAIN SELECT SQL_NO_CACHE * FROM employee WHERE assigned_branch_id <> 20;
-- 使用索引,这是由于使用索引可以不回表
EXPLAIN SELECT SQL_NO_CACHE assigned_branch_id,fname FROM employee WHERE assigned_branch_id <> 20;

-- LIKE 前面 % 不使用索引
EXPLAIN SELECT * FROM employee WHERE fname LIKE '%mi';
-- 不回表,使用索引
EXPLAIN SELECT emp_id,assigned_branch_id FROM employee WHERE fname LIKE '%mi';

覆盖索引的好处:

  • 避免InnoDB表进行索引的回表
  • 可以把随机IO变成顺序IO加快查询效率 (回表时,访问的是不同的区,不同的页,是随机IO,而使用索引,则是顺序IO

弊端:主要是维护索引字段。

索引下推

Index Condition PushdownICP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 索引下推 
CREATE INDEX idx_fname ON employee(fname);
-- 首先通过 > 条件查询,查询出结果后,不回表,而是直接使用索引 再进行判断 LIKE ,最后再回表,这个就是索引下推,下推到下一个条件
EXPLAIN SELECT * FROM employee WHERE fname > 'z' AND fname LIKE '%m';
-- 索引下推主要针对联合索引
CREATE INDEX idx_dept_fname_lname ON employee(dept_id,fname,lname);
-- Using index condition; Using where
-- 先通过索引 dept_id 查询,然后查询出来之后,索引中包含 后续fname的判断 内容,最后筛选出一部分之后,再进行回表判断 第三个不在索引中的条件,使用 where
EXPLAIN SELECT * FROM employee WHERE dept_id = 2 AND fname LIKE '%m%' AND title LIKE '%h%';

-- 开启和关闭ICP
SHOW VARIABLES LIKE '%optimizer_switch%';
SET optimizer_switch = 'index_condition_pushdown=on';
SET optimizer_switch = 'index_condition_pushdown=off';

ICP使用条件

  • 表访问的类型为rangerefeq_refref_or_null可以使用ICP
  • ICP可以用于InnoDBMyISAM表,包括分区表InnoDBMyISAM
  • 对于InnoDBICP仅用于二级索引,ICP的目标是减少全行读取次数,从而减少IO操作
  • SQL使用覆盖索引时,不支持ICP,因为这种情况下使用ICP不会减少IO
  • 相关子查询的条件不能使用ICP

其他查询优化策略

EXISTSIN的区分

1
2
3
4
5
-- 使用 IN 还是 EXISTS 主要取决于数据量,小表驱动大表,这种方式效率最高
-- A表大,B表小,是用这种方式,先获取B中数据,然后使用A表
SELECT * FROM A WHERE cc IN (SELECT cc FROM B);
-- A表小,B表大,通过A表驱动B表
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc = A.cc);

count(*)count(具体字段)效率

1
2
3
SELECT COUNT(*);
SELECT COUNT(1);
SELECT COUNT(id);

环节1:

COUNT(*)SELECT COUNT(1)都是对所有结果进行COUNT,这两个本质上没有区别。如果有WHERE子句,则是对所有符合条件的数据进行统计;如果没有WHERE子句,则是对所有数据行数进行统计。

环节2:

​ 如果是MyISAM存储引擎,统计数据表的行数只需要o(1)的复杂度,因为每张MyISAM的数据表都有一个meta信息存储了row_count值,而一致性则由表级锁来保证。

​ 如果是InnoDB存储引擎,因为InnoDB支持事务,采取行级锁和MVCC机制,所以无法想MyISAM一样维护一个row_count,因此需要采用扫描全表,进行循环+计数的方式来完成统计。

环节3:

​ 在InnoDB中,如果采用COUNT(具体字段)来统计,尽量采用二级索引,因为主键是聚簇索引,包含信息明显大于二级索引。对于count(*)count(1),他们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。如果没有二级索引,才会采用主键索引。

对于SELECT(*)

表查询中,建议明确字段,不要通过*作为查询的字段。

  1. 解析过程中,会通过查询数据字典将*按序转换成列名,会大大的耗费资源和时间
  2. 无法使用覆盖索引

LIMIT 1对优化的影响

如果是针对全表扫描,如果确定结果集只有1条,加上LIMIT 1的时候,找到一条记录就不会继续扫描,这样会加快速度。

如果这个字段建立了唯一索引,那么可以通过索引进行查询,此时LIMIT 1就不会产生优化。

多使用COMMIT

COMMIT会释放相关资源:

  • 回滚段上用于恢复数据的信息
  • 被程序语句获得的锁
  • redo / undo log buffer中的空间
  • 管理上述3种资源中的内部花费

大数据量设置主键

自增ID的问题:

  1. 可靠性不高:存在自增ID回溯的问题(例如插入一个主键是10000的数据,后面的数据会从10001开始),在MySQL 8.0中修复
  2. 安全性不高:对外暴露的信息,会通过id猜测数据量,容易被爬取
  3. 性能差:自增id需要在数据库服务端生成
  4. 交互多:获取最近的最大主键id需要执行last_insert_id()的函数,海量并发的系统中,开销会很大
  5. 局部唯一性:只有当前表唯一,在多个表中无法唯一

业务字段做主键

一般将一个非空的唯一字段作为主键。

而且,尽量不要用跟业务有关的字段做主键,例如电话号码、身份证号、会员id(会员id被回收后用于其他用户,可能导致数据错误)。

例如,在淘宝的订单业务中,订单表的主键,是订单ID,可以看做是如下方式生成:

1
订单ID = 时间戳 + 去重字段 + 用户ID后6位

这样设计能够做到全局唯一,并且对分布式系统查询及其友好。

推荐的主键设计

非核心业务:对应表的主键自增ID,如告警、日志、监控等信息

核心业务:主键设计至少应该是全局唯一且是单调递增的,全局唯一性保证在各个系统之间都是唯一的,单调递增是插入时不影响数据库性能。

UUID

这里推荐最简单的一种主键设计:UUID。UUID全局唯一,占用36字节,数据无序,插入性能差。

MySQL数据库的UUID组成如下:

1
2
3
4
5
6
7
8
-- UUID = 时间+UUID版本(16字节) - 时钟序列(4字节)-MAC地址(12字节)
SELECT UUID() FROM DUAL;
+--------------------------------------+
| UUID() |
+--------------------------------------+
| d33335b4-6977-11ed-bfd5-0242ac110002 |
+--------------------------------------+
1 row in set (0.02 sec)

image-20221121164532058

时间的低-中-高位的排序,实现无序。

通过时间戳和MAC地址保证全局唯一。

占用36字节是由于中间插入4个 -

改造UUID

将时间位互换,时间高-中-低,改造成单调递增。将字符串中的 -去掉,并且将字符串用二进制类型保存,将存储空间降低到16个字节

1
2
3
4
5
6
7
8
9
-- MySQL 8.0中 
SET @uuid = UUID();
SELECT @uuid,uuid_to_bin(@uuid),uuid_to_bin(@uuid,TRUE);
+--------------------------------------+----------------------------------------+--------------------------------------------------+
| @uuid | uuid_to_bin(@uuid) | uuid_to_bin(@uuid,TRUE) |
+--------------------------------------+----------------------------------------+--------------------------------------------------+
| 6e93c787-6979-11ed-bfd5-0242ac110002 | 0x6E93C787697911EDBFD50242AC110002 | 0x11ED69796E93C787BFD50242AC110002 |
+--------------------------------------+----------------------------------------+--------------------------------------------------+
1 row in set (0.01 sec)

通过函数 uuid_to_bin(@uuid)转换成二进制,通过 uuid_to_bin(@uuid,TRUE)转换成单调递增。

将16字节有序UUID和8字节自增ID进行性能比较:

image-20221121165245511

可以看到,插入1亿条数据的性能使用UUID是最快的,而且在实际业务中使用UUID可以在业务端生成。

推荐阅读:

详解 慢查询 之 mysqldumpslow