当我们遇到数据库调优问题的时候,该如何思考呢?这里把思考的流程整理成下面这张图。整个流程划分成了 观察(Show status)
和 行动(Action)
两个部分。字母 S 的部分代表观察(会使用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)。
小结:
在 MySQL中,可以使用 SHOW STATUS
语句查询一些数据库服务器的运行情况,如:性能参数 、 执行频率等。
备注:
1、show status 是由数据库自行维护的,作用是记录系统的运行情况,用户不可修改。
2、与 show status 类似的有一个 show variables 描述的是系统的一些系统变量,这些用户是可以控制的,用来调整系统的一些情况
SHOW STATUS语句语法如下:
SHOW [GLOBAL|SESSION] STATUS LIKE '参数';
一些常用的性能参数如下:
举例:
show status like 'Uptime';
关于慢日志,作者写了另外的文章,这里不做过多介绍,《5.4.5 The Slow Query Log(慢日志实验).md》
show profile 的作用是查看sql消耗的资源,也就是通常所说的成本。
show variables like 'profiling';
通过设置 profiling='ON'
来开启 show profile:
set profiling = 'ON';
然后执行相关的查询语句。接着看下当前会话都有哪些 profiles,使用下面这条命令:
show profiles;
你能看到当前会话一共有 2 个查询。查看某一次查询的资源使用情况使用:
show profile cpu,block io for query 2;
show profile
在新的版本中标注过期了,官方建议大家使用性能库,即performance_schema
使用语法:
EXPLAIN SELECT select_options
如果我们想看看某个查询的执行计划的话,可以在具体的查询语句前边加一个 EXPLAIN ,就像这样:
EXPLAIN SELECT 1;
EXPLAIN
语句输出的各个列的作用如下:
列名 | 描述 |
---|---|
id |
在一个大的查询语句中每个SELECT关键字都对应一个 唯一的id |
select_type |
SELECT关键字对应的那个查询的类型 |
table |
表名 |
partitions |
匹配的分区信息 |
type |
针对单表的访问方法 |
possible_keys |
可能用到的索引 |
key |
实际上使用的索引 |
key_len |
实际使用到的索引长度 |
ref |
当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows |
预估的需要读取的记录条数 |
filtered |
某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra |
一些额外的信息 |
为了让大家有比较好的体验,我们调整了下 EXPLAIN 输出列的顺序。
1. table:
不论我们的查询语句有多复杂,里边儿 包含了多少个表
,到最后也是需要对每个表进行 单表访问
的,所以MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名(有时不是真实的表名字,可能是简称)。
2. id:
我们写的查询语句一般都以 SELECT 关键字开头,比较简单的查询语句里只有一个 SELECT 关键字,id就可以理解为一个select语句。
3. select_type:
查询类型。有如下表:
名称 | 描述 |
---|---|
SIMPLE | Simple SELECT (not using UNION or subqueries)(只要不是union和子查询) |
PRIMARY | Outermost SELECT(最外面的查询) |
UNION | Second or later SELECT statement in a UNION(union) |
DEPENDENT UNION | Second or later SELECT statement in a UNION, dependent on outer query(union且依赖外部查询) |
UNION RESULT | Result of a UNION.( union之后的结果) |
SUBQUERY | First SELECT in subquery(子查询) |
DEPENDENT SUBQUERY | First SELECT in subquery, dependent on outer query(子查询且依赖外部查询) |
DERIVED | Derived table(派生表) |
DEPENDENT DERIVED | Derived table dependent on another table(派生表且依赖其他表) |
MATERIALIZED | Materialized subquery(物化子查询) |
UNCACHEABLE SUBQUERY | A subquery for which the result cannot be cached and must be re-evaluated for each row of the outer query |
具体分析如下:
SIMPLE
除了子查询和UNION都是simple
EXPLAIN SELECT * FROM s1;
当然,连接查询也算是 SIMPLE 类型,比如:
EXPLAIN SELECT * FROM s1 INNER JOIN s2;
PRIMARY
EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2;
UNION
UNION RESULT
SUBQUERY
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a';
DEPENDENT SUBQUERY
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE s1.key2 = s2.key2) OR key3 = 'a';
DEPENDENT UNION
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2 WHERE key1 = 'a' UNION SELECT key1 FROM s1 WHERE key1 = 'b');
DERIVED
EXPLAIN SELECT * FROM (SELECT key1, count(*) as c FROM s1 GROUP BY key1) AS derived_s1 where c > 1;
MATERIALIZED
EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2);
UNCACHEABLE SUBQUERY
UNCACHEABLE UNION
针对上文提到的type类型,这里重点说明。
EXPLAIN输出的type列描述了如何联接表。以下列表描述了连接类型,按从最佳类型到最差类型的顺序排列:
system
只有一行数据的表,是const的一种特殊情况
CREATE TABLE t(i int) Engine=MyISAM;
INSERT INTO t VALUES(1);
EXPLAIN SELECT * FROM t;
const
常量级别,表中最多只匹配一行且在查询开始的时候就被读取到了。这种情况就是 PRIMARY KEY
或 UNIQUE
。举例:
SELECT * FROM tbl_name WHERE primary_key=1;
-- 右边是常量,左边是主键
SELECT * FROM tbl_name
WHERE primary_key_part1=1 AND primary_key_part2=2;
eq_ref
等值引用。从当前的表读取一行与先前的表匹配。这是除了system、const以外最快的方式,如在 PRIMARY KYE
或 UNIQUE NOT NULL
会使用。举例如下:
SELECT * FROM ref_table,other_table
WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table
WHERE ref_table.key_column_part1=other_table.column
AND ref_table.key_column_part2=1;
ref
引用,跟 eq_ref 不同的是ref可能会匹配多行而eq_ref匹配一行。对于前一个表中的每一个行组合,都会从此表中读取具有匹配索引值的所有行。当键不是PRIMARY key或UNIQUE索引(换句话说,如果联接不能根据键值选择一行),则使用ref。举例:
-- 不是 primary key 或者 unique
SELECT * FROM ref_table WHERE key_column=expr;
SELECT * FROM ref_table,other_table
WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table
WHERE ref_table.key_column_part1=other_table.column
AND ref_table.key_column_part2=1;
fulltext
ref_or_null
这个join type类型跟ref类似,但是可能会包括null。举例:
SELECT * FROM ref_table
WHERE key_column=expr OR key_column IS NULL;
index_merge
此联接类型表示使用了索引合并优化。在查询的列都来自索引时可能会发生。
unique_subquery
用来在子查询中代替 eq_ref。举例:
-- primary_key 是唯一索引
value IN (SELECT primary_key FROM single_table WHERE some_expr)
index_subquery
跟 unique_subquery类似。它代替 IN 子查询,但是它和非唯一索引一起工作。举例:
-- key_column 是非唯一索引
value IN (SELECT key_column FROM single_table WHERE some_expr)
range
范围。使用索引选择给定范围的行的类型就是range。通常在这些情况发生:=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、IN()。举例:
SELECT * FROM tbl_name
WHERE key_column = 10;
SELECT * FROM tbl_name
WHERE key_column BETWEEN 10 and 20;
SELECT * FROM tbl_name
WHERE key_column IN (10,20,30);
SELECT * FROM tbl_name
WHERE key_part1 = 10 AND key_part2 IN (10,20,30);
index
当覆盖索引时一般使用index,该类型跟all差不多效率除了特殊情况外。
all
全表扫描
EXPLAIN输出的Extra列包含有关MySQL如何解析查询的附加信息。以下列表说明了可以在此列中显示的值。以下列举几个常见的
Backward index scan
反向索引扫描
const row not found
Distinct
Full scan on NULL key
Impossible HAVING
不可能的having条件
Impossible WHERE
不可能的where条件
No tables used
unique row not found
Using filesort
Using index
只使用索引树中的信息从表中检索列信息,而不必进行额外的查找来读取实际行。当查询仅使用作为单个索引一部分的列时,可以使用此策略。
Using temporary
为了解决查询,MySQL需要创建一个临时表来保存结果。通常发生在GROUP BY和ORDER BY子句
Using where
有如下的sql语句
EXPLAIN SELECT tt.TicketNumber, tt.TimeIn,
tt.ProjectReference, tt.EstimatedShipDate,
tt.ActualShipDate, tt.ClientID,
tt.ServiceCodes, tt.RepetitiveID,
tt.CurrentProcess, tt.CurrentDPPerson,
tt.RecordVolume, tt.DPPrinted, et.COUNTRY,
et_1.COUNTRY, do.CUSTNAME
FROM tt, et, et AS et_1, do
WHERE tt.SubmitTime IS NULL
AND tt.ActualPC = et.EMPLOYID
AND tt.AssignedPC = et_1.EMPLOYID
AND tt.ClientID = do.CUSTNMBR;
被比较的列如下:
Table | Column | Data Type |
---|---|---|
tt |
ActualPC |
CHAR(10) |
tt |
AssignedPC |
CHAR(10) |
tt |
ClientID |
CHAR(10) |
et |
EMPLOYID |
CHAR(15) |
do |
CUSTNMBR |
CHAR(15) |
表的索引如下:
Table | Column | Data Type |
---|---|---|
tt |
ActualPC |
CHAR(10) |
tt |
AssignedPC |
CHAR(10) |
tt |
ClientID |
CHAR(10) |
et |
EMPLOYID |
CHAR(15) |
do |
CUSTNMBR |
CHAR(15) |
现在用explain分析出来的结果如下:
table type possible_keys key key_len ref rows Extra
et ALL PRIMARY NULL NULL NULL 74
do ALL PRIMARY NULL NULL NULL 2135
et_1 ALL PRIMARY NULL NULL NULL 74
tt ALL AssignedPC, NULL NULL NULL 3872
ClientID,
ActualPC
Range checked for each record (index map: 0x23)
现在应该如何优化?
分析:
1、从执行计划的输出可以看到所有的链接类型都是ALL,这是全表扫描非常地效;从rows列的乘积 74 * 2135 * 74 * 3872 的结果可以看出需要扫描的行的数量将非常多(即使结果只有很少一部分);但是可以看到对表却是建立了索引,那为啥索引没有被使用到?
2、仔细观察发现是因为字段的类型长度不一样,有 char(10) 和 char(15),很明显只能扩长度不能缩小长度。执行如下操作重新观察执行计划
ALTER TABLE tt MODIFY ActualPC VARCHAR(15);
ALTER TABLE tt MODIFY AssignedPC VARCHAR(15),MODIFY ClientID VARCHAR(15);
table type possible_keys key key_len ref rows Extra
et ALL PRIMARY NULL NULL NULL 74
tt ref AssignedPC, ActualPC 15 et.EMPLOYID 52 Using
ClientID, where
ActualPC
et_1 eq_ref PRIMARY PRIMARY 15 tt.AssignedPC 1
do eq_ref PRIMARY PRIMARY 15 tt.ClientID 1
达到这一步基本已经很完美了,索引基本上都使用到了,而且是eq_ref和ref效率都还可以。
3、但是仔细分析发现
在 Extra 列使用过滤条件的列(即第二列)并没有作为驱动表,驱动表是et表(第一行是驱动表)。
优化器预估tt扫描52行,et扫描74行,既然tt表扫描的行少,那应该让tt表作为驱动表
4、执行以下语句让MySQL分析关键字的分布情况(在Oracle中也叫做收集统计信息)
ANALYZE TABLE tt;
5、重新查看执行计划,如下:
table type possible_keys key key_len ref rows Extra
tt ALL AssignedPC NULL NULL NULL 3872 Using
ClientID, where
ActualPC
et eq_ref PRIMARY PRIMARY 15 tt.ActualPC 1
et_1 eq_ref PRIMARY PRIMARY 15 tt.AssignedPC 1
do eq_ref PRIMARY PRIMARY 15 tt.ClientID 1
官网:https://dev.mysql.com/doc/refman/8.0/en/explain-output.html
show status:参考我的文章:《13.7.7.37 SHOW STATUS Statement.md》
理解执行计划:参考我的文章:《8.8.2 EXPLAIN Output Format(explain 输出执行计划的格式).md》
书籍:《InnoDB 存储引擎》,该书电子版书籍作者无套路免费下载
传送门: 保姆式Spring5源码解析
欢迎与作者一起交流技术和工作生活
联系作者
传送门: 保姆式Spring5源码解析
欢迎与作者一起交流技术和工作生活
联系作者