索引到底是什么?
索引的定义
- 我们先来看看维基百科对索引的定义:
数据库索引,是数据库管理系统(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,然后再到主键索引的叶子节点拿到数据。
- 为什么在辅助索引里面存储的是主键值而不是主键的磁盘地址呢?如果主键的数据类型比较大,是不是比存地址更消耗空间呢?我们前面说到 B Tree 是怎么实现一个节点存储多个关键字,还保持平衡的呢?
是因为有分叉和合并的操作,这个时候键值的地址会发生变化,所以在辅助索引里面不能存储地址。
- 另一个问题,如果一张表没有主键怎么办?
- 如果我们定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键作为聚集索引。
- 如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。
- 如果也没有这样的唯一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 作为隐藏的聚集索引,它会随着行记录的写入而主键递增。
select _rowid name from t2;
索引使用原则
我们容易有以一个误区,就是在经常使用的查询条件上都建立索引,索引越多越好,那到底是不是这样呢?
列的离散(sàn)度
第一个叫做列的离散度,我们先来看一下列的离散度的公式: 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'; 复制代码
2)使用左边的 name 字段,可以用到联合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= '权亮' 复制代码
3)使用右边的 phone 字段,无法使用索引,全表扫描:
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 次数,减少了数据的访问量,可以大大地提升查询效率。
索引条件下推(ICP)
再来看这么一张表,在 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' ; 复制代码
- 这条 SQL 有两种执行方式:
- 根据联合索引查出所有姓 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),也不是基于语义。怎么样开销小就怎么来。 docs.oracle.com/cd/B10501_0…dev.mysql.com/doc/refman/…