一、索引
1、索引概述
1.1、介绍
1.2、特点
2、索引结构
2.1、概述


2.2、B+Tree

- 绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。
- 红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。
2.3、MySQL里的B+tree索引结构
MySQL索引数据结构对经典的B+Tree进行了优化。在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能,利于排序。

2.4、Hash
2.4.1、结构


2.4.2、特点
- Hash索引只能用于对等比较(=,in),不支持范围查询(between,>,< ,...)
- 无法利用索引完成排序操作
- 查询效率高,通常(不存在hash冲突的情况)只需要一次检索就可以了,效率通常要高于B+tree索引
2.4.3、存储引擎支持
思考题: 为什么InnoDB存储引擎选择使用B+tree索引结构?
- 相对于二叉树,层级更少,搜索效率高;
- 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
- 相对Hash索引,B+tree支持范围匹配及排序操作;
3、索引分类
3.1、索引分类

3.2、聚集索引&二级索引

- 如果存在主键,主键索引就是聚集索引。
- 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
- 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索 引。

- 聚集索引的叶子节点下挂的是这一行的数据 。
- 二级索引的叶子节点下挂的是该字段值对应的主键值。

- 由于是根据name字段进行查询,所以先根据name='Arm'到name字段的二级索引中进行匹配查找。但是在二级索引中只能查找到 Arm 对应的主键值 10。
- 由于查询返回的数据是*,所以此时,还需要根据主键值10,到聚集索引中查找10对应的记录,最终找到10对应的行row。
- 最终拿到这一行的数据,直接返回即可。
4、索引语法
- 创建索引 :
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name ( index_col_name,... ) ;
- 查找索引 :
SHOW INDEX FROM table_name ;
- 删除索引 :
DROP INDEX index_name ON table_name ;
5、SQL性能分析
5.1、SQL执行频率
-- session 是查看当前会话 ;
-- global 是查询全局数据 ;
SHOW GLOBAL STATUS LIKE 'Com_______';
Com_delete: 删除次数Com_insert: 插入次数Com_select: 查询次数Com_update: 更新次数

5.2、慢查询日志

如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:
# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2
systemctl restart mysqld
然后,再次查看开关情况,慢查询日志就已经打开了。
- 执行如下SQL语句 :
select * from tb_user; -- 这条SQL执行效率比较高, 执行耗时 0.00sec select count(*) from tb_sku; -- 由于tb_sku表中, 预先存入了1000w的记录, count一次,耗时13.35sec
- 检查慢查询日志 :
最终我们发现,在慢查询日志中,只会记录执行时间超多我们预设时间(2s)的SQL,执行较快的SQL是不会记录的。
那这样,通过慢查询日志,就可以定位出执行效率比较低的SQL,从而有针对性的进行优化。
5.3、profile详情
SELECT @@have_profiling ;
SET profiling = 1;
select * from tb_user;
select * from tb_user where id = 1;
select * from tb_user where name = '白起';
select count(*) from tb_sku;
-- 查看每一条SQL的耗时基本情况
show profiles;
-- 查看指定query_id的SQL语句各个阶段的耗时情况
show profile for query query_id;
-- 查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;


5.4、explain
-- 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;


6、索引使用
6.1、验证索引效率

select * from tb_sku where id = 1\G;
SELECT * FROM tb_sku WHERE sn = '100000003145001';
create index idx_sku_sn on tb_sku(sn) ;
SELECT * FROM tb_sku WHERE sn = '100000003145001';
6.2、最左前缀法则

explain select * from tb_user where profession = '软件工程' and age = 31 and status = '0';
explain select * from tb_user where profession = '软件工程' and age = 31;
explain select * from tb_user where profession = '软件工程';
explain select * from tb_user where age = 31 and status = '0';
explain select * from tb_user where status = '0';
而通过上面的这两组测试,我们也可以看到索引并未生效,原因是因为不满足最左前缀法则,联合索引最左边的列profession不存在。
explain select * from tb_user where profession = '软件工程' and status = '0';
思考题:
当执行SQL语句:explain select * from tb_user where age = 31 and status = '0' and profession = '软件工程';
时,是否满足最左前缀法则,走不走上述的联合索引,索引长度?
可以看到,是完全满足最左前缀法则的,索引长度 54 ,联合索引是生效的。注意 : 最左前缀法则中指的最左边的列,是指在查询时,联合索引的最左边的字段 ( 即是第一个字段 ) 必须存在,与我们编写 SQL 时,条件编写的先后顺序无关。
6.3、范围查询
explain select * from tb_user where profession = '软件工程' and age > 30 and status = '0';
6.4、索引失效情况
6.4.1、索引列运算
不要在索引列上进行运算操作, 索引将失效。
在tb_user表中,除了前面介绍的联合索引之外,还有一个索引,是phone字段的单列索引。
A. 当根据phone字段进行等值匹配查询时, 索引生效。
explain select * from tb_user where phone = '17799990015';
explain select * from tb_user where substring(phone,10,2) = '15';
6.4.2、字符串不加引号
explain select * from tb_user where profession like '软件%';
explain select * from tb_user where profession like '%工程';
explain select * from tb_user where profession like '%工%';
6.4.4、or连接条件
explain select * from tb_user where id = 10 or age = 23;
explain select * from tb_user where phone = '17799990017' or age = 23;
由于age没有索引,所以即使id、phone有索引,索引也会失效。所以需要针对于age也要建立索引。
create index idx_user_age on tb_user(age);
建立了索引之后,我们再次执行上述的SQL语句,看看前后执行计划的变化
最终,我们发现,当or连接的条件,左右两侧字段都有索引时,索引才会生效。
6.4.5、数据分布影响
如果MySQL评估使用索引比全表更慢,则不使用索引。
select * from tb_user where phone >= '17799990005';
select * from tb_user where phone >= '17799990015';
explain select * from tb_user where profession is null;
explain select * from tb_user where profession is not null;
接下来,我们做一个操作将profession字段值全部更新为null。
然后,再次执行上述的两条SQL,查看SQL语句的执行计划。
6.5、SQL提示
目前tb_user表的数据情况如下
索引情况如下:
把上述的 idx_user_age, idx_email 这两个之前测试使用过的索引直接删除。
drop index idx_user_age on tb_user;
drop index idx_email on tb_user;
查询走了联合索引。
create index idx_user_pro on tb_user(profession);
C. 创建单列索引后,再次执行A中的SQL语句,查看执行计划,看看到底走哪个索引。
use index : 建议 MySQL 使用哪一个索引完成此次查询(仅仅是建议, mysql 内部还会再次进行评估)。explain select * from tb_user use index(idx_user_pro) where profession = '软件工程';
ignore index : 忽略指定的索引。explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';
force index : 强制使用索引
explain select * from tb_user force index(idx_user_pro) where profession = '软件工程';
6.6、覆盖索引
接下来,我们来看一组SQL的执行计划,看看执行计划的差别,然后再来具体做一个解析
explain select id, profession from tb_user where profession = '软件工程' and age = 31 and status = '0' ;explain select id,profession,age, status from tb_user where profession = '软件工程' and age = 31 and status = '0' ;explain select id,profession,age, status, name from tb_user where profession = '软件工程' and age = 31 and status = '0' ;explain select * from tb_user where profession = '软件工程' and age = 31 and status = '0';
上述这几条SQL的执行结果为:


id是主键,是一个聚集索引。 name字段建立了普通索引,是一个二级索引(辅助索引)。
B. 执行SQL : select * from tb_user where id = 2;
根据id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
C. 执行SQL:selet id,name from tb_user where name = 'Arm';
D. 执行SQL:selet id,name,gender from tb_user where name = 'Arm';
思考题:一张表 , 有四个字段 (id, username, password, status), 由于数据量大 , 需要对以下SQL 语句进行优化 , 该如何进行才是最优方案 :select id,username,password from tb_user where username = 'itcast';
答案: 针对于 username, password建立联合索引, sql为:
create index idx_user_name_pass on tb_user(username,password);
这样可以避免上述的 SQL 语句,在查询的过程中,出现回表查询。
6.7、前缀索引
- 语法
create index idx_xxxx on table_name(column(n)) ;
示例 :
为 tb_user 表的 email 字段,建立长度为5的前缀索引。create index idx_email_5 on tb_user(email(5));
- 前缀长度
可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值,索引选择性越高则查询效率越高,唯一索引的选择性是1 ,这是最好的索引选择性,性能是最好的。
select count(distinct email) / count(*) from tb_user ; select count(distinct substring(email,1,5)) / count(*) from tb_user ;
小贴士: 求取不重复的值, 在字段前加上distinct关键字来去重。
- 前缀索引的查询流程
6.8、单列索引与联合索引

在查询出来的索引中,既有单列索引,又有联合索引
接下来,我们来执行一条SQL语句,看看其执行计划:
create unique index idx_user_phone_name on tb_user(phone,name);
在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引,而非单列索引。
如果查询使用的是联合索引,具体的结构示意图如下:
7、索引设计原则
- 针对于数据量较大,且查询比较频繁的表建立索引。
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
- 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。