MySQL的索引操作及底层结构浅析

发布于:2025-07-20 ⋅ 阅读:(15) ⋅ 点赞:(0)

一.索引

        提高数据库的性能,索引是物美价廉的东西了。不用加内存,不用改程序,不用调sql,只要执行正确的 create index ,查询速度就可能提高成百上千倍。但是天下没有免费的午餐,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。所以它的价值,在于提高一个海量数据的检索速度。

1.1mysql与磁盘交互的基本单位

        内存与磁盘的基本交互单位通常为页(如4KB),访问一个字节时,若其所在页未加载,操作系统会将该页全部加载到内存,这体现了空间局部性原理。

        而 MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB.

我们可以使用SHOW GLOBAL STATUS LIKE 'innodb_page_size';来查看当前mysql的IO基本交互单位的大小(我们后面统一基于innodb进行讲解):

        磁盘这个硬件设备的基本单位是 512 字节,而 MySQL InnoDB引擎 使用 16KB 进行IO交互。即, MySQL 和磁盘进行数据交互的基本单位是 16KB 。这个基本数据单元,在 MySQL 这里叫做page(注意和系统的page区分).

我们先来理清下mysql进行IO的过程:

  • MySQL 中的数据文件,是以page为单位保存在磁盘当中的。
  • MySQL 的 CURD 操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。
  • 而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。

        为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 Buffer Pool 的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。

理解一下什么是随机访问与连续访问:
随机访问:本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据。

连续访问:如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问。

        对于没有索引的查询,必须加载整个表的数据页到Buffer Pool,在没有索引的情况下查找特定数据会产生大量随机磁盘访问.非常的耗时间,如何更加高效的利用mysql的buffer pool呢?索引的作用正是为了解决这些问题.我们先来看看索引是什么:

1.2索引的理解

        常见的索引有主键索引,唯一键索引,普通索引与全文索引(主键索引和唯一键索引只要我们创建了相对应的键便会自动基于键列生成索引).我们之前说过,表在不给主键的时候,会默认生成索引,所以表默认是有索引的也就是主键索引.我们先来看一个对于主键索引的样例:

create table if not exists user (
    id int primary key, --一定要添加主键哦,只有这样才会默认生成主键索引
    age int not null,
    name varchar(16) not null
);
mysql> show create table user \G
*************************** 1. row ***************************
Table: user
Create Table: CREATE TABLE `user` (
    `id` int(11) NOT NULL,
    `age` int(11) NOT NULL,
    `name` varchar(16) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 --默认就是InnoDB存储引擎
1 row in set (0.00 sec)

然后我们插入一组无序的数据:

--插入多条记录,注意,我们并没有按照主键的大小顺序插入
mysql> insert into user (id, age, name) values(3, 18, '杨过');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(4, 16, '小龙女');
Query OK, 1 row affected (0.00 sec)
mysql> insert into user (id, age, name) values(2, 26, '黄蓉');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(5, 36, '郭靖');
Query OK, 1 row affected (0.00 sec)
mysql> insert into user (id, age, name) values(1, 56, '欧阳锋');
Query OK, 1 row affected (0.00 sec)

查看结果的时候却发现他们自动就是有序的了:

mysql> select * from user; --发现竟然默认是有序的!是谁干的呢?排序有什么好处呢?
+----+-----+-----------+
| id | age | name |
+----+-----+-----------+
| 1 | 56 | 欧阳锋 |
| 2 | 26 | 黄蓉 |
| 3 | 18 | 杨过 |
| 4 | 16 | 小龙女 |
| 5 | 36 | 郭靖 |
+----+-----+-----------+
5 rows in set (0.00 sec)

理解单张page 

ok在此中断一下,我们上面说了mysql与磁盘的基本交互单位为page,他是基于局部性原理这么做的.mysql中有许多表,而要管理好这些文件就需要先描述再组织,我们目前可以简单理解成一个个独立文件是有一个或者多个Page构成的。

        不同的 Page ,在 MySQL 中,都是 16KB ,使用 prev 和 next 构成双向链表.因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的。
        为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗?插入数据时排序的目的,就是优化查询的效率。页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的。也正是因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的。

理解多张page

        通过上面的分析,我们知道,上面页模式中,只有一个功能,就是整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。但是,我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据

        如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。那么,查找特定一条记录,也一定是线性查找。这效率也太低了。

页目录 

        我们平常看书的时候,在起始页面总有几页是目录.有了目录,我们便可以通过目录快速找到目标页.而表数据查询本质上是一个筛选淘汰的过程,其效率取决于每次查询能够排除的无效数据量。那么我们可不可以在mysql中也为每张表内部添加上目录结构,mysql就是这样做的(我们这里说的是innodb引擎):

        诚然,单张page在添加了目录之后,虽然损耗了一定的空间,但是极大的提高了在单张页中的查询效率.可是,多张page呢?软件设计中存在一个悖论:遇到无法解决的问题时,我们总是倾向于添加中间层来解决。若问题仍未解决,就继续叠加中间层。所以,我们可以在所有page的上一层,添加一层页目录,像多叉树那样:

但是如果多添加一层还不够呢?那就再加一层!:

        而多出来的所有层所有的page中,不存储数据(innodb),只存储目录.这其实也就是对高阶数据结构的使用-B+树.随便找一个id=?我们发现,现在查找的Page数一定减少了,也就意味着IO次数减少了,那么效率也就提高了。
所以我们可以总结一下,在innodb引擎中:

  • Page分为目录页和数据页。目录页只放各个下级Page的最小键值。
  • 查找的时候,自定向下找,只需要加载部分目录页到内存,即可完成算法的整个查找过程,大大减少了IO次数

 InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?

  • 链表当然是不行的,我们上面也说过.
  • 二叉搜索树?退化问题,可能退化成为线性结构
  • AVL &&红黑树?虽然是平衡或者近似平衡,但是毕竟是二叉结构,相比较多阶B+,意味着树整体过高,大家都是自顶向下找,层高越低,意味着系统与硬盘更少的IO Page交互。虽然你很秀,但是有更秀的。
  • Hash?官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持.Hash跟进其算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行,另外还有其他差别,有兴趣可以查一下。
  • B树?最值得比较的是 InnoDB 为何不用B树作为底层索引

B+ vs B

B树: 

B+树:

 

  • B树节点,既有数据,又有Page指针,而B+,只有叶子节点有数据,其他目录页,只有键值Page指针
  • B+叶子节点,全部相连,而B没有(利好范围查找).

所以我们就可以理解为什么选择B+树了:

  • 节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。
  • 叶子节点相连,更便于进行范围查找

Innodb和MyIsam都使用的是B+树,那么他们有什么区别呢?

聚簇索引(Innodb)与非聚簇索引(MyISAM)

MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM表的主索引, Col1 为主键:

其中, MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址。相较于 InnoDB 索引, InnoDB 是将索引和数据放在一起的

--终端A
mysql> create database myisam_test; --创建数据库
Query OK, 1 row affected (0.00 sec)
mysql> use myisam_test;
Database changed
mysql> create table mtest(
-> id int primary key,
-> name varchar(11) not null
-> )engine=MyISAM; --使用engine=MyISAM
Query OK, 0 rows affected (0.01 sec)
--终端B
[root@VM-0-3-centos mysql]# ls myisam_test/ -al --mysql数据目录下
total 28
drwxr-x--- 2 mysql mysql 4096 Jun 13 13:33 .
drwxr-x--x 13 mysql mysql 4096 Jun 13 13:32 ..
-rw-r----- 1 mysql mysql 61 Jun 13 13:32 db.opt
-rw-r----- 1 mysql mysql 8586 Jun 13 13:33 mtest.frm --表结构数据
-rw-r----- 1 mysql mysql 0 Jun 13 13:33 mtest.MYD --该表对应的数据,当前没有数
据,所以是0
-rw-r----- 1 mysql mysql 1024 Jun 13 13:33 mtest.MYI --该表对应的主键索引数据
--终端A
mysql> create database innodb_test; --创建数据库
Query OK, 1 row affected (0.00 sec)
mysql> use innodb_test;
Database changed
mysql> create table itest(
-> id int primary key,
-> name varchar(11) not null
-> )engine=InnoDB; --使用engine=InnoDB
Query OK, 0 rows affected (0.02 sec)
--终端B
[root@VM-0-3-centos mysql]# ls innodb_test/ -al
total 120
drwxr-x--- 2 mysql mysql 4096 Jun 13 13:39 .
drwxr-x--x 14 mysql mysql 4096 Jun 13 13:38 ..
-rw-r----- 1 mysql mysql 61 Jun 13 13:38 db.opt
-rw-r----- 1 mysql mysql 8586 Jun 13 13:39 itest.frm --表结构数据
-rw-r----- 1 mysql mysql 98304 Jun 13 13:39 itest.ibd --该表对应的主键索引和用户
数据,虽然现在一行数据没有,但是该表并不为0,因为有主键索引数据

        当然, MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。对于 MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复.

同样, InnoDB 除了主键索引,用户也会建立辅助(普通)索引,我们以上表中的 Col3 建立对应的辅助索引如下图:

可以看到, InnoDB 的非主键索引中叶子节点并没有数据,而只有对应记录的key值。所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询.

1.3索引的操作

我们这里只介绍下主键,唯一键与普通索引.其他的索引诸如全文索引读者可自行进行了解. 

1.3.1主键索引 

主键索引的创建方式一共有三种:

-- 在创建表的时候,直接在字段名后指定 primary key
create table user1(id int primary key, name varchar(30));

-- 在创建表的最后,指定某列或某几列为主键索引
create table user2(id int, name varchar(30), primary key(id));

create table user3(id int, name varchar(30));
-- 创建表以后再添加主键
alter table user3 add primary key(id);

主键索引的特点:

  • 一个表中,最多有一个主键索引,当然可以使符合主键
  • 主键索引的效率高(主键不可重复)
  • 创建主键索引的列,它的值不能为null,且不能重复
  • 主键索引的列基本上是int

1.3.2唯一索引

唯一索引创建方式也有三种:
 

-- 在表定义时,在某列后直接指定unique唯一属性。
create table user4(id int primary key, name varchar(30) unique);

-- 创建表时,在表的后面指定某列或某几列为unique
create table user5(id int primary key, name varchar(30), unique(name));

create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);

 唯一索引的特点:

  • 一个表中,可以有多个唯一索引
  • 查询效率高
  • 如果在某一列建立唯一索引,必须保证这列不能有重复数据
  • 如果一个唯一索引上指定not null,等价于主键索引

1.3.3普通索引 

create table user8(id int primary key,
    name varchar(20),
    email varchar(30),
    index(name) --在表的定义最后,指定某列为索引
);

create table user9(id int primary key, name varchar(20), email varchar(30));
alter table user9 add index(name); --创建完表以后指定某列为普通索引

create table user10(id int primary key, name varchar(20), email varchar(30));
-- 创建一个索引名为 idx_name 的索引
create index idx_name on user10(name);

普通索引的特点:

  • 一个表中可以有多个普通索引,普通索引在实际开发中用的比较多
  • 如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引

 

 

 



 

 

 

 


网站公告

今日签到

点亮在社区的每一天
去签到