前面已经写了有两篇章长度的文章,第三篇我一直在寻思着要写什么(其实并没有),按照脑图来的话,这篇文章我们该来讲讲关于索引的知识了,这可是 MySQL 性能优化很关键的知识点,千万千万不要错过,不过我这里会相对比较深入地探究,相信大家读完之后多少会有点收获。
先送上两张飞机票还没读过前面文章的伙伴可以先前往阅读,由浅入深:
MySQL相关(一)- 一条查询语句是如何执行的
MySQL相关(二)- 一条更新语句是如何执行的
由于索引的知识点比较多,官网的内容也很多,如果大家想详细了解可以到官网,想先通读了解的话可以先看看我对索引的总结,这一章节分为三部分来讲:
前面提到的脑图如下,想要完整高清图片可以到微信我的公众号下【6曦轩】下回复 MySQL 脑图获取:
数据库索引,是数据库管理系统(DBMS)中一个排序的数据结构,以协助快速查询、 更新数据库表中数据。
首先数据是以文件的形式存放在磁盘上面的,每一行数据都有它的磁盘地址。如果 没有索引的话,要从 500 万行数据里面检索一条数据,只能依次遍历这张表的全部数据, 直到找到这条数据。
但是有了索引之后,只需要在索引里面去检索这条数据就行了,因为它是一种特殊 的专门用来快速检索的数据结构,我们找到数据存放的磁盘地址以后,就可以拿到数据 了。就像我们从一本 500 页的书里面去找特定的一小节的内容,肯定不可能从第一页开 始翻。那么这本书有专门的目录,它可能只有几页的内容,它是按页码来组织的,可以根据拼音或者偏旁部首来查找,只要确定内容对应的页码,就能很快地找到我们想要的 内容。
索引类型:Normal、Unique、Fulltext
在 Navicat 中可以直接创建索引,第一个是索引的名称,第二个是索引的列,比如我们是要对 id 创建索引还是对 name 创建索引。后面两个很重要,一个叫索引类型。
在 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 支持全文索引。 这个是索引的三种类型:普通、唯一、全文。
我们说索引是一个数据结构,那么它到底该选择一种什么数据结构才能实现数据的高效索引呢?我们继续往下看。
在这个篇章里我们通过一些数据结构来一步一步演算出 MySQL 为啥要用 B+tree 作为索引的数据结构以及对 B+tree 的详细介绍,篇幅较长,我们用另外的篇章来讲述:
飞机票:MySQL索引数据模型推演
聚集索引是指索引键值的逻辑顺序跟表数据行的物理存储顺序是一致的。(比如字典的目录是按拼音排序的,内容也是按拼音排序的,按拼音排序的这种目录就叫聚集索引)。
在 InnoDB 里面,它组织数据的方式叫做叫做(聚集)索引组织表(clustered index organize table),所以主键索引是聚集索引,非主键都是非聚集索引。
如果 InnoDB 里面主键是这样存储的,那主键之外的索引,比如我们在 name 字段上面建的普通索引,又是怎么存储和检索数据的呢?
InnoDB 中,主键索引和辅助索引是有一个主次之分的。
辅助索引存储的是辅助索引和主键值。如果使用辅助索引查询,会根据主键值在主键索引中查询,最终取得数据。
比如我们用 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 的优化器发现走索引跟使用全表扫描差不了多少的时候,就算建了索引,也不一定会走索引。
https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
建立索引,要使用离散度(选择度)更高的字段。
前面我们说的都是针对单列创建的索引,但有的时候我们的多条件查询的时候,也会建立联合索引。单列索引可以看成是特殊的联合索引。
比如我们在 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”代表使用了覆盖索引。
select * ,用不到覆盖索引。
很明显,因为覆盖索引减少了 IO 次数,减少了数据的访问量,可以大大地提升查询效率。
当字段值比较长的时候,建立索引会消耗很多的空间,搜索起来也会很慢。我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。
创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引:
create table shop(address varchar(120) not null);
alter table shop add key (address(12));
问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的,截取得少了,
重复内容太多,字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?
先看一下字段在全部数据中的选择度:
select count(distinct address) / count(*) from shop;
通过不同长度去计算,与全表的选择性对比:
select count(distinct left(address,10))/count(*) as sub10,
count(distinct left(address,11))/count(*) as sub11,
count(distinct left(address,12))/count(*) as sub12,
count(distinct left(address,13))/count(*) as sub13
from shop;
只要截取前 13 个字段,就已经有比较高的选择性了(这里的数据只是举例)。
https://dev.mysql.com/doc/refman/5.7/en/index-condition-pushdown-optimization.html
再来看这么一张表,在 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:
把 first_name LIKE '%zi’下推给存储引擎后,只会从数据表读取所需的 1 条记录。
索引条件下推(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),也不是基于语义。怎么样开销小就怎么来。
https://docs.oracle.com/cd/B10501_01/server.920/a96533/rbo.htm#38960 https://dev.mysql.com/doc/refman/5.7/en/cost-model.html
有问题?可以给我留言或私聊
有收获?那就顺手点个赞呗~
当然,也可以到我的公众号下「6曦轩」,
回复“学习”,即可领取一份
【Java工程师进阶架构师的视频教程】~
回复“面试”,可以获得:
【本人呕心沥血整理的 Java 面试题】
回复“MySQL脑图”,可以获得
【MySQL 知识点梳理高清脑图】
由于我咧,科班出身的程序员,php,Android以及硬件方面都做过,不过最后还是选择专注于做 Java,所以有啥问题可以到公众号提问讨论(技术情感倾诉都可以哈哈哈),看到的话会尽快回复,希望可以跟大家共同学习进步,关于服务端架构,Java 核心知识解析,职业生涯,面试总结等文章会不定期坚持推送输出,欢迎大家关注~~~