说明
文章的图片来源《MySQL是怎么运行的:从根儿上理解MySQL》,欢迎大家买一本看看,对于mysql是由浅入深的讲解非常细致
6.B+树的索引
没有索引是怎么查找的
普通的等值查找
- 二分找槽+遍历槽
上面是主键查找,但是对于非主键那么怎么查?
- 双向链表找槽+遍历槽
索引
为什么需要遍历所有的槽?
- 为什么不能使用页目录,原因就是如果使用二分法只是对主键有效,它会对比每个槽的最后一个记录的主键,但是对于特定的匹配条件来说,那就需要遍历所有槽,因为主键并不能决定这个条件所在的位置。
那么我们应该怎么做?
- 先加入3条记录
- 然后再加入一条,但是每个页只能3条所以开启了一个新页,但是加入的是4<5所以需要交换4和5的位置保证顺序,保证上一条的主键一定是小于下一条的
- 对于这样的一个页号不连续的数据页可以把第一个记录的主键作为目录值,记录它所在的页号
- 这个时候就可以通过二分找槽,然后遍历槽
- 这个目录也可以被叫做索引
InnoDB的索引方案
- 如果对于上面的目录的数据越来越多,这对记录数很多的表是不现实的
- 而且每次删除一个项,就需要把目录进行一个移位的调整,这不是好的方案
所以现在的设计是
- 数据页存储目录项
- 那么如何区分目录项记录和真实数据记录?可以通过记录的头信息record_type,记录除了真实数据还有各种额外信息,目录项的type值是1,普通记录是0,而且目录项只有主键和页。目录项的记录的页里面主键最小的那个记录的min_rec_mask的值就是1
- 下图可以看出就是使用了一个数据页去存储这些目录项,而不是使用单独一个页目录
- 如果一个目录项页不够就会分配多个,现在的查找顺序找主键是20的记录
- 确定目录项页
- 二分找目录项
- 遍历槽
- 如果目录项页太多,那么就再建立一个更高级的页
- 这个数据结构就是B+树,最底层的节点才会存储数据
- 每个节点100个记录,每层可以存放1000个节点,那么查找某条记录可以通过二分快速查找槽,然后走向下一层。
聚簇索引
b+树的特点
- 记录主键的大小进行记录和页的排序
- 页内按照主键排序单链表
- 存放的用户记录页也是按照主键大小建立双向链表。
- 存放目录项的页也是双向链表结构
- 叶子节点存储数据
这两种特性都具备的就是聚簇索引,索引的叶子节点存放了数据,数据也是索引
二级索引
和聚簇索引不同的地方
- 页内数据按照c2列来进行排序单链表
- 用户记录页也是按照c2列来形成双向链表
- 目录项页也是
- B+树的叶子节点并不是完整的用户记录,只有c2列和主键值
- 目录项的搭配是c2列+页号
查找顺序,如果是查找4
- 确定目录页,2<4<9确定了位置在42目录项页
- 通过目录项页确定用户记录页所在的位置所在的位置,定位到2<4<=4也就是34或者是35的位置
- 去到34和35定位到数据的位置。
- 最后数据不完整还是要去到主键索引里面去查找,这种被叫做回表,使用了2次B+树
联合索引
- 同时为多个列建立索引
- 比如c2和c3,先对比c2,c2相同的情况再对比c3
- 可以看到每条目录是c2+c3+页号
- 叶子节点是c2+c3+主键
B+树的生成
- 每个表默认生成B+树索引,一开始根节点没有记录
- 加入记录先存入根节点
- 根节点用完,就会把记录复制到新的页,然后进行页分裂,把复制的新页分成a和b,并根据插入值(主键或者是索引列)大小送到a或者是b
- 根节点是不会进行移动的
但是对于二级索引来说只有索引列+页号?
- 在下面如果插入一条索引列还是1的数据,那么数据应该放到页4还是页5?因为这里的页3有两个这样相同的目录项。
- 所以最后为了保证目录项是唯一的,那么就要使用
- 索引列的值
- 主键值
- 页号
- 如果c2相同那么就可以对比主键来进行查询
MyISAM的索引介绍
- 索引和数据分开,但是innodb是数据和索引都在一起
- 表记录存于数据文件,但是不会分页,可以通过行号查找数据
- 没有对主键进行排序
- MyISAM把索引存入文件,但是叶子节点是主键值+行号,先通过索引找到行号然后再找记录
总结
- 页内数据单链表存储,页之间就是双向链表,而且通过第一个记录的主键或者是索引列构建
- 通过多层的目录项页构成B+树索引
- 聚簇索引,目录项记录结构(主键+页号),叶子节点记录结构(主键+完整数据)
- 二级索引,目录项记录结构(索引列+主键+页号),叶子节点(索引列+主键)
- 联合索引,多个索引列,按照顺序进行比较排序。
7.B+树索引的使用
索引的代价
- 空间上的代价,B+树需要创建很多目录项的叶子节点,占用很大的空间,每个节点占用一个数据页
- 时间上的代价如果插入对数据的排序破坏就会出现页分裂,重新排序等操作
- 增删改越多,索引修改的次数就越多
B+树适用的条件
CREATE TABLE person_info(
id INT NOT NULL auto_increment,
name VARCHAR(100) NOT NULL,
birthday DATE NOT NULL,
phone_number CHAR(11) NOT NULL,
country varchar(100) NOT NULL,
PRIMARY KEY (id),
KEY idx_name_birthday_phone_number (name, birthday, phone_number)
);
- 下面这个图,就是按照上面建立的索引创建的。按照索引列的顺序进行进行排序
全值匹配
SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27' AND phone_number = '15123983239';
- 先按照name排序找到Ashburn
- 然后Ashburn相同的情况,是按照birthday的值进行排序的,快速按照birthday进行查找到’1990-09-27’
- 如果name和birthday都是相同那么就会通过phone_number列(排序)来进行查询
- 如果调换where后面条件顺序会有影响吗?
答案就是没有,因为有优化器这个部分。
匹配左边的值
SELECT * FROM person_info WHERE name = ‘Ashburn’;
SELECT * FROM person_info WHERE name = ‘Ashburn’ AND birthday = ‘1990-09-27’;
那么为什么一定是左边的值才能够使用到这个B+树的索引?
- SELECT * FROM person_info WHERE birthday = ‘1990-09-27’;这个就用不到了吗?
- 因为排序是按照name排序之后,name相同的时候才会使用birthday来进行排序,如果name不是相同的情况直接使用birthday那么很可能就是乱序的。如果需要使用birthday那么就创建多一个索引
- 所以最好就是左边的索引列一定要先匹配
- 那么如果是跳过birthday会怎么样?那么还是会出现birthday和phone_number的索引无法使用,原因就是phone_number必须是在birthday相同而且name也是相同的时候进行的排序。直接按照phone_number就是乱序的
匹配列前缀
- 字符串的比较规则使用的就是字符集的比较规则,就是每个字符有自己的大小,按顺序进行的一个比较排序
SELECT * FROM person_info WHERE name LIKE ‘As%’;
-
那么也就是说对于上面的As也是可以快速排序的。但是如果是%As%就无法直接在索引进行查找了。本质就是字符串的一个字符比较顺序,也可以类推为第一个字符相同,那么才会对比第二个字符,跟索引也是一样,第一个索引列相同才会去对比下一个索引列
-
如果是后缀不相同,那么就可以建立索引可以通过逆序的后缀。
匹配范围值
SELECT * FROM person_info WHERE name > ‘Asa’ AND name < ‘Barlow’;
查找的过程是
- 找到name=Asa记录
- 找到Barlow记录
- 然后他们之间的记录其实都是单链表连接的
- 找到每个记录的主键,然后回表去到聚簇索引查找
SELECT * FROM person_info WHERE name > ‘Asa’ AND name < ‘Barlow’ AND birthday > ‘1980-01-01’;
查询过程
- 通过name范围查找对应位置
- 然后就是通过birthday>‘1980-01-01’;逐个对比。而不能快速定位,因为name是不相同的,那么就无法使用到birthday索引
- 对于上面这个语句来说就无法使用birthday的索引,只有最左边的索引才能使用
精确匹配某一列并范围匹配另外一列
SELECT * FROM person_info WHERE name = ‘Ashburn’ AND birthday > ‘1980-01-01’ AND birthday < ‘2000-12-31’ AND phone_number > ‘15100000000’;
- 对于这种精确+范围是可以使用的
- 先查询name=Ashburn的
- 找到相同的Ashburn之后就能够通过birthday来查询 birthday > ‘1980-01-01’ AND birthday < ‘2000-12-31’ 的范围数据。
- 然后就是逐一对比phone_number > ‘15100000000’;
用于排序
- 通常把数据取出来之后通过各种排序算法排序,如果空间不够还需要存入磁盘,排序之后再返回给客户端,如果每次都需要排序那么就会耗费很多cpu资源。这种就是文件排序
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;
- 对于这语句就是name相同之后排序birthday,然后就是排序phone_number,就可以直接通过索引来排序
联合索引需要注意的是
- 排序一定要按照索引的顺序来
- 而且不能ASC或者DESC混合使用
SELECT * FROM person_info ORDER BY name, birthday DESC LIMIT 10;
- 如果是上面这种就是找到最小的name,然后找到等于name的值从右往左数10条,如果不够那么还要去找到第二小的值
- 但是如果两个都ASC那么直接找到最小就是直接从左到右数10条。ORDER BY name DESC, birthday DESC LIMIT 10,相比那种找最小之后还要相同的name才能从右往左数10条,而且还要重复这个过程直到有10条,所以相对非常复杂。因为birthday是DESC
如果没有索引列的查询
SELECT * FROM person_info WHERE country = ‘China’ ORDER BY name LIMIT 10;
- 那么就只能从原始的链表里面一个一个查并且对比
排序列包含一个非索引的列
SELECT * FROM person_info ORDER BY name, country LIMIT 10;
- 由于不是同一个索引上的列所以无法使用索引,因为name相同的时候country是乱序无法继续往下查找
排序列使用了复杂的表达式
SELECT * FROM person_info ORDER BY UPPER(name) LIMIT 10;
- 原因就是被函数计算之后索引记录的name已经和函数之后的完全不一样无法对比然后搜索
用于分组
SELECT name, birthday, phone_number, COUNT(*) FROM person_info GROUP BY name, birthday, phone_number
分组操作
- 先把name相同的弄到一起,然后name相同的记录里面再按照birthday的值来进行分组,分到一个小组,大分组里面的小分组
- 然后就是phone_num里面的小小分组
然后就会发现分组的方式是name相同然后让birthday排序分出那些不同的值的小组,然后就是让phone_number再在birthday和name相同的情况再次分成小小组。很明显这个顺序就是索引的顺序,可以按照索引的来进行分组。name相同,然后按照birthday排序分组,最后就是phone_num分组。可以看成是叶子节点name和birthday相同但是phone_num不相同的值进行分组。
回表的代价
SELECT * FROM person_info WHERE name > ‘Asa’ AND name < ‘Barlow’;
-
先从索引里面找到这些记录
-
由于查找的是*,索引叶子节点数据不够,然后根据查出来的主键再去主键索引里面查找
-
B+树的记录根据name查找,这些记录都是在一个数据页或者几个数据页里面连续存储的,可以把这些连着的记录读出来,顺序IO读取
-
但是取出来的主键并不是,可能分布在很多个数据页,这种读取就是随机IO读取
-
顺序IO的效率非常高。
而且需要回表(二级索引是顺序io,但是聚簇索引是随机IO)
- 也就是查询越多那么二级索引效率越低
- 那么有可能宁愿全表搜索,还不需要回表。(如果查询数量很多)
SELECT * FROM person_info WHERE name > ‘Asa’ AND name < ‘Barlow’ LIMIT 10;
- 上面限制了查询的个数,那么肯定比全表扫描快的多
SELECT * FROM person_info ORDER BY name, birthday, phone_number;
- 上面就是需要扫描所有索引并且回表,那么还不如不使用索引
SELECT * FROM person_info ORDER BY name, birthday, phone_number LIMIT 10;
- 这种限制了查询的个数,回表次数少所以可以使用
覆盖索引
SELECT name, birthday, phone_number FROM person_info WHERE name > ‘Asa’ AND name < ‘Barlow’
- 这种就直接能够在索引的叶子节点全部找到不需要回表。
如何挑选索引
排序、搜索、分组
- 只用于排序、搜索、分组的列创建索引,建立索引主要是看条件的索引列
考虑列的基数
- 考虑列的基数,给基数大的创建索引。基数的意思就是不重复的值,如果重复的值多,建立索引那么还是要靠主键的对比,最后还需要回表。
索引列类型尽量小
- 索引列类型尽量小,数据越小,对比越快而且占用空间小,一个数据页存入记录多,那么就能减少IO的次数
- 对于主键那么就更重要了,因为二级索引需要存储主键,主键小,那么占用空间也会更小
索引的前缀
- 可以使用索引的前缀,如果前缀的区分度高,那么就可以使用前缀作为索引,其实对比方式和字符串一样。
索引列前缀对排序的影响
- 由于没有包含完整的字符串,比如前10个相同后面不同的索引,那么就可能会排序错误,所以还是只能全表扫描获取数据来文件排序。
让索引列在比较表达式中单独出现
- WHERE my_col * 2 < 4
- WHERE my_col < 4/2
- 虽然看上去相同但是第二个效率更高,因为索引只认识不加计算的索引列
主键插入顺序
- 如果主键不是按顺序递增,问题就是会导致页分裂。如果是按顺序,那么就会直接插入到后面的页或者是开一个新的页。
冗余和重复索引
- 尽量不要给主键建立索引
- 不要建立没有用的索引
总结
- B+树需要消耗空间不要乱建索引
- B+索引使用情景
- 全值匹配
- 匹配左边的值
- 匹配范围值
- 精确一列并且范围匹配另一列
- 排序
- 分组
- 注意事项
-
索引尽量小
-
索引前缀不要用于排序
-
选择基数大的建立索引
-
尽量主键是自增的
-
删除冗余索引
-
尽量使用覆盖索引防止回表
-
WHERE my_col < 4/2
- 虽然看上去相同但是第二个效率更高,因为索引只认识不加计算的索引列
主键插入顺序
- 如果主键不是按顺序递增,问题就是会导致页分裂。如果是按顺序,那么就会直接插入到后面的页或者是开一个新的页。
[外链图片转存中…(img-2nVZpQIy-1635599279311)]
冗余和重复索引
- 尽量不要给主键建立索引
- 不要建立没有用的索引
总结
- B+树需要消耗空间不要乱建索引
- B+索引使用情景
- 全值匹配
- 匹配左边的值
- 匹配范围值
- 精确一列并且范围匹配另一列
- 排序
- 分组
- 注意事项
- 索引尽量小
- 索引前缀不要用于排序
- 选择基数大的建立索引
- 尽量主键是自增的
- 删除冗余索引
- 尽量使用覆盖索引防止回表