前面已经写了有两篇章长度的文章,第三篇我一直在寻思着要写什么(其实并无),按照脑图来的话,这篇文章咱们该来说讲关于索引的知识了,这但是 MySQL 性能优化很关键的知识点,千万千万不要错过,不过我这里会相对比较深刻地探究,相信你们读完以后多少会有点收获。php
先送上两张飞机票🛬还没读过前面文章的伙伴能够先前往阅读,由浅入深:html
因为索引的知识点比较多,官网的内容也不少,若是你们想详细了解能够到官网,想先通读了解的话能够先看看我对索引的总结,这一章节分为三部分来说:面试
前面提到的脑图以下,想要完整高清图片能够到微信个人公众号下【6曦轩】下回复 MySQL 脑图获取: 性能优化
数据库索引,是数据库管理系统(DBMS)中一个排序的数据结构,以协助快速查询、 更新数据库表中数据。bash
首先数据是以文件的形式存放在磁盘上面的,每一行数据都有它的磁盘地址。若是 没有索引的话,要从 500 万行数据里面检索一条数据,只能依次遍历这张表的所有数据, 直到找到这条数据。 可是有了索引以后,只须要在索引里面去检索这条数据就好了,由于它是一种特殊 的专门用来快速检索的数据结构,咱们找到数据存放的磁盘地址之后,就能够拿到数据 了。就像咱们从一本 500 页的书里面去找特定的一小节的内容,确定不可能从第一页开 始翻。那么这本书有专门的目录,它可能只有几页的内容,它是按页码来组织的,能够根据拼音或者偏旁部首来查找,只要肯定内容对应的页码,就能很快地找到咱们想要的 内容。
索引类型:Normal、Unique、Fulltext
在 InnoDB 里面,索引类型有三种,普通索引、惟一索引(主键索引是特殊的惟一 索引)、全文索引。
普通(Normal):也叫非惟一索引,是最普通的索引,没有任何的限制。
惟一(Unique):惟一索引要求键值不能重复。另外须要注意的是,主键索引是一 种特殊的惟一索引,它还多了一个限制条件,要求键值不能为空。主键索引用 primay key 建立。
全文(Fulltext):针对比较大的数据,好比咱们存放的是消息内容,有几 KB 的数 据的这种状况,若是要解决 like 查询效率低的问题,能够建立全文索引。只有文本类型 的字段才能够建立全文索引,好比 char、varchar、text。
用命令行建立索引以下:
create table m3 (
name varchar(50),
fulltext index(name)
);
复制代码
SELECT
*
FROM
fulltext_test
WHERE
MATCH(content) against('6曦轩' IN NATURAL LANGUAGE MODE);
复制代码
MyISAM 和 InnoDB 支持全文索引。 这个是索引的三种类型:普通、惟1、全文。
咱们说索引是一个数据结构,那么它到底该选择一种什么数据结构才能实现数据的高效索引呢?咱们继续往下看。
在这个篇章里咱们经过一些数据结构来一步一步演算出 MySQL 为啥要用 B+tree 做为索引的数据结构以及对 B+tree 的详细介绍,篇幅较长,咱们用另外的篇章来说述:
飞机票🛬:MySQL索引数据模型推演
汇集索引是指索引键值的逻辑顺序跟表数据行的物理存储顺序是一致的。(好比字典的目录是按拼音排序的,内容也是按拼音排序的,按拼音排序的这种目录就叫汇集索引)。
在 InnoDB 里面,它组织数据的方式叫作叫作(汇集)索引组织表(clustered index organize table),因此主键索引是汇集索引,非主键都是非汇集索引。
若是 InnoDB 里面主键是这样存储的,那主键以外的索引,好比咱们在 name 字段上面建的普通索引,又是怎么存储和检索数据的呢?
辅助索引存储的是辅助索引和主键值。若是使用辅助索引查询,会根据主键值在主键索引中查询,最终取得数据。
好比咱们用 name 索引查询 name= 'Jack',它会在叶子节点找到主键值,也就是id=1,而后再到主键索引的叶子节点拿到数据。
是由于有分叉和合并的操做,这个时候键值的地址会发生变化,因此在辅助索引里面不能存储地址。
- 若是咱们定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键做为汇集索引。
- 若是没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的惟一索引做为主键索引。
- 若是也没有这样的惟一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 做为隐藏的汇集索引,它会随着行记录的写入而主键递增。
select _rowid name from t2;
咱们容易有以一个误区,就是在常用的查询条件上都创建索引,索引越多越好,那究竟是不是这样呢?
第一个叫作列的离散度,咱们先来看一下列的离散度的公式: count(distinct(column_name)) : count(*),列的所有不一样值和全部数据行的比例。
数据行数相同的状况下,分子越大,列的离散度就越高。
简单来讲,若是列的重复值越多,离散度就越低,重复值越少,离散度就越高。
了解了离散度的概念以后,咱们再来思考一个问题,咱们在 name 上面创建索引和在 gender 上面创建索引有什么区别。
当咱们用在 gender 上创建的索引去检索数据的时候,因为重复值太多,须要扫描的行数就更多。例如,咱们如今在 gender 列上面建立一个索引,而后看一下执行计划。
ALTER TABLE user_innodb DROP INDEX idx_user_gender;
ALTER TABLE user_innodb ADD INDEX idx_user_gender (gender); -- 耗时比较久
EXPLAIN SELECT * FROM `user_innodb` WHERE gender = 0;
复制代码
show indexes from user_innodb;
复制代码
而 name 的离散度更高,好比“Jack”的这名字,只须要扫描一行。
ALTER TABLE user_innodb DROP INDEX idx_user_name;
ALTER TABLE user_innodb ADD INDEX idx_user_name (name);
EXPLAIN SELECT * FROM `user_innodb` WHERE name = 'Jack';
复制代码
查看表上的索引,Cardinality [kɑ:dɪ'nælɪtɪ] 表明基数,表明预估的不重复的值 的数量。索引的基数与表总行数越接近,列的离散度就越高。
show indexes from user_innodb;
复制代码
若是在 B+Tree 里面的重复值太多,MySQL 的优化器发现走索引跟使用全表扫描差不了多少的时候,就算建了索引,也不必定会走索引。
创建索引,要使用离散度(选择度)更高的字段。
前面咱们说的都是针对单列建立的索引,但有的时候咱们的多条件查询的时候,也会创建联合索引。单列索引能够当作是特殊的联合索引。
好比咱们在 user 表上面,给 name 和 phone 创建了一个联合索引。
ALTER TABLE user_innodb DROP INDEX comidx_name_phone;
ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);
复制代码
联合索引在 B+Tree 中是复合的数据结构,它是按照从左到右的顺序来创建搜索树的(name 在左边,phone 在右边)。
从这张图能够看出来,name 是有序的,phone 是无序的。当 name 相等的时候, phone 才是有序的。
这个时候咱们使用 where name= '青山' and phone = '136xx '去查询数据的时候, B+Tree 会优先比较 name 来肯定下一步应该搜索的方向,往左仍是往右。若是 name 相同的时候再比较 phone。可是若是查询条件没有 name,就不知道第一步应该查哪一个节点,由于创建搜索树的时候 name 是第一个比较因子,因此用不到索引。
因此,咱们在创建联合索引的时候,必定要把最经常使用的列放在最左边。
好比下面的三条语句,能用到联合索引吗?
1)使用两个字段,能够用到联合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= '权亮' AND phone = '15204661800';
复制代码
EXPLAIN SELECT * FROM user_innodb WHERE name= '权亮'
复制代码
EXPLAIN SELECT * FROM user_innodb WHERE phone = '15204661800'
复制代码
有一天咱们的 DBA 找到我,说咱们的项目里面有两个查询很慢。
SELECT * FROM user_innodb WHERE name= ? AND phone = ?; SELECT * FROM user_innodb WHERE name= ?;
复制代码
按照咱们的想法,一个查询建立一个索引,因此咱们针对这两条 SQL 建立了两个索引,这种作法以为正确吗?
CREATE INDEX idx_name on user_innodb(name);
CREATE INDEX idx_name_phone on user_innodb(name,phone);
复制代码
当咱们建立一个联合索引的时候,按照最左匹配原则,用左边的字段 name 去查询 的时候,也能用到索引,因此第一个索引彻底不必。
至关于创建了两个联合索引(name),(name,phone)。
若是咱们建立三个字段的索引 index(a,b,c),至关于建立三个索引:
index(a) index(a,b) index(a,b,c)
用 where b=? 和 where b=? and c=? 和 where a=? and c=?是不能使用到索引的。不能不用第一个字段,不能中断。
这里就是 MySQL 联合索引的最左匹配原则。
回表:
非主键索引,咱们先经过索引找到主键索引的键值,再经过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。
例如:select * from user_innodb where name = 'Jack';
在辅助索引里面,无论是单列索引仍是联合索引,若是 select 的数据列只用从索引中就可以取得,没必要从数据区中读取,这时候使用的索引就叫作覆盖索引,这样就避免了回表。
咱们先来建立一个联合索引:
--建立联合索引
ALTER TABLE user_innodb DROP INDEX comixd_name_phone;
ALTER TABLE user_innodb add INDEX `comixd_name_phone` (`name`,`phone`);
复制代码
这三个查询语句都用到了覆盖索引:
EXPLAIN SELECT name,phone FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666';
EXPLAIN SELECT name FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666';
EXPLAIN SELECT phone FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666';
复制代码
Extra 里面值为“Using index”表明使用了覆盖索引。
很明显,由于覆盖索引减小了 IO 次数,减小了数据的访问量,能够大大地提高查询效率。
再来看这么一张表,在 last_name 和 first_name 上面建立联合索引。
drop table employees;
CREATE TABLE `employees`(
`emp_no` INT(11) NOT NULL ,
`birth_date` date NULL ,
`first_name` VARCHAR(14) NOT NULL ,
`last_name` VARCHAR(16) NOT NULL ,
`gender` ENUM('M' , 'F') NOT NULL ,
`hire_date` date NULL ,
PRIMARY KEY(`emp_no`)
) ENGINE = INNODB DEFAULT CHARSET = latin1;
ALTER TABLE employees ADD INDEX idx_lastname_firstname(last_name , first_name);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(1 , NULL , '698' , 'liu' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(2 , NULL , 'd99' , 'zheng' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(3 , NULL , 'e08' , 'huang' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(4 , NULL , '59d' , 'lu' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(5 , NULL , '0dc' , 'yu' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(6 , NULL , '989' , 'wang' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(7 , NULL , 'e38' , 'wang' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(8 , NULL , '0zi' , 'wang' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(9 , NULL , 'dc9' , 'xie' , 'F' , NULL);
INSERT INTO `employees`(
`emp_no` ,
`birth_date` ,
`first_name` ,
`last_name` ,
`gender` ,
`hire_date`
)
VALUES
(10 , NULL , '5ba' , 'zhou' , 'F' , NULL);
复制代码
关闭 ICP:
set optimizer_switch='index_condition_pushdown=off';
复制代码
查看参数:
show variables like 'optimizer_switch';
复制代码
如今咱们要查询全部姓 wang,而且名字最后一个字是 zi 的员工,好比王胖子,王瘦子。查询的 SQL:
select * from employees where last_name='wang' and first_name LIKE '%zi' ;
复制代码
- 根据联合索引查出全部姓 wang 的二级索引数据,而后回表,到主键索引上查询所有符合条件的数据(3 条数据)。而后返回给 Server 层,在 Server 层过滤出名字以 zi 结尾的员工。
- 根据联合索引查出全部姓 wang 的二级索引数据(3 个索引),而后从二级索引中筛选出 first_name 以 zi 结尾的索引(1 个索引),而后再回表,到主键索引上查询所有符合条件的数据(1 条数据),返回给 Server 层。
很明显,第二种方式到主键索引上查询的数据更少。 注意,索引的比较是在存储引擎进行的,数据记录的比较,是在 Server 层进行的。而当 first_name 的条件不能用于索引过滤时,Server 层不会把 first_name 的条件传递给存储引擎,因此读取了两条没有必要的记录。 这时候,若是知足 last_name='wang'的记录有 100000 条,就会有 99999 条没有必要读取的记录。
执行如下 SQL,Using where:
explain select * from employees where last_name='wang' and first_name LIKE '%zi' ;
复制代码
Using Where 表明从存储引擎取回的数据不所有知足条件,须要在 Server 层过滤。先用 last_name 条件进行索引范围扫描,读取数据表记录,而后进行比较,检查是否符合 first_name LIKE '%zi' 的条件。此时 3 条中只有 1 条符合条件。
开启 ICP:
set optimizer_switch='index_condition_pushdown=on';
复制代码
此时的执行计划,Using index condition:
索引条件下推(Index Condition Pushdown)是 5.6 之后完善的功能。只适用于二级索引。ICP 的目标是减小访问表的完整行的读数量从而减小 I/O 操做。
由于索引对于改善查询性能的做用是巨大的,因此咱们的目标是尽可能使用索引。
- 在用于 where 判断 order 排序和 join 的(on)字段上建立索引
- 索引的个数不要过多。 ——浪费空间,更新变慢。
- 区分度低的字段,例如性别,不要建索引。 ——离散度过低,致使扫描行数过多。
- 频繁更新的值,不要做为主键或者索引。 ——页分裂
- 组合索引把散列性高(区分度高)的值放在前面。
- 建立复合索引,而不是修改单列索引。
- 过长的字段,怎么创建索引?
- 为何不建议用无序的值(例如身份证、UUID )做为索引?
- 索引列上使用函数(replace\SUBSTR\CONCAT\sum count avg)、表达式、计算(+ - * /):
explain SELECT * FROM `t2` where id+1 = 4;
- 字符串不加引号,出现隐式转换
ALTER TABLE user_innodb DROP INDEX comidx_name_phone; ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone); 复制代码
explain SELECT * FROM `user_innodb` where name = 136; explain SELECT * >FROM `user_innodb` where name = '136'; 复制代码
- like 条件中前面带%
- where 条件中 like abc%,like %2673%,like %888 都用不到索引吗?为何?
explain select *from user_innodb where name like 'wang%'; explain select *from user_innodb where name like '%wang'; 复制代码
过滤的开销太大,因此没法使用索引。这个时候能够用全文索引。 4. 负向查询
- NOT LIKE 不能:
explain select *from employees where last_name not like 'wang'
- != (<>)和 NOT IN 在某些状况下能够:
explain select *from employees where emp_no not in (1) explain select *from employees where emp_no <> 1 复制代码
注意一个 SQL 语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。其实,用不用索引,最终都是优化器说了算。
基于 cost 开销(Cost Base Optimizer),它不是基于规则(Rule-Based Optimizer),也不是基于语义。怎么样开销小就怎么来。 docs.oracle.com/cd/B10501_0… dev.mysql.com/doc/refman/…
有问题?能够给我留言或私聊 有收获?那就顺手点个赞呗~
固然,也能够到个人公众号下「6曦轩」,
回复“学习”,便可领取一份 【Java工程师进阶架构师的视频教程】~
回复“面试”,能够得到: 【本人呕心沥血整理的 Java 面试题】
回复“MySQL脑图”,能够得到 【MySQL 知识点梳理高清脑图】
因为我咧,科班出身的程序员,php,Android以及硬件方面都作过,不过最后仍是选择专一于作 Java,因此有啥问题能够到公众号提问讨论(技术情感倾诉均可以哈哈哈),看到的话会尽快回复,但愿能够跟你们共同窗习进步,关于服务端架构,Java 核心知识解析,职业生涯,面试总结等文章会不按期坚持推送输出,欢迎你们关注~~~