平常在CURD的过程当中,都避免不了跟数据库打交道,大多数业务都离不开数据库表的设计和SQL的编写,那如何让你编写的SQL语句性能更优呢?php
先来总体看下MySQL逻辑架构图:html
MySQL总体逻辑架构图能够分为Server和存储引擎层。mysql
Server层:程序员
Server层涵盖了MySQL的大多数核心服务功能,以及全部的内置函数(如日期、时间、数学和加密函数等),以及存储过程、触发器、视图等跨存储引擎的实现也在这一层来实现。sql
存储引擎层:数据库
负责数据的存储和提取,是一种插件式的架构方式。支持 InnoDB、MyISAM、Memory 等多个存储引擎。MySQL 5.5.5版本开始默认存储引擎是 InnoDB,也是目前经常使用的存储引擎。segmentfault
今天咱们来看下详细看下优化器里的执行计划如何分析,要分析一个 SQL 的执行效率,就要会看执行计划,根据执行计划优化 SQL,使其能达到高效查询的目的。后端
一条查询语句须要通过 MySQL 查询优化器的各类基于成本和规则,优化后会生成一个所谓的执行计划
。缓存
那么这个执行计划主要展现具体执行查询的方式,好比多表链接的顺序是多少,表里包含多个索引,每一个表采用什么访问方法来具体执行查询等。架构
而设计 MySQL 的大佬是很是贴心的,知道开发的朋友们都是亲自写 SQL 的,可是写出 SQL 容易,想写出性能高的 SQL 可不简单。
因此,大佬提供了 Explain
语句来帮咱们查询某个查询语句的具体执行计划。
本文带你们看懂 EXPLAIN
语句,必需要熟悉各项输出是作什么的,从而有针对性的提高SQL 查询语句的性能。
列名 |
用途 |
---|---|
id | 每个SELECT关键字查询语句都对应一个惟一id |
select_type | SELECT关键字对应的查询类型 |
table | 表名 |
partitions | 匹配的分区信息 |
types | 单表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际使用到的索引 |
key_len | 实际使用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估须要读取的记录条数 |
filtered | 某个表通过条件过滤后剩余的记录条数百分比 |
Extra | 额外的一些信息 |
为了方便解释上面的执行计划各项输出的含义,下面建立三张数据库表。
DROP TABLE IF EXISTS user; CREATE TABLE `user` ( `id` int(11) NOT NULL, `name` varchar(45) DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO user (`id`, `name`, `update_time`) VALUES (1,'a','2017-12-22 15:27:18'), (2,'b','2017-12-22 15:27:18'), (3,'c','2017-12-22 15:27:18'); DROP TABLE IF EXISTS `group`; CREATE TABLE `group` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `group` (`id`, `name`) VALUES (1,'group1'),(2,'group2'),(3,'group3'); DROP TABLE IF EXISTS user_group; CREATE TABLE `user_group` ( `id` int(11) NOT NULL, `user_id` int(11) NOT NULL, `group_id` int(11) NOT NULL, `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_group_id` (`group_id`), KEY `idx_user_group_id` (`user_id`,`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO user_group (`id`, `user_id`, `group_id`, `remark`) VALUES (1,1,1,'bak1'), (2,2,2,'bak2'), (3,3,3,'bak3');
下载了最新的 MySQL8.0+ 版本,直接执行 EXPLAIN
,对比了 MySQL 5.0+ 版本执行的 EXPLAIN EXTENDED
命令一样都提供了一些查询优化的信息。除了执行计划各项输出参数外,额外还有 filtered
列,是一个百分比的值,rows * filtered/100
能够估算出将要和 EXPLAIN
中前一个表进行链接的行数 。
以下所示:
EXPLAIN
中的列
接下来咱们将详细说明下 EXPLAIN
执行结果每一列的信息。
一、id 列
设计表时一般会设计 id,通常会做为主键,执行计划的结果也不例外,也有 id 列,id
列编号是 SELECT
的序列号,而且 id 的顺序是按 SELECT
出现的顺序增加的。id列越大执行优先级越高,id 相同则从上往下执行,id 为 NULL 最后执行。
MySQL将 SELECT
查询分为简单查询 SIMPLE
和复杂查询 PRIMARY
。
复杂查询包括:简单子查询、派生表( FROM
语句中的子查询)、UNION
和 UNION ALL
查询。
简单查询:
复杂查询:
1)简单子查询
EXPLAIN SELECT (SELECT 1 from user LIMIT 1) from user
;
2)FROM
子句中的子查询
EXPLAIN SELECT FROM (SELECT id, count() as c from group
GROUP BY name) as derived
这个查询执行时有个临时表别名为 derived
,外部 SELECT
查询引用了这个临时表
3)UNION
和 UNION ALL
查询
EXPLAIN SELECT FROM user UNION SELECT FROM user;
UNION
结果老是放在一个匿名临时表中,临时表不在 SQL 中出现,临时表名为 <union1, 2>
,所以它的 id
是 NULL
,代表这个临时表是为了合并两个查询结果集而建立的。
跟 UNION
对比,UNION ALL
无需为最终结果而去重,仅是单纯的将多个查询结果集中的记录合并成一个并返回给用户,因此不会使用到临时表,故没有 id
为 NULL
记录。以下所示:
EXPLAIN SELECT FROM user UNION ALL SELECT FROM user;
注意点:子查询优化为链接查询
查询优化器可能对子查询进行重写,进而转换为链接查询
,查询计划中的两个id值是相同的,以下所示:
EXPLAIN SELECT * FROM user WHERE id IN (SELECT user_id FROM user_group
);
二、select_type 列
MySQL中优化器中的概念:
物化
:
子查询语句中的子查询结果集中的记录保存到临时表的过程称之为 物化
(英文名:Materialize
),简单理解为存储子查询结果集的临时表称之为 物化表
。
也正由于物化表的记录都创建了索引(基于内存的物化表有哈希索引,基于磁盘的有B+树索引),所以经过 IN
语句判断某个操做数在不在子查询的结果集中变得很快,从而提高语句的性能。
半链接 semi-join
:
也是跟 IN
语句子查询有关。
通用语句:
SELECT ... FROM outer_tables WHERE expr IN (SELECT ... FROM inner_tables ...) AND ...
outer_tables
表对 inner_tables
半链接的意思:
对于
outer_tables 的某条记录来讲,咱们仅关心在
inner_tables 表中是否存在匹配的记录,而不用关心具体有多少条记录与之匹配,最终结果只保留 outer_tables 表的记录
。
每个 SELECT
关键字的查询都定义了一个 select_type
属性,知道这个查询属性就能知道在整个查询语句中所扮演的角色。
1)SIMPLE
:简单查询。查询不包含子查询 和 UNION
。
2)PRIMARY
:复杂查询中最外层的SELECT
,可参照上面的 UNION
查询语句。
3)SUBQUERY
:包含的子查询语句没法转换为 semi-join
,而且为不相关子查询,查询优化器采用物化方案执行该子查询,该子查询的第一个 SELECT
就会 SUBQUERY
。该查询因为被物化,只须要执行一次
。
4)DERIVED
:对于采用物化形式执行的包含派生表的查询,该派生表的对应的子查询为 DERIVED
。
查询语句以下所示:
EXPLAIN SELECT * FROM (SELECT id, count(*) as c FROM user GROUP BY id) AS derived_u where c>1;
5)UNION
:在 UNION
查询语句中的第二个和紧随其后的 SELECT
。
6)UNION RESULT
:MySQL选择使用临时表完成 UNION
查询的去重工做。
当 select_type
为这个值时,常常能够看到table的值是 <unionN,M>
,这说明匹配的 id 行 是这个集合的一部分。请看上面 UNION
查询示例。
7)MATERIALIZED
:当查询优化器执行包含子查询的语句时,选择将子查询物化以后与外层查询进行链接查询时,该子查询类型为 MATERIALIZED
。
8)DEPENDENT SUBQUERY
:包含的子查询语句没法转换为 semi-join
,而且为相关子查询,则该子查询的第一个 SELECT
就会 DEPENDENT SUBQUERY
。该查询可能会被执行屡次
。
8)DEPENDENT UNION
:包含的子查询语句中包含了 UNION
或者 UNION ALL
的大查询,这些查询都依赖外层查询,这些子查询语句类型为 DEPENDENT UNION
。
EXPLAIN SELECT * FROM user WHERE id IN (SELECT user_id FROM user_group WHERE name = 'a' UNION SELECT id FROM user WHERE name = 'b');
上面这个子查询语句中的 SELECT user_id FROM user_group WHERE name = 'a'
这个小查询是第一个子查询,因此它的 select_type
为 DEPENDENT SUBQUERY
,而 SELECT id FROM user WHERE name = 'b'
这个查询在 UNION
后面,因此它的 select_type
为 DEPENDENT UNION
。
最多见的值包括:SIMPLE
、PRIMARY
、DERIVED
、UNION
。
三、table 列
table
列表示 EXPLAIN
的单独行的惟一标识符。这个值多是表名、表的别名或者一个未查询产生临时表的标识符,如派生表、子查询或集合。
当 FROM
子句中有子查询时,若是优化器采用的物化方式,table 列是 <derivenN>
格式,表示当前查询依赖 id=N
的查询,因而先执行 id=N
的查询。
当使用 UNION
查询时,UNION RESULT
的 table 列的值为 <UNION1,2>
,1和2表示参与 UNION
的 SELECT 的行 id。
四、type 列
这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。
依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL
通常来讲,得保证查询达到range级别,最好达到ref
NULL:mysql可以在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,能够单独查找索引来完成,不须要在执行时访问表。
1)system,const
:MySQL 能对查询的某部分进行优化并将其转化成一个常量。用于主键或惟一二级索引列与常数比较时,因此表最多有一个匹配行,读取1次,速度比较快
。system
是 const
的特例,表里只有一条记录匹配时为 system
。
EXPLAIN SELECT FROM (SELECT FROM user where id = 1) tmp;
2)eq_ref
:在链接查询时,若是被驱动表是经过主键或者惟一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是 eq_ref
。这多是在 const 以外最好的联接类型了。
EXPLAIN SELECT * FROM user_group INNER JOIN user ON user_group.user_id = user.id;
3)ref
:相比 eq_ref,不使用惟一索引,而是使用普通索引或者惟一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。
a. 简单 SELECT
查询,name 是普通索引(非惟一索引)。
EXPLAIN SELECT * FROM user where user.name = 'a';
b. 关联表
查询,idx_user_group_id (user_id,group_id)
为联合索引,这里使用到了user_group联合索引最左边前缀 user_id。
EXPLAIN SELECT user_id FROM user LEFT JOIN user_group ON user.id = user_group.user_id;
4)ref_or_null
:对普通二级索引进行等值查询,该索引列也能够为NULL值时。
EXPLAIN SELECT * FROM user where user.name = 'a' OR name IS NULL;
5)index_merge
:MySQL使用索引合并的方式执行的。
EXPLAIN SELECT * FROM user WHERE user.name = 'a' OR user.id = 1;
6)range
:使用索引获取范围区间
的记录,一般出如今 in, between ,> ,<, >=
等操做中。
EXPLAIN SELECT * FROM user WHERE user.id > 1;
7)index
:扫描全表索引,这一般比ALL快一些。(index
是从索引中读取的,而 ALL
是从硬盘中读取)
group
表里的两个字段都有索引。
EXPLAIN SELECT * FROM group
;
8)ALL
:即全表扫描,MySQL 须要从头至尾去查找表中所须要的行。一般状况下这须要增长索引来进行优化了。
EXPLAIN SELECT * FROM user
;
五、possible_keys 列
possible_keys
列表示查询可能使用哪些索引来查找。
EXPLAIN
执行计划结果可能出现 possible_keys
列,而 key
显示 NULL
的状况,这种状况是由于表中数据很少,MySQL 会认为索引对此查询帮助不大,选择了全表查询。
若是 possible_keys
列为 NULL
,则没有相关的索引。在这种状况下,能够经过检查 WHERE
子句去分析下,看看是否能够创造一个适当的索引来提升查询性能,而后用 EXPLAIN
查看效果。
另外注意:不是这一列的值越多越好,使用索引过多,查询优化器计算时查询成本高,因此若是可能的话,尽可能删除那些不用的索引。
六、key 列
key
列表示实际采用哪一个索引来优化对该表的访问。
若是没有使用索引,则该列是 NULL。若是想强制 MySQL使用或忽视 possible_keys
列中的索引,在查询中使用 force index
、ignore index
。
七、key_len 列
key_len
列表示当查询优化器决定使用某一个索引查询时,该索引记录的最大长度。
key_len
列计算规则以下:
char(n):n字节长度
varchar(n):2字节存储字符串长度,若是是utf-8,则长度 3n + 2
注意:该索引列能够存储NULL
值,则key_len
比不能够存储NULL
值时多1个字节。
好比:varchar(50),则实际占用的key_len
长度是 3 * 50 + 2 = 152,若是该列容许存储NULL
,则key_len
长度是153。
tinyint:1字节
smallint:2字节
int:4字节
bigint:8字节
date:3字节
timestamp:4字节
datetime:8字节
索引最大长度是768字节,当字符串过长时,MySQL 会作一个相似左前缀索引的处理,将前半部分的字符提取出来作索引。
举例1:
user_group
表中的联合索引 idx_user_group_id
由 user_id
和 group_id
两个int 列组成,而且每一个 int 是 4 字节。
EXPLAIN SELECT * FROM user_group WHERE user_id = 2;
经过结果中的 key_len=4可推断出查询使用了第一个列:user_id
列来执行索引查找。
举例2:
再看 user
表 name 字段是 varchar(45) 变长字符串类型,key_len
为138 等于 45 * 3 + 2 (变长字节) + 1字节(容许存储NULL值)
EXPLAIN SELECT * FROM user WHERE name = 'a';
因此,之后再看到 key_len
字段的值,不要在懵逼咯,固定套路~
八、ref 列
ref
列显示了在 key
列记录的索引中,表查找值所用到的列或常量,常见的有:const
(常量),字段名
(例:user.id
)。
九、rows 列
rows
列是查询优化器估计要读取并检测的行数,注意这个不是结果集里的行数。
若是查询优化器使用全表扫描查询,rows
列表明预计的须要扫码的行数;
若是查询优化器使用索引执行查询,rows
列表明预计扫描的索引记录行数。
十、filtered 列
对于单表来讲意义不大,主要用于链接查询中。
前文中也已提到 filtered
列,是一个百分比的值,对于链接查询来讲,主要看驱动表
的 filtered
列的值 ,经过 rows * filtered/100
计算能够估算出被驱动表
还须要执行的查询次数。
EXPLAIN SELECT * FROM user INNER JOIN user_group ON user.id = user_group.user_id WHERE user.update_time = '2019-01-01';
能够看到驱动表user
执行的rows列为3行,filtered列为 33.33,计算驱动表的扇出值
为 3 * 33.33% 约等于1,说明还须要对被驱动表执行大约1次查询。
十一、Extra 列
Extra
列提供了一些额外信息。这一列在 MySQL中提供的信息有几十个,这里仅列举一些常见的重要值以下:
1)Using index
:查询的列被索引覆盖,而且 WHERE
筛选条件是索引的前导列,使用了索引性能高。通常是使用了覆盖索引(查询列都是索引列字段)。对于 INNODB 存储引擎来讲,若是是辅助索引性能会有很多提升,而且也不须要回表查询。
2)Using where Using index
:查询的列被索引覆盖,而且 WHERE
筛选条件是索引列之一,但并非索引的前导列,意味着没法直接经过索引查找来查询到符合条件的数据。
3)NULL
:查询的列未被索引覆盖,而且 WHERE
筛选条件是索引的前导列,意味着用到了索引,可是部分字段未被索引覆盖,必须经过 回表
来查询,不是纯粹地用到了索引,也不是彻底没用到索引。
4)Using index condition
:与Using where
相似,查询的列不彻底被索引覆盖,WHERE
条件中是一个前导列的范围。
5)Using temporary
:MySQL 中须要建立一张内部临时表来处理查询,通常出现这种状况就须要考虑进行优化了,首先是想到用索引来优化。
一般在许多执行包括DISTINCT、GROUP BY、ORDER BY等子句查询过程当中,若是不能有效利用索引来完成查询,MySQL颇有可能会寻求创建内部临时表来执行查询。
因此,执行计划中出现了 Using temporary
并非个好兆头,由于创建与维护临时表要付出很大的成本的,要考虑使用索引
来优化改进。
6)Using filesort
:MySQL 会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。此时 MySQL 会根据联接类型浏览全部符合条件的记录,并保存排序关键字和行指针,而后排序关键字并按顺序检索行信息。这种状况下通常也是要考虑使用索引来优化的。
查询中须要使用 filesort
的方式进行排序的记录很是多,那么这个过长是很耗时的,想办法将使用 文件排序
的执行方式改进为使用索引
进行排序。
7)Index merges
:一般显示为Using sort_union(...)
说明准备用 Sort-Union
索引合并方式来查询;显示为 Using union(...)
,说明准备用Union
索引合并方式查询;显示为Using intersect(...)
,说明准备使用Intersect
索引合并方式查询。
8)LooseScan
:在 IN 子查询转为 semi-join
时,若是采用的是 LooseScan
执行策略,则会在Extra
中提示。
9)FirstMatch(tbl_name)
:在 IN 子查询转为 semi-join
时,若是采用的是 FirstMatch
执行策略,则会在Extra
中提示。
10)Using join buffer
:强调了在获取链接条件时没有使用索引,而且须要链接缓冲区来存储中间结果。出现该值,应该注意,根据查询的具体状况可能须要添加索引来改进性能。
咱们所提到的回表
操做 ,实际上是一种随机IO,比较耗时,因此尽可能避免上面提到的回表操做,当发现Extra
提示为 Using filesort
、Using temporary
时就须要格外注意了,考虑索引优化。
staff
表表演使用:# 重建 `staff` 表 DROP TABLE `staff`; CREATE TABLE `staff` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名', `s_name` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '花名', `s_no` INT(4) NOT NULL DEFAULT 0 COMMENT '工号', `work_age` int(11) NOT NULL DEFAULT '0' COMMENT '工龄', `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位', `arrival_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间', `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', # 容许 NULL PRIMARY KEY (`id`), # 主键 UNIQUE KEY idx_s_name (s_name), # 惟一索引 KEY idx_s_no (s_no), # 普通索引 KEY `idx_name_age_position` (`name`,`work_age`,`position`) USING BTREE # 联合索引 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='员工记录表'; # 初始化 `staff` 表数据 INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('zhangsan','zs',10,2,'manager',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('lisi','ls',11,3,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('wangwu','ww',12,8,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('zhangliu','zl',110,5,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('xiaosun','xs',111,5,'dev',NOW()); INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('donggua','dg',200,3,'dev',NOW());
一、全值匹配:
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan';
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age = 2;
EXPLAIN SELECT * FROM staff where name = 'zhangsan' AND work_age = 2 AND position = 'dev';
EXPLAIN SELECT * FROM staff where position = 'dev' AND name = 'zhangsan' AND work_age = 2;
最后一条,咱们将 position
放到了 WHERE
条件后面,尽管没有按照联合索引的顺序编写条件,MySQL 优化器会自动优化,将 name 排到最前面去,因此仍是会正确使用联合索引的。
联合索引建立后,你必须严格按照最左前缀的原理进行使用,不然会没法使用到索引。尽可能按照这个顺序去写,这样避免 MySQL 优化器再次优化了。
二、最佳左前缀法则:
若是索引了多列,要遵照最左前缀法则。指的是查询从索引的最左前列开始而且不跳过索引中的列。
如下 SQL 符合最左前缀匹配法则:
EXPLAIN SELECT * FROM staff WHERE name = 'zhangsan' AND work_age = 3 AND position = 'manager';
EXPLAIN SELECT * FROM staff WHERE name = 'zhangsan' AND position = 'manager';
如下执行都是全表扫描,type
为 ALL
,都不符合最左前缀法则:
EXPLAIN SELECT * FROM staff WHERE work_age = 2 AND position ='dev';
EXPLAIN SELECT * FROM staff WHERE position = 'dev';
三、索引列上避免作计算操做
索引上尽可能避免作函数计算等操做,会致使索引失效而转向全表扫描。
WHERE
条件后面索引列使用函数:
EXPLAIN SELECT * FROM staff WHERE LEFT(name, 5) = 'zhang';
EXPLAIN SELECT * FROM staff WHERE LOWER(name) = 'zhangsan';
EXPLAIN SELECT FROM staff WHERE staff.s_no 2 > 3;
查询的结果 type 列为 ALL
,key 是空的,索引失效,全表扫描。
计算逻辑尽可能放到业务层去处理,最大限度的命中索引,同时还能节省数据库资源开销。
四、存储引擎没法使用索引中范围条件右边的列
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age > 2 AND position ='dev';
咱们看到了执行结果中 type 为 range
级别,使用了范围查找,而 position 字段并无用到索引(没有使用到BTree的索引去查询),只是从 name = 'zhangsan' AND work_age > 2
条件返回的结果集中,再过滤符合 position 字段条件的数据。
五、尽可能使用覆盖索引
覆盖索引:简单理解,只访问建了索引的列。减小使用 SELECT *
语句查询列。
使用了覆盖索引:
EXPLAIN SELECT name,work_age FROM staff WHERE name= 'zhangsan' AND work_age = 3;
使用了 SELECT *
查询:
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age = 3;
咱们重点看下使用了 覆盖索引
方式查询,会在结果中 Extra
列显示 Using index
,这说明在查询列包含了索引列,不须要再次回表查询了。而若是使用 SELECT *
方式查询,查询列包含非索引的列,Extra
显示为 NULL
,因此还会进行回表查询。
附一个曾经线上SQL的优化记录:
artist 表有几十万条的数据量,第一条执行的SQL没有索引直接查询,查询耗时 0.557
毫秒;第一次优化
新建 founded 字段做为普通索引,查询耗时 0.0224
毫秒;第二次优化
再次重建联合索引 founded_name,优化后查询耗时:0.0051
毫秒。由于使用了覆盖索引查询方式,基于此优化,SQL查询效率提高很是明显。
六、范围条件查找可以命中索引
范围条件主要包括 <、<=、>、>=、between
等。
若条件中范围列有普通索引和主键索引同时存在, 优先使用主键索引:
EXPLAIN SELECT * FROM staff WHERE staff.s_no > 10 AND staff.id > 2;
范围列能够用到索引,注意联合索引必须符合最左前缀法则,若是查询条件中有两个范围列则没法全用到索引,优化器会去选择:
EXPLAIN SELECT * FROM staff WHERE staff.name != 'zl' AND staff.s_no > 1;
若条件中范围查询和等值查询同时存在,优先匹配等值查询列的索引:
EXPLAIN SELECT * FROM staff WHERE staff.s_no > 10 AND staff.s_name = 'zl';
七、索引列不为 NULL,IS NOT NULL没法使用索引
索引列建议都使用 NOT NULL 约束
及默认值,单列索引不存 NULL 值,联合索引不存所有为 NULL 的值,若是列容许为 NULL,查询结果可能不符合预期。
staff 表中为 remark
字段新建普通索引:
ALTER TABLE staff ADD INDEX idx_remark (remark);
IS NULL
查询命中索引:
EXPLAIN SELECT * FROM staff WHERE staff.remark IS NULL;
IS NOT NULL
查询不会命中索引:
EXPLAIN SELECT * FROM staff WHERE staff.name IS NOT NULL;
八、模糊条件查询以通配符开头索引失效
like '%xx'
或 like '%xx%'
前导模糊查询不能命中索引:
EXPLAIN SELECT * from staff where name like '%zhang%';
如何使用模拟查询才能命中索引?
a)like 'xx%'
非前导模糊查询能够命中索引:
EXPLAIN SELECT * FROM staff WHERE name LIKE 'zhang%';
b)使用覆盖索引,查询字段必需要创建覆盖索引字段
EXPLAIN SELECT name,work_age FROM staff WHERE name LIKE '%zhang%';
联合索引是 idx_name_work_age_position
九、字符串类型不加单引号索引失效
字符串的数据类型必定要将常量值使用单引号,这个在平常开发中要特别注意的,数据类型出现隐式转换的时候不会命中索引。
不加单引号索引失效
EXPLAIN SELECT * FROM staff WHERE name = 1;
name=1 相似于在该字段上作了一个函数运算,所以不会走索引的。
加单引号会命中索引:
EXPLAIN SELECT * FROM staff WHERE name = 'zhangsan';
十、OR
使用多数状况下索引会失效
EXPLAIN SELECT * FROM staff WHERE name='zhangsan' OR work_age = 2;
尽管 name 和 work_age 是联合索引,可是 work_age 列上并无建索引,因此使用了 OR
不会走索引。
若是 OR
先后都是联合索引带头大哥 name 字段,那么就会用到索引,以下所示:
因 OR
后面的条件列中没有索引,会走全表扫描。存在全表扫描的状况下,就没有必要多一次索引扫描增长IO访问。
可以使用覆盖索引查询:
EXPLAIN SELECT name,work_age FROM staff WHERE name='zhangsan' OR work_age = 2;
OR 后面也使用索引列:
EXPLAIN SELECT * FROM staff WHERE name='zhangsan' OR s_name='wangwu';
s_name 是惟一索引,name是联合索引第一个字段,二者使用 OR
查询结果 Extra
显示 Using sort_union(idx_name_age_position,idx_s_name); Using where
解释一下。
若是执行计划 Extra
列出现了 Using sort_union(...)
的提示,说明准备使用 Sort-Union
索引合并的方式执行查询。若是出现了 Using intersect(...)
的提示,说明准备使用 Intersect
索引合并方式执行查询,若是出现了 Using union(...)
的提示 ,说明准备使用 Union
索引合并方式执行查询。 括号中 ...
表示须要进行索引合并的索引名称。
使用UNION优化改进:
EXPLAIN SELECT FROM staff WHERE name='zhangsan' UNION SELECT FROM staff WHERE s_name = 'zs';
使用 UNION
执行计划中出现了第三条记录,Extra
中出现 Using temporary
,说明 MySQL由于不能有效利用索引,创建了内部临时表来执行查询。当你在使用 DISTINCT 、GROUP BY、UNION
等子句中的查询过程当中,都有可能会出现该扩展信息。
使用UNION ALL进一步优化:
EXPLAIN SELECT FROM staff WHERE name='zhangsan' UNION ALL SELECT FROM staff WHERE s_name = 'zs';
执行结果中再也不出现内部临时表,具体用的时候结合实际需求来定是否使用。
十一、负向查询条件不能使用索引,能够优化为 IN
查询
负向查询条件包括:!=、<>、NOT IN、NOT EXISTS、NOT LIKE
等。
不会命中索引:
EXPLAIN SELECT * FROM staff WHERE s_no !=1 AND s_no != 2;
EXPLAIN SELECT * FROM staff WHERE s_no NOT IN (1,2);
使用IN优化,命中索引:
EXPLAIN SELECT * FROM staff WHERE s_no IN (11,12);
可是使用 IN
命中索引有个前提,是查询条件字段数据区分度要高,一般如:状态、类型、性别之类的字段。
十二、排序对索引的影响
ORDER BY
是常常用的语句,排序也遵循最左前缀列的原则。
查询全部列未命中索引:
EXPLAIN SELECT * FROM staff ORDER BY name,work_age;
覆盖索引查询可命中索引:
覆盖索引可以利用联合索引查询,可是 ORDER BY
后的条件查询不符合最左前缀原则,执行结果 Extra
中出现了 Using filesort
的提示,通常看到这个就要想办法优化了。
调整排序的两个字段顺序以后,Extra
会提示为 Using index
,使用了索引,避免了排序的资源开销:
EXPLAIN SELECT name,work_age FROM staff ORDER BY name,work_age;
1三、局部索引的使用
局部索引,区别于最左列索引(顺序取索引中靠左的列的查询),它只取某列的一部分做为索引。
INNODB存储引擎下,通常是字符串类型,很长,所有做为索引大大增长存储空间,索引也须要维护,对于长字符串,又想做为索引列,可取的办法就是取前一部分(局部),表明一整列做为索引串。
如何确保这个前缀能表明或大体表明这一列?MySQL中有个概念是 索引选择性
,是指索引中不重复的值的数目(也称基数X)与整个表该列记录总数(T)的比值。基数能够经过SHOW INDEX FROM 表名
查看。
好比一个列表 [1,2,2,3,5,6],总数是 6,不重复值数目为 5,选择性为 5/6,所以选择性范围是[X/T, 1],这个值越大,表示列中不重复值越多,越适合做为局部索引,而惟一索引(UNIQUE KEY)的选择性是1。
`SELECT COUNT(DISTINCT(CONCAT(LEFT(remark, N))/COUNT(*) FROM t; 测试出接近 1 的索引选择性,其中N是索引的长度,穷举法去找出N的值,而后再建索引。
建立 局部索引
,使用 remark 字段举个例子
EXPLAIN SELECT * FROM staff where remark LIKE 'xxx%';
对 remark 字段重建局部索引:
ALTER TABLE staff DROP INDEX idx_remark_part, ADD INDEX idx_remark_part(remark(5));
再次执行查询:
EXPLAIN SELECT * FROM staff where remark LIKE 'xxx%';
上面列了大部分场景索引最佳实战,除此以外,不宜建索引的几点小总结:
1)更新很是频繁字段不宜建索引
由于字段更新台频繁,会致使B+树的频繁的变动,重建索引。因此这个过程是十分消耗数据库性能的。
2)区分度不大的字段不宜建索引
好比相似性别这类的字段,区分度不大,创建索引的意义不大。由于不能有效过滤数据,性能和全表扫描至关。另外注意一点,返回数据的比例在 30%
以外的,优化器不会选择使用索引。
3)业务中有惟一特性的字段,建议建成惟一索引
业务中若是有惟一特性的字段,即便是多个字段的组合,也尽可能都建成惟一索引。尽管惟一索引会影响插入效率,可是对于查询的速度提高是很是明显的。此外,还可以提供校验机制,若是没有惟一索引,高并发场景下,可能还会产生脏数据。
4)多表关联时,要确保关联字段上必须有索引
5)建立索引时避免创建错误的认识
索引越多越好,认为一个查询就须要建一个索引。
宁缺勿滥,认为索引会消耗空间、严重拖慢更新和新增速度。
抵制惟一索引,认为业务的惟一性一概须要在应用层经过“先查后插”方式解决。
过早优化,在不了解系统的状况下就开始优化。
6)最佳索引实践口诀
若是你以为上面哪些太啰嗦,有朋友已总结为一套优化口诀,优化SQL时也能提个醒吧。
全值匹配我最爱,最左前缀要遵照;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围以后全失效;
Like百分写最右,覆盖索引不写星;
不等空值还有or,索引失效要少用;
VAR引号不可丢,SQL高级也不难!
7)EXPLAIN
执行计划实践总结
若是仍是以为 EXPLAIN
执行计划列太多了,也记不住呀,那么请重点关注如下几列:
第1列
:ID越大,执行的优先级越高;ID相等,从上往下优先顺序执行。
第2列
:select_type 查询语句的类型,SIMPLE简单查询,PRIMARY复杂查询,DERIVED衍生查询(from子查询的临时表),派生表。
第4列
:请重点掌握,type类型,查询效率优先级:system->const->eq_ref->ref->range->index->ALL
ALL
是最差
的,system
是最好
的,性能最佳,阿里巴巴开发规约中要求最差也获得 range
级别,而不能有 index、ALL
。
最后,对于后端工程师而言,尽力都能掌握 EXPLAIN
的使用,写完SQL请习惯性的用它帮助你分析一下,作一个对SQL性能有追求的程序员,由于SQL也是程序员必备技能,将慢查询问题拍死在项目上线前夕。
若是以为本文有所收获,欢迎转发分享。
参考资料:
MySQL官网
https://www.cnblogs.com/songw...
https://www.cnblogs.com/phpdr...
欢迎关注个人公众号,扫二维码关注得到更多精彩原创文章,与你一同成长~