10.索引 【目标】了解什么是索引了解索引使用的数据结构掌握B树在索引中的应用掌握索引分类和使用【前言】本章的实战内容不多更多的是理论知识。会先从索引的底层、数据的存储方式等讲起然后再讲索引的基础语法。所以说请大家静下心来理论知识不是一遍就能完全记住的大家要多看几遍。1.索引简介1.1 索引是什么MySQL的索引是⼀种数据结构它可以帮助数据库高效地查询、更新数据表中的数据。索引通过⼀定的规则排列数据表中的记录使得对表的查询可以通过对索引的搜索来加快速度。MySQL索引类似于书籍的目录只是目录作用的是书籍而索引作用的是数据库中的数据。两者的对应类似关系如下MySQL索引书籍目录索引的排列规则之一通过页数找到具体要看的内容索引的排列规则之二通过章节名找到具体要看的内容............1.2 为什么要使用索引在实际应用程序的运行过程中查询操作的频率远远高于增删改的频率。所以使用索引的目的在于提升数据检索的效率2.索引底层的数据结构2.1 HASH哈希表查询的时间复杂度是O(1)查询的速度非常快。但是在MySQL中并没有选择用哈希表作为索引的底层数据结构原因就在于哈希表不支持范围查找。2.2 二叉搜索树先来回顾一下二叉搜索树对于任何一棵树其左子树的数据都比右子树的数据小并且左右树高差的绝对值不超过1。对于二叉搜索树来说它的特点之一中序遍历整棵树拿到的数据是有序的。但是有几个问题会导致它不适合作为索引的底层结构1.最坏情况下时间复杂度为O(N)如果我们插入的数据是类似于10、20、30、40等递增或者40、30、20、10等递减的数那么会导致这颗二叉搜索树是单边树这时候查询的时间复杂度为O(N)。2.节点个数过多的时候无法保证树高无论是二叉搜索树、AVL树或者是红黑树他们虽然是平衡或者近似平衡的。但是他们都是二叉结构那么就导致了一层中被填满了但是实际并没有存储多少数据最后树的高度一直增加。在检索数据的时候每次访问某一个节点时就会发生一次磁盘IO{也就是innoDBMySQL的默认存储引擎会把数据从磁盘上读取到内存上}而在数据库系统中IO是性能的瓶颈减少IO次数可以有效的提升性能2.3 B树为了解决树高的问题可以使用B树。有兴趣的可以在这个网站上进行演示数据结构算法可视化通过观察在相同的数据量的情况下B树的树高可以得到有效的控制。也就是说在相同数据量的情况下B树检索数据需要的IO次数更少从而能提高效率。但是MySQL认为B树作为索引的数据结构还不够好因为在相同的数据量下B树可能没有B树的树高更低——B树仅是叶节点存储真实的数据而B树的所有节点都存储数据具体见下。2.4 B树MySQL索引底层B树是⼀种经常用于数据库和文件系统等场合的平衡查找树MySQL索引采用的数据结构就是B树。 有兴趣的可以在这个网站上进行演示数据结构算法可视化提醒一点在数据库中叶节点之间的连接是通过双向链表的形式来的而非单项链表以上仅是普通B树的结构2.4.1 B树的特点能够保持数据稳定有序插入与修改有较稳定的时间复杂度非叶子节点仅具有索引作用不存储数据所有叶子节点保真实数据所有叶子节点构成⼀个有序链表可以按照key排序的次序依次遍历全部数据2.4.2 B树与B树的对比叶子节点中的数据是连续的且相互链接便于区间查找和搜索。非叶子节点的值都包含在叶字节点中对于B树而言在相同树高的情况下查找任⼀元素的时间复杂度都⼀样性能均衡。2.4.3 为什么MySQL底层选择B树至于为什么相同数据量B树的树高比B树的树高更矮等到学完本章的“页”之后再进行讲解3.MySQL中的页3.1 什么是页3.1.1 关于ibd文件和页的关系在我的数据库bit_mall的数据库中有class、course、score、student、student1这五个表对应到文件上就是五个.ibd文件注意默认情况下一个表就对应着一个.ibd文件也存在一个.ibd文件对应着多个表的情况这里不做解释。在.ibd文件中最重要的结构体就是Page页一个.ibd文件中有多个页。页是内存与磁盘交互的最小单元通常磁盘和内存进行IO时拿到的是一个或多个页而不是整个.ibd文件默认大小为16KB每次内存与磁盘的交互至少读取一页。而在磁盘中每个页为内部的地址都是连续的因为在使用数据的过程中虽然我们大多时候需要的可能就是页中的一条数据。但是根据局部性原理将来要使用的数据大概率与当前访问的数据在空间上是临近的为了防止多次IO那么一次性就会读取一页或者多个页的数据到内存中当下次查询的数据还在这个页里面时就可以直接从内存中读取从而减少磁盘IO、提高性能。局部性原理是指程序在执行时呈现出局部性规律在⼀段时间内整个程序的执行仅限于程序中的某⼀部分。相应地执行所访问的存储空间也局限于某个内存区域。局部性通常有两种形式时间局部性和空间局部性。时间局部性Temporal Locality如果⼀个信息项正在被访问那么在近期它很可能还会被再次访问。空间局部性Spatial Locality将来要⽤到的信息⼤概率与正在使⽤的信息在空间地址上是临近的。3.1.2 页的存储容量每⼀个页中无论有没有数据、数据满没满都会使用 16KB 的存储空间。可以通过系统变量 innodb_page_size 来进行查看语法SHOW VARIABLES LIKE innodb_page_size;3.1.3 页的种类在MySQL中有很多不同类型的页其中最常用的是索引页用来存储实际数据的地址和数据页用来存实际数据。页的基本结构如下更详细的见后3.2 页的结构3.2.1 页文件头和页文件尾这里关注的为页号、上⼀页页号和下⼀页页号通过这两个属性可以把页与页之间链接起来形成⼀个双向链表。3.2.2 页主体页主体部分是保存真实数据的主要区域仅是数据行。每当创建⼀个新页都会自动分配两个行⼀个是页内最小行Infimun另⼀个是页内最大行Supremun这两个行并不存储任何真实信息而是做为数据行链表的头和尾起到标识真实数据的作用。提醒一句行中具体的字段这里不进行讲解因为重点并不在这如果有兴趣的可以自行学习。1.在没有数据的情况下2.在有数据的情况下当向⼀个新页插入数据时将 Infimun 连接第⼀个数据行最后⼀行真实数据行连接 Supremun 这样数据行就构建成了⼀个单向链表更多的行数据插入后会按照 主键从小到大 的顺序进行链接。当然实际的存储当中我们可能并不是把数据按照主键值依次输入比如可能不会按照(1,数据1),(2,数据2),(3,数据3)的形式来存入可能是(1,数据1),(3,数据3),(2,数据2)这种这就导致了实际上在连续性来说其并不是按照逐渐顺序存储的。但是可以确定的一点是页会根据主键来进行链表链接也就是在逻辑上是连续的3.2.3 页目录一、页的定位当我们按照索引来查找某条数据的时候要先定位到具体的页。该图仅为演示实际上每一个页中能存储的数据量远不止这些。比如找 id5 的数据。1.把索引页1从硬盘上拿到内存进行一次磁盘IO判断5在哪2.把索引页2从硬盘上拿到内存进行一次磁盘IO判断5在哪3.把数据页3从硬盘上拿到内存进行一次磁盘IO二、行的定位等我们定位到具体的页之后那么就要在页内进行定位比如一个页内有300条数据第一条数据的 id1 现在找 id150 的数据如果不加以任何的快捷查找方式那么只能从 第1条数据 一直遍历到 第150条数据 。这是非常浪费时间的所以我们增加了页目录三、页目录为了解决上述问题我们增加了一个叫Page Directory页目录的结构。它的作用很简单就是将页主体中的数据进行分槽方便查找。其中1.最小行为一个槽2.其他的槽中最多能容纳8条数据同时把每个槽中的最后一条数据的主键值作为标识3.最大行永远在最后一个槽中比如在上图中已经定位到了具体的数据页请找到主键为6的行这时候就不会再从头开始找而是会先定位到槽2然后再从槽2内容的开头进行查找。大大提高了效率3.2.4 数据页头类似于页文件头数据页头也是一个简介。不同的是页文件头的作用域是页与页之间比如通过页文件头来找到下一页而数据页头的作用域是页内比如查找总共有多少条数据。4.索引类型4.1 前提须知一般情况下一个表对应着一个ibd文件但是业务中的一个表通常是很多数据的那么一个页肯定存不了所有的数据因此一个ibd文件中有很多的页。由于B树的特性只有叶节点是数据页存储真实数据其余的节点都是索引页存储的是主键值和指向页的地址。4.2 聚集索引聚集索引的查找域是在主索引树主索引树的底层是B树。只有主索引树中才存储真实数据。一个表中必须有一个聚集索引而聚集索引按照优先级又分以下三种4.2.1 第一优先级如果一个表中有PRIMARY KEY那么innoDB会把他作为聚集索引。所以在定义表的时候非常推荐定义一个主键这样在查找数据的时候就会根据我们定义的主键来查找。4.2.2 第二优先级如果一个表中没有定义主键那么innoDB会把表中第一个NOT NULL 且 UNIQUE的列作为聚集索引4.2.3 第三优先级如果一个表中没有上述两种情况那么innnoDB就会在底层悄悄生成一个 6 字节的隐藏列DB_ROW_ID用这个自增的 ID 来当聚集索引4.3 非聚集索引二级索引非聚集索引的查找域是在辅助索引树辅助索引树是单独创建的一颗B树。但不同的是辅助索引树的数据页存储的并不是真实数据而是 主键值和非聚集索引列的值。4.3.1 普通索引普通索引可能为单列索引也可能为多列组合索引这时候被称为复合索引4.3.2 唯一索引唯一索引是在普通索引的基础上加了一条限制UNIQUE指定唯一索引的列不能有重复值。4.4 主索引树 和 辅助索引树 的区别结构层级 主索引树聚集索引树 辅助索引树二级/非聚集索引树索引页非叶子节点存的是(主键的值, 子节点页号)。为什么因为主索引树就是按主键排序的它需要通过比较主键的大小来决定下一步往左边找还是往右边找仅用于导航指路不存整行真实数据。存的是(建索引列的值, 子节点页号)例如name。为什么因为这棵树是按name排序的它通过比较姓名的拼音/字母大小来决定往哪走同样仅用于导航指路不存真实数据。数据页叶子节点存的是(主键的值, 完整的整行数据 Row)。为什么因为这是树的终点。你平时SELECT *查出来的所有列全都在这里的叶子节点上。存的是(建索引列的值, 主键的值)例如(张三, 1)。为什么到了终点它需要证明自己是你要找的name所以得存name的值同时它没有完整的行数据为了能找到完整的行必须捎带上主键的值方便拿去主树“回表”查。物理数量限制全表只能有 1 棵。全表可以有 N 棵建几个非聚集索引就有几棵。是否包含整行数据包含不包含虽然主索引树和辅助索引树在物理结构上都是 B 树都由索引页和数据页构成。但辅助索引树就像是一个‘精简的目录’。如果我们在辅助索引树里找不到想要的全部数据也就是不会发生索引覆盖现象就必须拿着它提供的主键回到主索引树里去查。这就是产生‘回表’的根本原因。在这里有两个名词需要解释一下一、回表由于在普通索引查询时普通索引的叶节点存储的是 (普通索引值,主键值当我们要查询的数据在辅助索引树没有时那么就会根据主键值回到主索引树的数据页去查找数据。这个过程就叫做回表。二、索引覆盖如果我们要查询的数据在辅助索引树中能查到那么就不需要再根据主键值回到主索引树也就是说不需要回表。这种情况就叫做索引覆盖5.使用索引5.1 创建索引5.1.1 自动创建自动创建索引的情况只发生在 聚集索引的第三种情况非聚集索引是不会发生自动创建的。5.1.2 手动创建一、主键索引-- 主键索引 -- 方式一直接指定主键 create table t_test_pk( id bigint primary key auto_increment, name varchar(10) ); -- 方式二创建表时单独指定主键 create table t_test_pk1( id bigint auto_increment, name varchar(10), primary key(id) ); -- 方式三修改表中的列为主键索引 create table t_test_pk2( id bigint, name varchar(10) ); alter table t_test_pk2 add primary key(id); -- 或者是 alter table t_test_pk2 modify id bigint primary key auto_increment;二、唯一索引-- 唯一键索引 -- 方式一直接指定唯一键 create table t_test_uk( id bigint primary key auto_increment, name varchar(10) unique ); -- 方式二创建表时单独指定唯一键 create table t_test_uk1( id bigint primary key auto_increment, name varchar(10), unique key(name) ); -- 方式三修改表中的列为唯一索引 create table t_test_uk2( id bigint primary key auto_increment, name varchar(10) ); alter table t_test_uk2 add unique key(name); -- 或者是 alter table t_test_uk2 modify name varcahr(10) unique auto_increment;三、普通索引和复合索引复合索引和普通索引一样只是在指定索引列的时候复合索引是多个索引列这里不再进行演示。-- 普通索引 -- 方式一直接指定普通索引 create table t_test_index( id bigint primary key auto_increment, name varchar(10) unique, sno varchar(10), index(sno) ); -- 方式二修改普通索引 create table t_test_index1( id bigint primary key auto_increment, name varchar(10) unique, sno varchar(10) ); alter table t_test_index1 add index(sno) -- 方式三单独创建索引列并且指定索引名 create table t_test_index2( id bigint primary key auto_increment, name varchar(10) unique, sno varchar(10) ); create index index_name on t_test_index2(sno);四、总结虽然说每个索引的创建都有很多方式但是说到根本也就如下主键索引的语法字段名 字段类型 PRIMARY KEYor PRIMARY KEY(指定的主键列)唯一索引的语法字段名 字段类型 UNIQUEor UNIQUE(指定的主键列)普通/复合如果有指定普通索引列为多列则为复合索引索引的语法INDEX(指定的普通索引列[,指定的普通索引列2][,......])or CREATE INDEX 索引名 ON 表名(指定的普通索引列[,指定的普通索引列2][,......])5.2 查看索引查看索引的语法SHOW KEYS FROM 表名orSHOW INDEX FROM 表名建议用大家显示的信息会比图上多因为图上仅是我们必须要记住的其他的有兴趣可自行了解。5.3 删除索引对于这么一个表我们进行删除索引5.3.1 删除主键索引删除主键索引语法无自增属性ALTER TABLE 表名 DROP PRIMARY KEY;有自增属性先ALTER TABLE 表名 MODIFY 列名 类型 NOT NULL PRIMARY KEY;后ALTER TABLE 表名 DROP PRIMARY KEY;提醒日常业务中除非特殊要求要不然我们不会吃饱了撑的删除主键索引的。5.3.2 删除其他索引删除普通索引语法ALTER TABLE 表名 DROP INDEX 索引名;