MySQL索引

目录

索引的引入

再次理解MySQL数据操作

索引

页内目录

页间目录

索引结构为什么要采用B+树?

聚簇索引和非聚簇索引

聚簇索引

非聚簇索引

主键索引和非主键索引

索引相关操作

创建主键索引

创建唯一键索引

创建普通索引

查询索引

删除索引

索引创建的原则


本期我们要学习的是MySQL中比较重要的一个知识点,即MySQL索引。

索引的引入

通过一个场景为大家引入索引的概念。

创建了一个数据库表,并向其中插入了 80000000 条记录。

在建立索引的情况下,查询员工编号为 998877 的员工。

我们不难发现,通过上述查询的结果可知,在800万条员工记录中查找 998877 这一员工我们花费了 5.01 秒。

给 empno 字段添加索引。

添加索引之后,再去查找 998877 号员工的记录。

我们发现此时查询的效率提高了250倍。

再次理解MySQL数据操作

MySQL数据库操作, 最重要的就是对数据的增,删,查,改。以一图示再次解释MySQL的工作原理。

我们在MySQL的第一期就已经讲过,MySQL其实就是一个client和sever程序,client对应mysql进程,sever对应mysqld进程。

基于此,在研究mysql的增删查改时,我们以mysqld为基础进行研究。

我们也知道,用户创建的数据库其实就是一个目录,用户在该数据库中创建的表其实就是该目录下的文件,表中的记录其实就是文件中的数据。我们知道文件都是存储在磁盘上的,所以mysqld对数据进行增删查改,其实就是与物理磁盘进行多次IO。

在Linux系统编程的基础IO章节,我们也讲过了磁盘在与操作系统进行IO时,一次IO的基本单位是4KB,那么mysqld进程在于磁盘进行IO时,也是4KB吗?

当然不是,我们先来谈谈mysqld与磁盘上的宏观数据交互的逻辑。如果mysqld要对数据库表进行查询操作,此时磁盘会先将数据通过操作系统拷贝到操作系统的内核缓冲区,然后由操作系统将内核缓冲区中的数据刷新到mysqld对应的buffer_pool缓冲区,最终由mysqld读取缓冲区中的数据进行查询。

此时就又有疑惑了。数据一次加载多少呢?加载什么数据呢?

直接给出结论,mysqld在于磁盘进行IO时,交互的基本单位为一个page页,大小为16KB,对于数据库中的每个表,磁盘都会为这个表建立对应的page,建立几个page,这要根据该表中的记录的大小来定。所以加载数据就是将该表对应的page页加载到内存中。

磁盘中有大量的page页,操作系统为了方便管理这些page页,所以创建了一个数据结构,用于链接每个page页和存储page页的数据。

索引

不知道大家有没有在数据库中见过这样一个现象。

比如说此时我们创建了一个adu表,图示如下。

依次按序向表中插入(2,'张三'),(1,'李四'),(5,'王五'),(3,'赵六')四条记录,然后查询表的全部记录。

好家伙,为什么显示的顺序和插入的顺序不一样呢?这其实也是与索引有关的,因为id是一个主键,在创建表的时候,操作系统会自动为主键添加索引,将插入的顺序在表中有序的排列,这可以大大的提高操作系统建立索引的效率。

通过了上述铺垫,我们知道了,数据库表的记录存储在了磁盘的page页中,在没有添加索引时,表对应的page页通过双向链表进行连接,如下图所示。

此时我们要查询一条记录,就得从表的第一个page页开始查询,先在page页内进行线性查询,不过不在第一个page页就要通过链接关系,找到下一个page页然后在下一个page页内再次进行查询,如果我们要查询的是最后一个记录,那么就意味着,查询这个记录就是在一个双向链表中,一直进行线性遍历,最终才查找到了对应的记录。

通过上述描述不难发现,没有索引的话,mysqld在查找对应的记录是完全就是一个线性查找的过程,查找的效率非常低。基于此我们引入了索引的概念。

索引是什么呢,索引其实就是通过建立页内目录和页间目录,最终形成的一个查找效率极高的B+树。

先来谈谈页内目录。

页内目录

目录其实大家都耳熟目染,在每本书的前几页都会有10几页的目录,那么书的这10几页目录是干什么的呢?

比如说西游记这本书,我们要找到真假美猴王这一章节,如果没有目录,就得从书的第一页开始找,一页页往后找,直到看到了真假美猴王这一章节的标题此时就找到了,此时就可以开始阅读。如果有了目录,就可以通过目录,在目录中找到真假美猴王这一章节对应的页码,直接翻到对应的页码就可以看真假美猴王这一章节的内容。

所以,目录就提高了查找的效率,之前是一页一页的查找,现在是直接通过目录定位到对应的章节页。

在page页中,页内目录图示如下。

如果我们要找3号id对应的记录,页内记录也是通过链表形式进行连接的,所以如果我们要找3号记录,就得从1号id对应的记录开始一次次的进行遍历,上述知识建立在我们要查询的记录就在当前页的基础上,如果不在当前页,先遍历完当前页,然后再遍历下一页直到找到对应的记录,所以说没有目录的遍历,就是一个线性遍历,查找效率很低。

但是如果我们有了页内目录,就可以直接通过页内目录2直接定位到3号id对应的记录上,就算要查询的id对应的记录不在当前页,我们也可以直接通过目录降低查找的次数,虽然表记录很多时,也是一个线性遍历,但是对比与没有目录的线性遍历,大大的提高了查找的效率。

但是此时还有一个问题,就是万一表的数据很多呢?有几百万个记录,此时存储表数据的page页也会有很多,即使页内有了目录,但是要查找一个记录也是从该表的第一个page页开始进行查询的,即使通过每个page页中的目的可以查找一次就排除一个页,但是万一要查找的记录刚好就是最后一个page页的最后一个记录,此时的查找仍然是线性遍历的,效率很低。

总而言之,页内目录虽然提高了mysqld在页内查找记录的效率,但是一旦数据过多,对于每个page页之间的查找效率并没有提高,每个page页之间仍然是线性查找的方式,总是查询完了当前的,查询下一个page页,直到找到最终的记录。

为了解决数据表中的记录过多时,存在多个page页,page页之间,查询效率低的问题,我们又引入了页间目录的概念。

页间目录

页间目录图示如下。

我们会在最底层的数据page页的上一层,再创建page页,上层的page页中也会存储一个个记录,只不过此时的记录不是数据库表中的记录,上层page页的记录=下层某个page所有的记录中最小的主键的id(键值)+指向下层page页的指针。

因为表的记录是按照主键id按顺序递增的。以上层第一个page页进行举例,通过第一个记录我们可以迅速找到下层第一个page页,通过第二个记录可以迅速找到第二个page页。如果此时要查询主键id为7的记录,因为7号id一定是在6号号id之后的。上层page页的每个记录也是通过链表链接起来的,所以mysqld会先遍历上层page页中的一个个记录,按顺序遍历每一个键值,发现第一个记录键值为,然后遍历第二个键值发现为6,所以此时就会排除第一个键值为1的记录中指针所指向的下层的page页,此时就不用再去线性遍历当前这个下层page页的,所以此时依次会排除一个page页,极大的提高了效率。

但是我们知道,一个page页的大小是16KB,也就是说page页存储的记录也是有个数限制的,当表中的数据很多时,下层的page页也很多,所以上层的page页中存储的记录也会很多,就导致了上层也会存储大量的page页,基于此,我们又会在上上层再次建立page页,上上层的page页中也会有记录,该记录=下层page页键值的最小值+指向下层page页的指针。最终按照此逻辑,使得最上层只保留一个page页。最终建立的多个page结构如下图所示。

上图就是操作系统帮助磁盘给表的对应字段建立索引最终形成B+树的结构。

我们简单的计算一下,一个page页是16KB,一个主键和指针的大小加起来是8字节。所以上层的一个page页可以存储2048个记录,也就是说上层的一个page页中可以存储下层的2048个page页,也就意味着,顶层的page页可以一次排除2048个底层page页,可以排除大量的不相关数据,极大的提高了对应记录的查询效率。

总而言之,页间目录就是上层page页中的记录,通过这些记录,可以去排除多个page页,越往上的页间目录,一次排除的page页就越多。

最终,通过构建页内目录和页间目录,页内目录排除记录,页间目录排除page页。最终构建了相关字段的索引结构即一颗B+树。

上图其实也就是构建主键索引的整个过程。

索引结构为什么要采用B+树?

为什么不用链表?

因为链表是线性遍历的,查询的时间复杂度是很高的。

为什么不用搜索二叉树?

因为搜索二叉树会退化,比如插入一些有序的元素,此时就会退化成单支的一颗搜索二叉树,即最终也还是退化成了链表结构,所以此时查询仍然是线性遍历,查询时间复杂度很高。

为什么不用AVL树和红黑树呢?

因为,AVL树和红黑树虽然都是平衡搜索二叉树,但是有一个致命的缺点,就是这两棵树都是二叉树,二叉树就意味着,结构是严格固定的,当叶子节点很多时,也势必的意味着这个数朝着满二叉树靠近,也就意味着将来的树的高度是很高的,搜索二叉树的查询的时间复杂度是与树的高度密切相关的,所以树的高度很高时,查询的时间复杂度也是很大的。

为什么不用B树呢?

B树和B+树最大的差别就是,B树不论是叶子节点还是普通节点,都会保存数据,而B+树只有叶子节点会保存数据。所以这就导致了,上层的page页中B数所能保存的页间目录记录一定是比B+树要少的,上层每个page页的目录记录一旦一少,那么此时需要的page页就要更多,上层page页一多,意味着上上层的page页也就变多,这样逐层增多,就会导致B树的高度一定是比B+树的高度要高的。

树的高度决定了查询时间复杂度,高度越高查询的时间复杂度越高,所以最终选择了高度低的B+树作为构建索引的结构。

聚簇索引和非聚簇索引

聚簇索引

创建table1,存储引擎为innodb。

在对应的 var/lib/mysql/study下生成了下图所示文件。

如上图,table1.ibd表示索引和数据在同一文件中。

innodb存储引擎下建立的主键索引结构如下。

我们上述 Innodb 存储引擎下的主键索引,不难发现,B+树的叶子节点中存储了表的记录,像这种索引结构中存储表记录数据的索引结构我们称之为聚簇索引。

非聚簇索引

myisam存储引擎下的主键索引如下图所示。

myisam存储引擎下的索引结构也是一颗B+树,不过此时的B+树的叶子节点中存储的不是表的记录,而是表记录对应的地址,mysqld通过myisam建立的主键索引,从根节点出发查找,最终在对应的数据page页查找到id之后,会根据id对应的记录地址,在内存中查找对应的记录。

主键索引和非主键索引

我们在上面已经展示了innodb和myisam引擎下的主键索引。

对于innodb存储引擎而言,主键索引和非主键索引的差别还是很大的。innodb的非主键索引图示如下。

对于innodb的普通索引,从根节点出发,通过飞主键字段找到了对应的数据page页之后,通过非主键字段找到对应的主键字段,然后再通过主键字段,从主键索引的根节点开始,按照逐渐索引的步骤,最终查找到对应的主键对应的记录。

我们把从非主键索引中通过非主键字段找到主键字段,在通过主键从主键索引中找到对应的主键对应的记录的这一操作,我们称之为回表查询。

对与myisam存储引擎而言,非主键索引如下图所示。

通过图示其实就可以看出来,myisam存储引擎的非主键索引和主键索引是类似的,都可以从根节点直接查询到数据页,再根据查询到的非主键字段获取与之对应的记录的地址,然后再根据地址找到对应的要查找到的记录,也就是说通过一次索引就可以找到,不用再去通过主键索引进行回表查询,由此可见,myisam相比较于innodb存储引擎,因为不用回表查询,所以查询效率是很高的。

那么既然查询效率高,那我们广泛使用myisam存储引擎不就行了,为什么还要有innodb存储引擎的存在呢?

存在即合理,这是因为myisam存储引擎虽然查询效率比innodb存储引擎查询效率高,但是myisam不支持事务模式,也就是不能保证数据插上删除时的安全,所以必须有innodb存储引擎的存在。

索引相关操作

创建主键索引

主键的索引不需要手动创建,操作系统会自动创建,所以主键索引的创建其实就是主键的创建。

  • 在创建表的时候,直接在字段名后指定。
sql 复制代码
create table user1(id int primary key, name varchar(30));
  • 在创建表的时候,在表的最后,制定一列或者几列为主键。
sql 复制代码
create table  user2(id int, name varchar(30), primary key(id));  
  • 在表创建之后,再去指定主键。
sql 复制代码
create table  user3(id int, name varchar(30));
alter table user3 add primary key(id);

主键索引的字段一般都是int类型,因为主键唯一所以查询效率高。

创建唯一键索引

创建唯一键索引和主键索引创建的方法一致,因为操作系统也会默认为唯一键创建索引,所以创建唯一键索引其实就是创建唯一键。

  • 在创建表的时候,直接在字段名后指定。
sql 复制代码
create table user1(id int unique, name varchar(30));
  • 在创建表的时候,在表的最后,制定一列或者几列为唯一键。
sql 复制代码
create table  user2(id int, name varchar(30), unique(id));  
  • 在表创建之后,再去指定唯一键。
sql 复制代码
create table  user3(id int, name varchar(30));
alter table user3 add unique(id);

唯一键索引的查询效率也很高, 因为唯一键对应的记录也是唯一的。

创建普通索引

  • 在创建表时进行普通索引的创建。
sql 复制代码
create table user1(id int primary key, 
name varchar(20), 
email varchar(30), 
index(name) 
); 
  • 在创建完表后,指定一列为普通索引。
sql 复制代码
create table user2(id int primary key,  name varchar(20),  email varchar(30)); 
alter table user2 add index(name);
  • 在创建完表后,给表的某个字段创建一个索引。
sql 复制代码
create table user3(id int primary key,  name varchar(20),  email varchar(30)); 
create index idx_name on user10(name);

查询索引

推荐大家使用。

sql 复制代码
show index from table_name;

表user2中有两个索引,一个为id字段的主键索引,一个为name字段的普通索引。

删除索引

虽然主键索引的创建和唯一键索引的创建是一样的,只要制定了主键字段和唯一键字段操作系统就会自动创建对应的主键索引和唯一键索引。但是因为主键是唯一的,而唯一键不是唯一的,所以主键索引的删除和唯一键索引的删除是不一样的。

删除主键索引直接删除主键,主键索引自然也就删除了,sql语句如下。

sql 复制代码
alter table 表名 drop primary key;

非主键索引和其它索引的删除,sql语句如下。

sql 复制代码
alter table 表名 drop index 索引名;

索引名就是show keys from 表名中的Key_name 字段。

sql 复制代码
 drop index name on table_name;

name为索引名称,table_name为表的名称。

索引创建的原则

  • 索引创建是为了提高记录查询的效率,也就是说是索引与查询相关的,查询伴随着筛选条件,所以索引的创建一般情况下是对where子句中的字段进行创建,从而在B+树中,对where子句中的key值进行查找。不出现在where子句中的字段,一般不创建索引。
  • 唯一性太差的字段不适合创建索引,唯一性差意味着创建非主键索引,非主键索引要经历回表查询,唯一性差的字段建立了索引,就会查出重复的该字段,然后就要多次回表查询,查询效率会降低。
  • 更新频繁的字段不适合创建索引,因为索引的创建其实就是一颗B+树的创建,一个字段更新频繁还设置了索引,就意味着上层page页中的记录中key值也会频繁的更改,所以对于B+树的维护会变得很频繁,所以不适合。

总的来说就是在构建的B+树索引结构中,是使用key值进行查找的,给哪个字段创建了索引,那么key值就是哪个字段,哪个字段会作为查询的条件,那么就要考虑给字段添加索引,添加好索引之后,让该字段作为key值在构建的B+树中进行查询,直到查询到对应的page页的对应记录或者对应的主键id。

以上便是MySQL索引的所有重点内容。

本期内容到此结束^_^

相关推荐
小高Baby@6 分钟前
解决幻读问题
数据库·mysql
TDengine (老段)9 分钟前
TDengine 转化函数 TO_TIMESTAMP 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
java叶新东老师30 分钟前
CMakelists.txt 实现多级目录编译
java·服务器·数据库
Sean_summer33 分钟前
暑期第二周
前端·数据库·python
左直拳44 分钟前
linux下变更mysql的数据文件目录
mysql·datadir·数据目录·变更数据目录·变更mysql目录
whn19771 小时前
达梦有多少个模式
数据库
王柏龙1 小时前
Entity Framework Core (EF Core) 中Database
数据库·microsoft
时序数据说1 小时前
时序数据库IoTDB的优势场景分析
大数据·数据库·物联网·时序数据库·iotdb
MacroZheng1 小时前
换掉Navicat!一款集成AI功能的数据库管理工具,功能真心强大!
java·后端·mysql
是阿建吖!1 小时前
【Redis】初识Redis(定义、特征、使用场景)
数据库·redis·缓存