目录
索引的理解
理解单个Page
理解多个Page
页目录
单页情况
多页情况
复盘一下
聚簇索引VS非聚簇索引
区别
索引操作
主键索引
唯一索引的创建
普通索引的创建
查询索引
删除索引
索引创建原则
索引的理解
理解单个Page
MySQL 中要管理很多数据表文件,而要管理好这些文件,就需要先描述,在组织 ,我们目前可以简单理解 成一个个独立文件是有一个或者多个Page构成的。
不同的 Page ,在 MySQL中,都是16KB,使用 prev 和 next 构成双向链表;
因为有主键的问题,MySQL 会默认按照主键给我们的数据进行排序,从上面的Page内数据记录可以看出,数据是有序且彼此关联的;
这个排序是MySQL自己做的
为什么数据库在插入数据时要对其进行排序呢?我们按正常顺序插入数据不是也挺好的吗? 插入数据时排序的目的,就是优化查询的效率。 页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的。 正式因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的
如果没有主键则插入什么顺序查找就是什么顺序
理解多个Page
通过上面的分析,我们知道,上面页模式中,只有一个功能,就是在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。但是我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。
如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。那么查找特定一条记录,也一定是线性查找,这效率也太低了。
页目录
我们在看《三国演义》这本书的时候,如果我们要看武松那一章节,找到该章节有两种做法:
1. 从头逐页的向后翻,直到找到目标内容2. 通过书提供的目录,发现指针章节在234页(假设),那么我们便直接翻到234页。同时,查找目录的方案,可以顺序找,不过因为目录肯定少,所以可以快速提高定位
本质上,书中的目录,是多花了纸张的,但是却提高了效率,所以,目录,是一种“空间换时间的做法”
单页情况
针对上面的单页Page,我们也当然可以引入目录;
page牺牲一部分空间用来存目录(目录不会很大),目录只有两个字段:第一个就是指向它起始记录的key值;第二个是有一个指针字段,指向记录的起始位置;
在一个Page内部,我们引入了目录。比如,我们要查找id=4记录,之前必须在数据记录里线性遍历4次, 才能拿到结果。现在直接通过目录2[3],直接进行定位新的起始位置,再通过指针找到这条记录,再往下遍历从而提高了效率(查找次数变少了)。我们不用直接在数据记录里查找了而是先在目录里找再在数据记录里找;
所以只有数据有序了才能方便引入页内目录;就相当于一本书也是有序的,才能有目录;
多页情况
MySQL 中每一页的大小只有 16KB ,单个Page大小固定,所以随着数据量不断增大, 16KB不可能存下所有的数据,那么必定会有多个页来存储数据。
在单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来。page之间是线性连接的,
需要注意上面的图是理想结构,大家也知道,目前要保证整体有序,那么新插入的数据,不一定会在新Page上面,这里仅仅做演示。
这样,我们就可以通过多个Page遍历,Page内部通过目录来快速定位数据。可是,貌似这样也有效率问题,在Page之间,也是需要 MySQL 遍历的(线性连接),遍历意味着依旧需要进行大量的IO(比如我要找的数据在结尾,那么我就要遍历所有page太费时),将下一个Page加载到内存,进行线性检测(在内存中才能遍历,遍历本身就是线性检测,效率低下)。这样就显得我们之前的Page内部的目录,有点杯水车薪了。
那么如何解决呢?解决方案,其实就是我们之前的思路,给Page也带上目录。
1. 使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值。(再创page存目录,这个page不存数据只存目录)
2. 和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。
3. 其中,每个目录项的构成是:键值+指针。图中没有画全。
存在一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的数据(既最小主键值)。有数据,就可通过比较并且找到该访问那个Page,进而通过指针,找到这个Page。其实目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
那么当子page存储的数据多了,上级目录也便多了,那么从宏观上来讲是不是也是遍历查找,那么我们可以再在上级目录加目录,可以在加目录页: 这就是传说中的B+树(多叉树)啊!没错,至此,我们已经给我们的表构建完了主键索引。 随便找一个id=x我们发现,现在查找的Page数一定减少了,也就意味着IO次数减少了,那么效率也就提高了。
所以不是所有的索引都是用B+树存储的,但是主流是B+树不论哪个存储引擎都是;只有最底层的page之间用指针前后互连,上层的都不用;
叶子节点保存真实表中的数据,路上节点(沿途的节点)不保存数据,非叶子节点不要数据只要目录项,意味着可以存储更多的目录项,那么目录页可以管理更多的叶子page,那么这棵树就一定是矮胖树(未来搜索都是由根到叶子节点),如果比较矮胖那么途径路上节点变少(每个page都是从磁盘IO来的到内存中)则找到目标数据只需更少的page(减少了IO次数,提高了效率),每一个节点都有目录项可以提高搜索效率;
所以我们把上面的树叫做mysql innode db下的索引结构!一般我们建表插入数据的时候,就是在该结构下CURD,即使自己没有设置主键也是这样的结构,因为mysql会自己设置主键(mysql设置的主键是隐藏列的),所以根据主键搜索快,不是主键搜索慢
叶子节点全部用链表互联起来,那为什么叶子节点是互联呢?首先,这是B+树的特点;其次我们比较希望进行范围查找
复盘一下
1. Page分为目录页和数据页。目录页只放各个下级Page的最小键值。
2. 查找的时候,自定向下找,只需要加载部分目录页到内存,即可完成算法的整个查找过程,大大减少了IO次数
InnoDB 在建立索引结构来管理数据的时候,其他数据结构为何不行?
链表?线性遍历
二叉搜索树(二叉搜索树一定是瘦高状的)?退化问题,可能退化成为线性结构
AVL &&红黑树?虽然是平衡或者近似平衡,但是毕竟是二叉结构,相比较多阶B+,意味着树整体 过高,大家都是自顶向下找,层高越低,意味着系统与硬盘更少的IO Page交互。虽然它可以解决问题但是有更优秀的。
Hash?官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持.Hash跟进其算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行,另外还有其他差别,有兴趣可以查一下。
B树?最值得比较的是 InnoDB 为何不用B树作为底层索引?
B树
B+树
那么我们了解B+树非叶子节点不存储数据,数据全部都在叶子节点并且叶子节点全部用链式结构连接起来;
B树呢除了叶子节点有数据,其他节点也存储数据并且叶子节点不会互联;
那为什么选择B+树不选B树呢?首先因为MySQL认为如果你在节点里加了数据那么所保存的目录项变少了,那么一个目录页所管理的page变少了那么相对的B树高度会高一点,那么可能需要更多的IO,IO的时间成本比算法的时间成本是更高的;其次B树叶子节点没有整体相连,那么我们进行范围查找的时候需要再次遍历这颗B树则注定了需要每次都要查B树,可能又需要IO因为有的配置可能不在内存中,需要IO到内存中,不像B+树找到起始位置既可线性遍历
总之:B树节点既有数据又有Page指针,而B+只有叶子节点有数据,其他目录页只有键值和 Page指针 ;B+叶子节点全部相连,而B没有;
为何选择B+ :节点不存储data,这样一个节点就可以存储更多的key从而可以使得树更矮,所以IO操作次数更少。 叶子节点相连,更便于进行范围查找;
聚簇索引VS非聚簇索引
MyISAM存储引擎-主键索引
MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM表的主索引,Col1 为主键。叶子节点本来存数据但是现在存这条记录的地址;
MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据 的地址。相较于 InnoDB 索引, InnoDB 是将索引和数据放在一起的。
MyISAM 这种用户数据与索引数据分离的索引方案,叫做非聚簇索引。InnoDB 这种用户数据与索引数据在一起索引方案,叫做聚簇索引;
区别
mysql> create table test1(
id int primary key,
name varchar(20) not null
)engine=innodb;
现在使用的是mysql客户端,访问的是mysqld服务端,把sql交给sql建表;
一张表对应两个文件,frm对应表结构数据,idb该表对应的主键索引和用户数据
mysql> create table test2(
id int primary key,
name varchar(20) not null
)engine=myisam;
一张表对应三个文件;frm对应表结构数据,MYD该表对应的数据,当前没有数 据,所以是0,MYI该表对应的主键索引数据
当然, MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引。
对于 MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复。
索引的本质就是数据结构
同样 InnoDB 除了主键索引,用户也会建立辅助(普通)索引,我们以上表中的 Col3 建立对应的辅助索引如下图:
可以看到, InnoDB 的非主键索引中叶子节点并没有数据,而只有对应记录的key值。
所以通过辅助(普通)索引,找到目标记录,需要两遍索引:首先检索辅助索引获得主键,然后用主键 到主索引中检索获得记录。这种过程,就叫做回表查询
为何 InnoDB 针对这种辅助(普通)索引的场景,不给叶子节点也附上数据呢?原因就是太浪费空间了,存两份没必要导致回表。
只要有主键必定有索引;如果你构建了主键,那么你的表就会配上主键索引;如果主键建立好,未来你可能会对其他列设定索引的话,可以手动添加,添加后在mysql内部重新构建B+树,只不过innodb对应的是主键值方便我们快速索引,mynisam就会直接指向记录;一张表可能会对应多颗B+树;
索引操作
索引一般分为:主键,唯一键,普通索引;
主键索引
第一种方式:
在创建表的时候直接在字段名后指定
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
唯一索引的创建
第一种方式:
在表定义时,在某列后直接指定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 test(id int primary key, name varchar(30));
alter table user1 add unique(name);
两颗B+树,第一个是primary打在id列是btree,第二个是索引名称叫name(以列名为名称),打在name列,类型BTREE
唯一索引的特点: 一个表中可以有多个唯一索引;查询效率高 ;如果在某一列建立唯一索引,必须保证这列不能有重复数据 ;如果一个唯一索引上指定not null,等价于主键索引;
普通索引的创建
第一种方式
mysql> create table test3(
id int primary key,
name varchar(20) not null,
email varchar(30),
index(name)
);在表的定义最后,指定某列为索引
第二种方式
create table user3(id int primary key, name varchar(20), email varchar(30));
alter table user3 add index(name); --创建完表以后指定某列为普通索引
创建多列为索引:mysql> alter table test3 add index(name,email);但还是创建一个B+树,他俩合并才是一个B+树;多列的话默认使用第一个为B+树的名称,我这里因为已经有了名称为name的B+树,所以会显示name_2
它俩共用一个B+树,合起来才是完整的一个mysql> alter table test3 drop index name_2;发现没有3和4了只有1和2了
我们把多列构建起来的索引叫做复合索引。通过复合索引可以避免回表查询,例如我要通过name找email,拿到主键值就返回了
第三种方式
create table user10(id int primary key, name varchar(20), email varchar(30));
-- 创建一个索引名为 idx_name 的索引
create index idx_name on user10(name);
普通索引的特点: 一个表中可以有多个普通索引,普通索引在实际开发中用的比较多 ;如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引;
查询索引
第一种方法: show keys from 表名\G
第二种方法: show index from 表名\G
索引名叫primary,打在id列,索引类型是BTREE(B+)
第三种方法(信息比较简略): desc 表名;
删除索引
第一种方法-删除主键索引: alter table 表名 drop primary key;
第二种方法-其他索引(除了主键以外的)的删除:
alter table 表名 drop index 索引名; 索引名就是show keys from 表名中的 Key_name 字段
mysql> alter table user10 drop index idx_name;
第三种方法方法: drop index 索引名 on 表名
mysql> drop index name on user8;
索引创建原则
1. 比较频繁作为查询条件的字段应该创建索引
2. 唯一性太差的字段不适合单独创建索引(比如性别就只有男女,B+树构建了也不是好的B+树),即使频繁作为查询条件
3. 更新非常频繁的字段不适合作创建索引(改动太频繁了,索引结构可能变化)
4. 不会出现在where子句中的字段不该创建索引
就比如查找员工原来先行检测需要十几秒,加了索引构建B+树它不在建立而是查找索引当然快;