MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。如,以下面图所示:
其中不同的存储引擎对于索引结构的支持情况如图:
关于数据结构什么是树,b树,散列函数等做简单介绍不具实现按,推荐查阅严蔚敏写的数据结构书。
B-Tree是一种多路平衡查找树,相对于二叉树,B树每个节点可以有多个分支即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,(即B树每个节点最多存储4个key,5个指针):
树的度数指的是一个节点的子节点个数。
B+Tree是B-Tree的变种,以一颗最大度数(max-degree)为4(4阶,3个key,4个指针)的b+tree为例,其结构示意图:
B+Tree 与 B-Tree相比,主要有以下三点区别:
MySQL索引数据结构对经典的B+Tree进行了优化。在原B+Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能,利于排序。
哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。
特点
存储引擎支持
在MySQL中,支持hash索引的是Memory存储引擎。 而InnoDB中具有自适应hash功能,hash索引是InnoDB存储引擎根据B+Tree索引在指定条件下自动构建的。
为什么InnoDB存储引擎选择使用B+tree索引结构?
相对于二叉树,层级更少,搜索效率高;
对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
相对Hash索引,B+tree支持范围匹配及排序操作;
红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构,这一节将结合计算机组成原理相关知识讨论B-/+Tree作为索引的理论基础。
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。下面先介绍内存和磁盘存取原理,然后再结合这些原理分析B-/+Tree作为索引的效率。
目前计算机使用的主存基本都是随机读写存储器(RAM),现代RAM的结构和存取原理比较复杂,这里本文抛却具体差别,抽象出一个十分简单的存取模型来说明RAM的工作原理。
从抽象角度看,主存是一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据。每个存储单元有唯一的地址,现代主存的编址规则比较复杂,这里将其简化成一个二维地址:通过一个行地址和一个列地址可以唯一定位到一个存储单元。图5展示了一个4 x 4的主存模型。
主存的存取过程如下:
当系统需要读取主存时,则将地址信号放到地址总线上传给主存,主存读到地址信号后,解析信号并定位到指定存储单元,然后将此存储单元数据放到数据总线上,供其它部件读取。
写主存的过程类似,系统将要写入单元地址和数据分别放在地址总线和数据总线上,主存读取两个总线的内容,做相应的写操作。
这里可以看出,主存存取的时间仅与存取次数呈线性关系,因为不存在机械操作,两次存取的数据的“距离”不会对时间有任何影响,例如,先取A0再取A1和先取A0再取D3的时间消耗是一样的。
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。
程序运行期间所需要的数据通常比较集中。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
到这里终于可以分析B-/+Tree索引的性能了。
上文说过一般使用磁盘I/O次数评价索引结构的优劣。先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为\(O(h)=O(log_dN)\)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
上文还说过,B+Tree更适合外存索引,原因和内节点出度d有关。从上面分析可以看到,d越大索引的性能越好,而出度的上限取决于节点内key和data的大小:
\(d_{max}=floor(pagesize / (keysize + datasize + pointsize))\)
floor表示向下取整。由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。
在MySQL数据库,将索引的具体类型主要分为以下几类:主键索引、唯一索引、常规索引、全文索引。
在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:
聚集索引选取规则:
如果存在主键,主键索引就是聚集索引
如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。
聚集索引和二级索引的具体结构如下:
索引的叶子节点下挂的是这一行的数据 。
索引的叶子节点下挂的是该字段值对应的主键值。
当执行 select* from user where name = ‘Arm’,具体的查找过程如下:
由于是根据name字段进行查询,所以先根据name字段的二级索引中进行匹配查找。Lee—>Geek—>Arm,在二级索引中只能查找到 Arm 对应的主键值 10。
由于查询返回的数据是*(所有返回字段),所以还需要根据主键值10,到聚集索引中查找10对应的记录,15—>10最终找到10对应的行row。
最终拿到这一行的数据,直接返回即可。
回表查询:
先到二级索引中查找数据,找到主键值,再到聚集索引中根据主键值,这种获取数据的方式就称之为回表查询。
如何避免回表操作,可以使用覆盖索引(Covering Index)来优化查询。覆盖索引是指创建一个包含所有需要返回的列的复合索引,这样查询时就无需再次访问聚簇索引或数据页了。
例如,对于以下查询语句:
SELECT name, age FROM users WHERE gender = 'female';
如果我们创建了如下复合索引:
CREATE INDEX idx_users_gender ON users(gender, name, age);
则可以将查询语句改写为:
SELECT name, age FROM users USE INDEX (idx_users_gender) WHERE gender = 'female';
这样,在使用覆盖索引的情况下,MySQL只需要扫描一次idx_users_gender索引,并直接从该索引中返回结果集中所需的name和age列值,而不必再进行回表操作。这种方式可以显著提高查询性能并减少数据库负载。
此外,在设计表结构时还可以考虑将常用的字段放在聚簇索引中以避免回表操作。但是需要注意不能将过多字段放入聚簇索引中,否则可能会导致内存消耗过大或索引失效的问题。
创建索引:
create [unique | fulltext] index index_name on table_ name (index_col_name,...);
查看索引:
show index from table_name;
删除索引
drop index index_name on table_name;
基础数据准备
create table tb_user(
id int primary key auto_increment comment '主键',
name varchar(50) not null comment '用户名',
phone varchar(11) not null comment '手机号',
email varchar(100) comment '邮箱',
profession varchar(11) comment '专业',
age tinyint unsigned comment '年龄',
gender char(1) comment '性别 , 1: 男, 2: 女',
status char(1) comment '状态',
createtime datetime comment '创建时间'
) comment '系统用户表';
INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('吕布', '17799990000', '[email protected]', '软件工程', 23, '1','6', '2001-02-02 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('曹操', '17799990001', '[email protected]', '通讯工程', 33,'1', '0', '2001-03-05 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('赵云', '17799990002', '[email protected]', '英语', 34, '1','2', '2002-03-02 00:00:00');
INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('孙悟空', '17799990003', '[email protected]', '工程造价', 54,'1', '0', '2001-07-02 00:00:00');
......
INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('姜子牙', '17799990023', '[email protected]', '工程造价', 29,'1', '4', '2003-05-26 00:00:00');
为name创建常规索引(因为字段可能重复):create index idx_user_name on tb_user(name)
为phone字段创建唯一索引:create unique index idx_user_phone on tb_user(phone);
为profession、age、status创建联合索引:create index idx_user_pro_age_sta on tb_user(profession,age,status);
为email创建合适的索引来提升查询效率:create index idx_email on tb_user(email);
查看tb_user表的所有的索引数据:show index from tb_user;