《MySQL:MySQL索引特性》
索引理解
索引:提高数据库的性能。不用加内存,不用改程序,不用调sql,只要执行正确的create index,查询速度就可能提高成百上千倍。但是,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。所以,索引的价值在于提高一个海量数据的检索速度。
常见索引分为
- 主键索引(primary key)
- 唯一索引(unique)
- 普通索引(index)
- 全文索引(fultext)——用于解决文本数据中快速搜索特定关键词或短语的问题
认识磁盘
关于磁盘的基本了解在下面这篇文章中有提到。下面再简单说明一下。
【Linux】基础I/O -> 如何谈文件与文件系统?-CSDN博客
MySQL与存储
MySQL给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中。磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的,再加上IO本身的特征(大部分时间都是在等外设准备好),可以知道,如何提高效率,是MySQL的一个重要话题。
了解磁盘
再看看磁盘中一个盘面。
扇区。
数据库文件,本质就是保存在磁盘的盘片中。其实也就是扇区。数据库文件很大很多,一定需要占据多个扇区。
在使用Linux时,所看到的大部分(部分内存文件系统除外)目录或者文件,其实就是保存在硬盘当中的。
可以ls /var/lib/mysql查看自己mysql中的文件。
找到一个文件的全部,就是在磁盘找到所有保存文件的扇区。能够定位任何一个扇区,就能找到所有的扇区(因为查找方式是一样的)。
定位扇区
- 柱面(磁道):多盘磁盘,每盘都是双面,大小完全相等。同半径的磁道整体上就构成了一个柱面。
- 每个盘面都有一个磁头,磁头和盘面的对应关系是1对1。
- 只需要知道,柱面(cylinder等价于磁道)、磁头(heads)、扇区(sector)对应的编号,就可以在磁盘上定位所要访问的扇区。这种磁盘数据定位方式叫做“CHS”定位法。但是,在实际系统软件使用的并不是CHS(硬件是),而是LBA,一种线性地址(可以想象成物理地址与虚拟地址)。系统讲LBA地址最后会转化成CHS,交给磁盘去进行数据读取。
现在能够在硬件层面定位任何一个基本数据块(扇区)了。那么在系统软件上,就直接按照扇区512字节(部分4096字节)进行IO交互吗?不是。
1、如果OS直接使用硬件提供的数据大小进行交互,那么系统IO代码就和硬件强相关,如果硬件发生变化,系统必须也跟着变化。
2、单次IO只有512字节,太小了。IO单位小,意味着读取同样的数据内容,需要访问磁盘的次数就多,效率就低。
3、类比文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位就不是扇区,是数据块。
即,系统读取磁盘,是以块为单位的,基本单位是4KB。
磁盘随机访问与连续访问
随机访问:本次IO所给出的扇区地址和上次IO给出的扇区地址不连续,这样磁头在两次IO操作之间需要做比较大的移动动作才能重新开始读写数据。
连续访问:如果本次IO给出的扇区地址和上次IO结束的扇区地址是连续的,那磁头就能很快开始这次IO操作,不需要做较大的移动,这样的多个IO操作称为连续访问。
尽管相邻的两次IO操作在同一时刻发出,但如果它们请求的扇区地址相差很大的话也只能随机访问,而不能连续访问。
磁盘是通过机械运动进行寻址的,随机访问不需要过多的定位,故效率比较高。
MySQL与磁盘交互基本单位
MySQL作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率,MySQL进行IO的基本单位是16KB(下面统一使用InnoDB存储引擎来作说明)。
163849byte/1024=16KB。
也就是说,磁盘这个硬件设备的基本单位是512字节,系统和磁盘进行交互是以块4KB为单位进行交互,而MySQL InnoDB引擎使用16KB和磁盘进行IO交互。
即,MySQL和磁盘进行数据交互的基本单位是16KB。这个基本数据单元,在MySQL这里叫做page(和系统的page不同,不要混肴)。
几点说明
1、MySQL中的数据文件,是以Page为单位保存在磁盘当中的。
2、MySQL的CURD操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。
3、只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中,CURD的操作都是在内存中进行的。
4、所以在特定时间内,数据一定是磁盘中有,内存中也有。后续后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。
5、为了更好的进行上面的操作,MySQL服务器在内存中运行的时候,在服务器内部,就申请了被称为Buffer Pool的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。
6、为了更高的效率,一定要尽可能的减少系统和磁盘IO的次数。
- 数据在Buffer Pool中进行CURD
- 在Buffer Pool发生变化的数据刷新到OS的文件缓冲区
- OS以16KB刷新到磁盘
磁盘数据搬到内存做数据处理,内存数据搬到磁盘做持久化。
索引理解
建立测试表。
插入多条记录。
查看查询结果。
发现,记录竟然是按照主键大小经过排序的!
为何MySQL与磁盘进行IO交互是以Page为单位?
为何MySQL和磁盘进行IO交互的时候,要采用Page的方案进行交互呢?用多少,加载多少不行吗?
- 如上面的5条记录,如果MySQL要查找id=2的记录,第一次加载id=1,第二次加载id=2,一次一条记录,那么就需要2次IO。如果要找id=5,就需要5次IO。
- 但是,如果着5条(或者更多)都被保存在一个Page中(16KB,能保存很多记录),那么第一次IO查找id=2时,整个Page都会被加载到MySQL的Buffer Pool中,这里完成了一次IO。但是往后如果在查找id=1,3,4,5等记录时,完全不需要进行IO了,而是直接在内存中进行了。所以,就在单Page里面,大大减少了IO的次数。
怎么保证,用户下一次找的数据,就在这个Page里面?
- 不能严格保证,但是有很大概率,因为有局部性原理。
往往IO效率低下的最主要矛盾不是IO单次数据量的大小,而是IO的次数。
预加载机制可以有效利用局部性原理,减少IO次数,提高效率。
理解单个Page
MySQL内部,一定存在大量的Page,也就决定了MySQL必须要将多个同时存在的Page管理起来,要管理所有的MySQL内的Page,需要“先描述,再组织”。
所以,不要简单的将Page认为是一个内存块,Page内存也必须写入对应的管理信息!
struct page
{
struct page * next;struct page * prev;
char buffer[NUM]; //16KB
// ...
};
将所有的page用“链表”的形式管理起来,在Buffer pool内部,对MySQL中的Page进行了一个建模!
不同的Page,在MySQL中都是16KB,使用prev和next构成双向链表。
因为有主键的问题,MySQL会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的。
为什么MySQL在插入数据时要对其进行排序呢?按照正常顺序插入数据不好吗?
插入数据时排序的目的,就是优化查询的效率。
页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询效率是必须的。
正是因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,可以提前结束查找过程的。
主要是为了方便建立目录。
理解多个Page
通过上面的分析,我们知道,上面页模式中,只有一个功能,就是在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。但是,我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。
如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。那么,查找特定一条记录,也一定是线性查找。效率也太低了。
页目录
我们在看《C语言程序设计》这本书的时候,如果要看“指针章节”,找到该章节有两种做法。
1、从头逐页的向后翻,直到找到目标内容。
2、通过书提供的目录,发现“指针章节”在123页,那么我们便可以直接翻到234页。同时,查找目录的方案,可以顺序找,不过因为目录肯定少,所以可以快速提高定位。
本质上,书中的目录,是多花了纸张的(本来可以写更多的内容),但是却提高了查询的效率。
所以,目录,是一种“以空间换时间的做法”。
单页情况
针对上面的单页Page,是否也能引入目录呢?当然可以。
当前,在一个Page内部,就引入了目录。比如,要查找id=4的记录,之前必须线性遍历4次才能拿到结果。现在直接通过目录2[3],直接进行快速定位新的其实位置,提高了效率。
此时,就可以回答上面的问题了,为什么MySQL通过键值会自动排序?
可以很方便的引入目录。
多页情况
MySQL中每一页的大小只有16KB,单个Page大小固定,所以随着数据量不断增大,16KB不可能存下所有的数据,那么必定会有多个页来存储数据。
在单表数据不断被插入的情况下,MySQL会在容量不足的时候,自动开辟新的Page来存储新的数据,然后通过指针的方式,将所有的Page组织起来。
(注意:上面的图,是理想结构,要保证整体有序,那么新插入的数据,不一定会在新Page上面,这里仅仅做演示。)
这样,我们就可以通过多个Page遍历,Page内部通过目录来快速定位数据。可是,貌似这样也有效率问题,在Page之间,也是需要MySQL遍历的,遍历就意味着依旧需要进行大量的IO,将下一个Page加载到内存,进行线性检测。这样,有没有更好的办法进一步提升效率呢?
给Page也带上目录。
- 使用一个目录项来指向某一页,而这个目录项中存放的就是将要指向的页中存放的最小数据的键值。
- 和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。
- 其中,每个目录项的构成是:键值+指针。(图中没有画指向页的指针)
存在一个目录页来管理页目录,目录页中的目录项存放的数据就是指向的那一页中最小的数据。有数据,就可以通过比较,找到要访问的那个Page,进而通过指针,找到下一个Page。
目录页的本质也是页,普通页中存放的数据是用户数据,而目录页中存放的数据是普通页的地址和普通页最小数据的键值。
但是,每次检索数据的时候,该从哪里开始呢?虽然顶层的目录页少了,还是需要遍历啊。此时,可以再加目录页。
这就是B+树结构!至此,就给表user构建完了主键索引。
此时,随便找个一个id,查找的Page数一定减少了,也就意味着IO次数减少了,那么效率也就提高了。
- Page分为目录页和数据页。目录页只存放下级Page的最小键值和地址。
- 查找的时候,自顶向下找,磁盘只需要加载部分目录页到内存,即可完成算法的整个查找过程,大大减少了IO次数。
InnoDB在建立索引结构来管理数据的时候,其他数据结构为何不行?
- 链表:线性遍历。
- 二叉搜索树:退化问题,有可能退化成线性结构,线性遍历。
- AVL&&红黑树:虽然平衡或近似平衡,但是毕竟是二叉结构,相比多阶B+,意味着整体树过高,都是自顶向下查找,层高越低,意味着系统与硬盘IO,加载到内存的Page越少,也就是IO次数越少,效率越高。
- Hash:官方的索引实现方式,MySQL是支持Hash的,不过InnoDB和MyISAM不支持。Hash根据其算法特征,决定了查找时间也很快O(1),但是,在面对范围查找时就明显不行(尽管查找的数据相邻,但是依旧需要Hash查找)。
- B树:?
InnoDB为何不用B树作为底层索引?
B树结构:
B+树结构:
- B树节点,既有键值和Page指针,又有数据;而B+树,只有叶子节点有数据,其他目录页,只有键值和Page指针。
- B+树的叶子节点,全部相连;而B树没有。
为何选择B+?
1、非叶子节点不存储数据,意味着一个节点就可以存储更多的目录项,也就是可以管理更多的Page。所以,这棵树一定是“矮胖型”的树!查找的时候,途径的节点就少(每途径一个节点就意味着一个Page要加载到内存,要进行一次IO),找到目标数据只需要更少的Page,IO次数就少了,效率就高了。
2、叶子节点全部使用链表相连,更便于进行范围查找。
聚簇索引和非聚簇索引
MyISAM存储引擎:主键索引。
MyISAM引擎,同样使用B+树作为索引结果,叶子节点的data域存放的是数据记录的地址。下图为MyISAM的主键索引,Col1为主键。
MyISAM存储引擎的最大特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址。
而InnoDB存储引擎,是将索引Page和数据Page放在一起的。
像InnoDB这种,用户数据和索引数据在一起的索引方案,叫做聚簇索引。
像MyISAM这种,用户数据和索引数据分离的索引方案,叫做非聚簇索引。
MySQL除了默认会建立主键索引外,用户也有可能按照其他列信息建立索引,一般这种索引叫做辅助(普通)索引。
对于MyISAM,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可以重复。
下图就是基于MyISAM的Col2建立的索引,和主键索引没有差别。
InnoDB除了主键索引,用户也会建立辅助(普通)索引,以上表中的Col3建立对应的辅助索引如图。
InnoDB的非主键索引中叶子节点并没有数据,而只有对应记录的key值。
所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键到主键索引中检索获得记录。这种过程,叫做回表查询。
为什么InnoDB针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?
原因是:太浪费空间了。
索引操作
查询索引
- 方式一:show keys from 表名
- 方式二:show index from 表名
- 方式三:desc 表名(显示出的信息简略)
删除索引
- 删除主键索引:alter table 表名 drop primary key
- 删除其他索引:alter table 表名 drop index 索引名;索引名就是show keys from 表名中的Key_name字段
- 删除其他索引:也可以drop index 索引名 on 表名
创建主键索引
- 方式一
//创建表的时候,直接在字段名后指定primary key
create table user1(id int primary key, name varchar(20));
- 方式二
//在创建表的最后,指定某列或几列为主键索引
create table user2(id int, name varchar(20), primary key(id));
- 方式三
create table user3(id int, name varchar(20));
//创建表以后再添加主键索引
alter table user3 add primary key(id);
创建主键索引的操作和创建主键的操作是一样的,创建了一个主键,MySQL会默认给这个主键创建索引。
主键索引特点
- 一个表中,最多有一个主键索引,可以使用复合主键
- 主键索引效率高
- 创建主键索引的列,不能为null,不能重复
- 主键索引的列基本上是int
演示:
创建主键索引。
查询主键索引。
删除主键索引。
添加主键索引。
创建唯一索引
- 方式一
//创建表的时候,直接在字段名后指定unique唯一属性
create table user4(id int primary key, name varchar(20) unique);
- 方式二
//在创建表的最后,指定某列或几列为主键索引
create table user5(id int, name varchar(20), unique(name));
- 方式三
create table user6(id int, name varchar(20));
//创建表以后再添加唯一索引
alter table user6 add unique(name);
唯一索引特点
- 一个表中,可以有多个唯一索引
- 查询效率高
- 如果在某一列建立唯一索引,必须保证这列不能有重复数据
- 如果一个唯一索引指定not null,等价于主键索引
演示:
创建唯一索引。
查询唯一键索引。
删除唯一键索引。
查询唯一键索引。
删除了唯一键索引,只剩下主键索引了。
添加唯一键索引。
删除唯一键索引。
创建普通索引
- 方式一
//在表定义最后,指定某列为普通索引
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);
普通索引的特点
- 一个表中可以有多个普通索引,普通索引实际开发用的比较多
- 如果某列需要创建索引,但是该列有重复的值,就应该使用普通索引
演示:
创建普通索引。
查询普通索引。
删除普通索引。
添加普通索引。
创建一个索引名为idx_name的索引。
创建全文索引
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中午。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)。
有如下articles表。
查询有没有database数据。
使用这种方式,虽然查到了数据,但是没有使用全文索引。
可以使用explain工具看一下是否使用到索引。
下面使用全文索引。
使用explain分析这个sql语句。
复合索引(了解)
复合索引是将多个列组合在一起创建的索引,数据库系统会根据这些列的顺序来存储和排序数据,以加速基于这些列的查询操作。
创建复合索引。
查询复合索引。
删除复合索引。
工作原理
复合索引会按照创建索引时指定的列顺序,对数据进行排序和存储。例如,若在列name和列email上创建了复合索引(name,email),MySQL首先会按照列name的值进行排序,当列name的值相同时,再按照列email的值进行排序。查询时,MySQL会根据查询条件从索引中快熟定位符合条件的数据。
复合索引特点
- 对于涉及多个列的查询条件,复合索引可以一次性定位到符合条件的数据,减少了MySQL的扫描范围。
- 覆盖索引:如果查询的所有列都包含在复合索引中,MySQL可以直接从索引中获取所需数据,而无需再访问数据行,进一步提高查询效率
最左匹配原则
在使用复合索引时,查询条件必须从索引的最左边列开始,并且按照索引定义的列顺序依次使用。例如,复合索引(name,email),查询条件必须为where name='张三' and emial='1234@qq.com'。