索引是一种能提高数据库查询效率的数据结构,一般存储在磁盘的文件中,它是占用物理空间的
适当的索引能提高查询效率,过多的索引会影响数据库表的插入和更新功能。
优势:
劣势:
①:数据结构维度
O(logn)
,适合范围查询MyISAM
和 InnoDB
(MYSQL 5.6 版本之后) 中都支持使用全文索引,一般在文本类型 char、text、varchar 类型上创建②:物理存储维度
③:逻辑维度
④:字段个数
准备:
CREATE TABLE `user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(10) DEFAULT NULL,
`city` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (1, '李四', 20, '杭州');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (2, '张三', 18, '北京');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (3, '张三', 23, '上海');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (4, '赵六', 22, '杭州');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (5, '王五', 19, '北京');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (6, '赵六', 24, '上海');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (7, '刘七', 20, '上海');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (8, '刘七', 22, '上海');
INSERT INTO `user` (`id`, `name`, `age`, `city`) VALUES (9, '王九', 9, '杭州');
Hash 索引其实用的不多,最主要是因为最常见的存储引擎 InnoDB 不支持显示地创建 Hash 索引,只支持自适应Hash索引
虽然可以使用 sql 语句在 InnoDB 显示声明 Hash 索引,但是其实是不生效的:
对 name 字段建立 Hash 索引,但是通过 show index from 表名
就会发现实际还是 B+ 树
在存储引擎中,Memory 引擎支持 Hash 索引
Hash 索引其实有点像 Java 中的 HashMap 底层的数据结构,他也有很多的槽,存的也是键值对,键值为索引列,值为数据的这条数据的行指针,通过行指针就可以找到数据
假设现在 user 表用 Memory 存储引擎,对 name 字段建立 Hash 索引,表中插入三条数据:
Hash 索引会对索引列 name 的值进行 Hash 计算,然后找到对应的槽下面,如下图所示:
当遇到 name 字段的 Hash 值相同时,也就是 Hash 冲突,就会形成一个链表,比如有 name = 张三
有两条数据,就会形成一个链表。
之后如果要查 name=李四
的数据,只需要对李四进行 Hash 计算,找到对应的槽,遍历链表,取出 name = 李四
对应的行指针,然后根据行指针去查找对应的数据
Hash索引优缺点
B+ 树是 MYSQL 索引中用的最多的数据结构,这里先不介绍,下面会着重介绍
我们插入表的数据其实最终都要持久化到磁盘上,InnoDB 为了方便管理这些数据,提出了页的概念,它会将数据划分到多个页中,每个页大小默认是16KB,这个页我们可以称为 数据页
当我们插入一条数据的时候,数据都会存在数据页中,如下图所示:
当数据不断地插入数据页中,数据会根据主键(没有的话会自动生成)的大小进行排序,形成一个单向链表:
数据页中除了会存储我们插入的数据之外,还会有一部分空间用来存储额外的信息,额外的信息类型比较多
假设现在需要在数据页中定位到 id=2 的这条记录的数据,如何快速定位?
有一种笨办法:从头开始顺着链表遍历就行了,判断 id 是不是等于 2,如果等于 2 就取出数据就行了
虽然这种方法可行,但是如果一个数据页存储的数据多,几十或者是几百条数据,每次都这么遍历,不是太麻烦了
所以,MYSQL 想了一个好办法:给这些数据分组
假设数据页中存了 12 条数据,那么整个分组大致如下图所示:
当我们不断的往表中插入数据的时候,数据占用空间就会不断变大,但是一个数据页的大小是一定的,当一个数据页存不下数据的时候,就会重新创建一个数据页来存储数据
MYSQL 为了区分每个页,会为每个数据页分配一个页号,存在额外信息的存储空间中,同时额外信息还会存储当前数据页的前一个和后一个数据页的位置,从而形成数据页之间的双向链表
并且 MYSQL 规定,前一个数据页的存储数据 id 的最大值要小于后一个数据页的存储数据 id 的最小值,这样就实现了数据在所有数据页中按照 id 的大小排序
现在,如果有多个数据页,当我们需要查找 id = 5 的数据,怎么办呢?
当然还是可以用上面的笨办法,那就是从第一个数据页开始遍历,然后遍历每个数据页中的数据,最终也可以找到 id = 5 的数据
但是你仔细想想,这个笨办法就相当于全表扫描了呀,这肯定是不行的
优化:跟前面单数据页查找数据的优化思路差不多
它会将每个数据页中最小的 id 拿出来,单独放到另一个数据页中,这个数据页不存储我们实际插入的数据,只存储最小的 id 和这个 id 所在数据页的页号,如图所示:
此时数据页 5 就是抽取出来的,存放了下面三个存放数据的数据页的最小的 id 和对应的数据页号
如果此时查找 id = 5 的数据就很方便了,大致分为以下几个步骤:
这样就实现了根据主键 id 到在多个数据页之间查找数据
随着数据量不断增多,存储数据的数据页不断变多,数据页5的数据就会越来越多,但是每个数据页默认就 16k,所以数据页 5 也会分裂出多个数据页的情况,如下图:
MYSQL 会去抽取数据页 5 和数据页 10 存储的最小的数据的 id 和对应的数据页号,单独拎出来放到一个数据页中,如下图:
数据页11就是新抽取的数据页,存储了id=1和对应的数据页5的页号以及数id=10和对应的数据页10的页号(而这就是B+树)
而这种叶子节点存储实际插入的数据的 B+ 树就被称为 聚簇索引,非叶子节点存储的就是记录的 id 和对应的数据页号
所以对于InnoDB存储引擎来说,数据本身就存储在一颗B+树中
非聚簇索引也被称为二级索引,本身也就是一颗B+树,一个二级索引对应一颗B+树,但是二级索引B+树存储的数据跟聚簇索引不一样
聚簇索引:叶子节点存的就是我们插入到数据库的数据,非叶子节点存的就是数据的主键 id 和对应的数据页号
非聚簇索引:叶子节点存的是索引列的数据和对应的主键id,非叶子节点除了索引列的数据和id之外,还会存数据页的页号
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。它表示索引结构和数据一起存放的索引。非聚集索引是索引结构和数据分开存放的索引
在 MySQL 的 InnoDB
存储引擎中, 聚簇索引与非聚簇索引最大的区别:叶节点是否存放一整行记录。聚簇索引叶子节点存储了一整行记录,而非聚簇索引叶子节点存储的是主键信息,因此,一般非聚簇索引还需要回表查询
而在 MyISM
存储引擎中,它的主键索引,普通索引都是非聚簇索引,因为数据和索引是分开的,叶子节点都使用一个地址指向真正的表数据
场景:
我们现在对 name 字段加了一个普通非唯一索引,那么 name 就是索引列,同时 name 这个索引也就是单列索引
此时如果往表中插入三条数据,那么 name 索引的叶子节点存的数据就如下图所示:
MYSQL 会根据 name 字段的值进行排序,这里我假设张三排在李四前面,当索引列的值相同时,就会根据 id 排序,所以索引实际上已经根据索引列的值排好序了
name 字段存储的中文也可以排序嘛?
答案是可以的,并且 MYSQL 支持很多种排序规则,我们在建数据库或者是建表的时候等都可以指定排序规则。
对于单个索引列数据查找也是跟前面说的聚簇索引一样,也会对数据分组,之后可以根据二分查找在单个索引列来查找数据
当索引页不断增多是,为了方便在不同索引页中查找数据,也就会抽取一个索引页,除了存页中 id,同时也会存储这个 id 对应的索引列的值:
当数据越来越多越来越多,还会抽取,也会形成三层的一个 B+ 树
除了单列索引,联合索引其实也是一样的,只不过索引页存的数据就多了一些索引列
比如,在 name 和 age 上建立一个联合索引,此时单个索引页就如图所示:
先以 name 排序,name 相同时再以 age 排序,如果再有其它列,依次类推,最后再以 id 排序
相比于只有 name 一个字段的索引来说,索引页就多存了一个索引列
最后形成的 B+ 树简化为如下图:
where
、group by
、order by
等后面没有使用到的字段,不需要建立索引可以从几个维度去看这个问题,查询是否够快,效率是否稳定,存储数据多少, 以及查找磁盘次数,为什么不是二叉树,为什么不是平衡二叉树,为什么不是 B 树,而偏偏是 B+树呢?
①:为什么不是一般二叉树?
如果二叉树特殊化为一个链表,相当于全表扫描。平衡二叉树相比于二叉查找 树来说,查找效率更稳定,总体的查找速度也更快。
②:为什么不是平衡二叉树呢?
如果树这种数据结构作 为索引,那我们每查找一次数据就需要从磁盘中读取一个节点,也就是我们说 的一个磁盘块,但是平衡二叉树可是每个节点只存储一个键值和数据的,如果 是 B 树,可以存储更多的节点数据,树的高度也会降低,因此读取磁盘的次数 就降下来啦,查询效率就快啦。
③:那为什么不是 B 树而是 B+树呢?
假设有以下表结构,并且初始化了这几条数据:
CREATE TABLE `employee` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`date` datetime DEFAULT NULL,
`sex` int(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into employee values(100,'小伦',43,'2021-01-20','0');
insert into employee values(200,'俊杰',48,'2021-01-21','0');
insert into employee values(300,'紫琪',36,'2020-01-21','1');
insert into employee values(400,'立红',32,'2020-01-21','0');
insert into employee values(500,'易迅',37,'2020-01-21','1');
insert into employee values(600,'小军',49,'2021-01-21','0');
insert into employee values(700,'小燕',28,'2021-01-21','1');
执行这条查询SQL,需要执行几次的树搜索操作?
select * from Temployee where age=32;
可以先画出 idx_age
普通索引的索引结构图,大概如下:
再画出 id
主键索引,我们先画出聚族索引结构图,如下:
这条 SQL 查询语句执行大概流程是这样的:
idx_age
索引树,将 磁盘块1
加载到内存,由于 32<43,搜索左路分支,到磁盘寻址 磁盘块2
磁盘块2
加载到内存中,由于 32<36,搜索左路分支,到磁盘寻址 磁盘块4
磁盘块4
加载到内存中,在内存继续遍历,找到 age=32 的记录,取得 id = 400
id主键索引树
id主键索引树
,将 磁盘块1
加载到内存,因为 300<400<500,所以在选择中间分支,到磁盘寻址 磁盘块3
磁盘块3
,找到了 id=400,但是它不是叶子节点,所以会继续往下找。到磁盘寻址 磁盘块8
磁盘块8
加载内存,在内存遍历,找到 id=400 的记录,拿到 R4 这一行的数据,好的,大功告成回表:当查询的数据在索引树中,找不到的时候,需要回到主键索引树中去获取,这个过程叫做回表
比如:
select * from Temployee where age=32;
需要查询所有列的数据,idx_age 普通索引
不能满足,需要拿到主键 id 的值后,再回到id主键索引查找获取,这个过程就是回表
如果我们查询 SQL 的 select *
修改为 select id, age
的话,其实是不需要回表的。因为 id 和 age 的值,都在 idx_age 索引树
的叶子节点上
覆盖索引是 select 的数据列只用从索引中就能够取得,不必回表,换句话说,查询列要被所建的索引覆盖。
所以,在日常开发中,尽量不要 select *
,需要什么查什么,如果出现覆盖索引的情况,查询会快很多
给你这个SQL:
select * from employee where name like '小%' and age=28 and sex='0';
其中,name
和 age
为联合索引 (idx_name_age)
如果是 Mysql5.6 之前,在 idx_name_age
索引树,找出所有名字第一个字是 “小”
的人,拿到它们的主键 id,然后回表找出数据行,再去对比年龄和性别等其他字段。如图:
有些朋友可能觉得奇怪,
idx_name_age(name,age)
不是联合索引嘛?为什么选出包含“小”
字后,不再顺便看下年龄 age 再回表呢,不是更高效嘛?
所以呀,MySQL 5.6 就引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数
因此,MySQL5.6 版本之后,选出包含 “小”
字后,顺表过滤 age=28
最左前缀匹配原则:在 MySQL 建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配
比如你建立一个组合索引(a,b,c),其实可以相当于建了(a),(a,b),(a,b,c) 三个索引,大大提高了索引复用能力
explain
查看 SQL 的执行计划,这样就知道是否命中索引了
当 explain 与 SQL 一起使用时,MySQL 将显示来自优化器的有关语句执行计划的信息:
一般来说,我们需要重点关注 type、rows、filtered、extra、key
type 表示连接类型,查看索引执行情况的一个重要指标。以下性能从好到坏依次:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
该列表示 MySQL 估算要找到我们所需的记录,需要读取的行数。对于 InnoDB 表,此数字是估计值,并非一定是个准确值
该列是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例
该字段包含有关 MySQL 如何解析查询的其他信息,它一般会出现这几个值:
该列表示实际用到的索引。一般配合 possible_keys
列一起看
索引合并(index merge)是从 MySQL5.1 开始引入的索引优化机制,在之前的 MySQL 版本中,一条 sql 多个查询条件只能使用一个索引,但是引入了索引合并机制之后,MySQL 在某些特殊的情况下会扫描多个索引,然后将扫描结果进行合并
结果合并会为下面三种情况:
删除之前所有的索引,然后为 nam e和 age 各自分别创建一个二级索引 idx_name
和 idx_age
当执行下面这条 sql 就会出现取交集的情况:
select * from `user` where name = '赵六' and age= 22;
查看执行计划:
type 是 index_merge,并且 possible_key 和 key 都是 idx_name 和 idx_age,说明使用了索引合并,并且 Extra 有 Using intersect(idx_age,idx_name)
整个过程大致是这样的:分别根据 idx_name 和 idx_age 取出对应的主键 id,之后将主键 id 取交集,那么这部分交集的 id 一定同时满足查询 name = ‘赵六’ and age= 22 的查询条件(仔细想想),之后再根据交集的 id 回表
不过要想使用取交集的联合索引,需要满足各自索引查出来的主键 id 是排好序的,这是为了方便可以快速的取交集
比如下面这条 sql 就无法使用联合索引:
select * from `user` where name = '赵六' and age > 22;
只能用 name 这个索引,因为 age > 22 查出来的id是无序的,前面在讲索引的时候有说过索引列的排序规则
取并集就是将前面例子中的 and 换成 or
select * from `user` where name = '赵六' or age = 22;
前面执行的情况都一样,根据条件到各自的索引上去查,之后对查询的 id 取并集去重,之后再回表
同样地,取并集也要求各自索引查出来的主键id是排好序的,如果查询条件换成 age > 22 时就无法使用取并集的索引合并
select * from `user` where name = '赵六' or age > 22;
虽然取并集要求各自索引查出来的主键 id 是排好序的,但是如果遇到没排好序的情况,mysql 会自动对这种情况进行优化,会先对主键 id 排序,然后再取并集,这种情况就叫 排序后取并集(sort-union)。
比如上面提到的无法直接取并集的sql就符合排序后取并集(sort-union)这种情况:
select * from `user` where name = '赵六' or age > 22;