概述
对于MYSQL的INNODB存储引擎的索引,大家是不陌生的,都能想到是 B+树结构,可以加速SQL查询。但对于B+树索引,它到底“长”得什么样子,它具体如何由一个个字节构成的,这些的基础知识鲜有人深究。本篇文章从MYSQL行记录开始说起,层层递进,包括数据页,B+树聚簇索引,B+树二级索引,最后在文章末尾给出MYSQL索引的建议。
表空间
首先,我们来了解一下 MySQL 的表空间。在 MySQL 中,所有的数据都被存储在一个空间内,称之为表空间,表空间内部又可以分为段(segment)、区(extent)、页(page)、行(row),其逻辑结构如下图:
段(segment)
表空间是由不同的段组成的,常见的段有:数据段,索引段,回滚段等等,在 MySQL 中,数据是按照 B+ 树来存储,因此数据即索引,因此数据段即为 B+ 树的叶子节点,索引段为 B+ 树的非叶子节点,回滚段用于存储undo日志,用于事务失败后数据回滚以及在事务未提交之前通过undo日志获取之前版本的数据,在 InnoDB 1.1
版本之前,一个 InnoDB 只支持一个回滚段,支持 1023 个并发修改事务同时进行,在 InnoDB 1.2
版本,将回滚段数量提高到了 128 个,也就是说可以同时进行128 * 1023
个并发修改事务。
区(extent)
区是由连续页组成的空间,每个区的固定大小为 1MB,为保证区中页的连续性,InnoDB 会一次从磁盘中申请 4 ~ 5 个区,在默认不压缩的情况下,一个区可以容纳 64 个连续的页。但是在开始新建表的时候,空表的默认大小为 96KB,是由于为了高效的利用磁盘空间,在开始插入数据时表会先利用 32 个页大小的碎片页来存储数据,当这些碎片使用完后,表大小才会按照 MB 倍数来增加。
页(page)
页是 InnoDB 存储引擎的最小管理单位,每页大小默认是 16KB,从 InnoDB 1.2.x
版本开始,可以利用innodb_page_size来改变页大小,但是改变只能在初始化 InnoDB 实例前进行修改,之后便无法进行修改,除非mysqldump
导出创建新库,常见的页类型有:数据页、undo页、系统页、事务数据页、插入缓冲位图页、插入缓冲空闲列表页、未压缩的二进制大对象页以及压缩的二进制大对象页等。
行(row)
行对应的是表中的行记录,每页存储最多的行记录也是有硬性规定的最多16KB/2-200
,即 7992 行,其中 16KB 是页大小。
Clustered Index 聚簇索引
MySQL InnoDB 引擎具有强制聚簇索引,通常使用主键。也就是主键就是Clustered Index,如果没有主键按以下规则生成。
Clustered Index 条件优化级:
-
表有明确的PRIMARY KEY:使用PRIMARY KEY
-
无PRIMARY KEY:InnoDB 默认使用第一个 UNIQUE INDEX,且索引列需要全部定义为非空列(NOT NULL)作为Clustered Index
-
如无PRIMARY KEY,也没有合适的UNIQUE INDEX,InnoDB将会在包含行ROW ID的合成列上生成一个名为GEN_CLUST_INDEX的隐藏Clustered Index
ROW ID:ROW ID是6 byte字段,由InnoDB分配,用于行排序。插入新行而单调增加,在物理上插入按ROW ID顺序排列
注:UNIQUE INDEX 包含的列需要全部定义为NOT NULL非空,才会被当做Clustered Index
MyISAM 存储引擎不支持聚簇索引并且一直使用堆表
2. 聚簇索引如何加速查询
通过聚簇索引访问行很快,因为索引搜索直接指向包含行数据的页面。如果表很大,与使用与索引记录不同的页来存储行数据的存储组织相比,聚簇索引架构通常可以节省磁盘 I/O 操作。
3. Clustered Index 示例及查询:
INNODB_INDEXES 表type字段说明:
-
0 = 非唯一索引的二级索引 :nonunique secondary index;
-
1 = 自动生成的聚簇索引:automatically generated clustered index (GEN_CLUST_INDEX);
-
2 = 唯一索引(非聚簇索引): unique nonclustered index;
-
3 = 聚簇索引 clustered index;
-
32 = 全文索引 full-text index
不同MySQL版本表名不同,使用命令查询:SHOW TABLES FROM INFORMATION_SCHEMA LIKE 'INNODB_%';
自动生成名为GEN_CLUST_INDEX的Clustered Index示例:
-- 创建无主键、无唯一索引
CREATE TABLE `clustered_index_demo` (`id` int DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;-- 查询表索引
-- 如5.7以下版本表名不同,使用命令查询:SHOW TABLES FROM INFORMATION_SCHEMA LIKE 'INNODB_SYS%';
SELECT t2.INDEX_ID ,t2.`NAME` , t2.TABLE_ID , t2.`TYPE` , t2.N_FIELDS , t2.PAGE_NO , t2.`SPACE` , t2.MERGE_THRESHOLD
FROM information_schema.INNODB_TABLES t1
INNER JOIN information_schema.INNODB_INDEXES t2 ON t1.TABLE_ID = t2.TABLE_ID
WHERE t1.`NAME` = 'wiki/clustered_index_demo';-- 查询结果| INDEX_ID | NAME | TABLE_ID | TYPE | N_FIELDS | PAGE_NO | SPACE | MERGE_THRESHOLD |
|----------|-----------------|----------|------|----------|---------|-------|-----------------|
| 3616 | GEN_CLUST_INDEX | 3276 | 1 | 5 | 4 | 2113 |
增加包含NOT NULL列的唯一索引示例:
Tips : 修改表结构,InnoDB将删除原自动生成的GEN_CLUST_INDEX索引
-- 增加两列
ALTER TABLE `wiki`.`clustered_index_demo`
ADD COLUMN `username` varchar(32) NOT NULL,
ADD COLUMN `name` varchar(64) NOT NULL;
-- 增加唯一索引
ALTER TABLE `wiki`.`clustered_index_demo`
ADD UNIQUE INDEX `IDX_UNIQUE` (`username`,`name`) USING BTREE;| INDEX_ID | NAME | TABLE_ID | TYPE | N_FIELDS | PAGE_NO | SPACE | MERGE_THRESHOLD |
|----------|------------|----------|------|----------|---------|-------|-----------------|
| 3620 | IDX_UNIQUE | 3278 | 3 | 5 | 4 | 2115 | 50 |
唯一索引包含NULL列
-- 将唯一索引,其中一列改为NULL, Clustered Index将被删除,重新生成GEN_CLUST_INDEX
ALTER TABLE `wiki`.`clustered_index_demo`
CHANGE `username` `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
CHANGE `name` `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;| INDEX_ID | NAME | TABLE_ID | TYPE | N_FIELDS | PAGE_NO | SPACE | MERGE_THRESHOLD |
|----------|-----------------|----------|------|----------|---------|-------|-----------------|
| 3625 | GEN_CLUST_INDEX | 3281 | 1 | 6 | 4 | 2118 | 50 |
| 3626 | IDX_UNIQUE | 3281 | 2 | 3 | 5 | 2118 |
查询所有自动生成的Clustered Index
SELECTi.TABLE_ID,t.NAME
FROMinformation_schema.INNODB_INDEXES iJOIN information_schema.INNODB_TABLES t ON (i.TABLE_ID = t.TABLE_ID)
WHEREi.NAME = 'GEN_CLUST_INDEX';| TABLE_ID | NAME |
|----------|---------------------------|
| 3281 | wiki/clustered_index_demo |
辅助索引
除了聚簇索引之外的索引都可以称之为辅助索引,与聚簇索引的区别在于辅助索引的叶子节点中存放的是主键的键值。一张表可以存在多个辅助索引,但是只能有一个聚簇索引,通过辅助索引来查找对应的航记录的话,需要进行两步,第一步通过辅助索引来确定对应的主键,第二步通过相应的主键值在聚簇索引中查询到对应的行记录,也就是进行两次 B+ 树搜索。相反,通过辅助索引来查询主键的话,遍历一次辅助索引就可以确定主键了,也就是所谓的索引覆盖,不用回表。
创建辅助索引,可以创建单列的索引,也就是用一个字段来创建索引,也可以用多个字段来创建副主索引称为联合索引,创建联合索引后,B+ 树的节点存储的键值数量不是 一个,而是多个,如下图:
-
联合索引的 B+ 树和单键辅助索引的 B+ 树是一样的,键值都是排序的,通过叶子节点可以逻辑顺序的读出所有的数据,比如上图所存储的数据时,按照(a,b)这种形式
(1,1),(1,2),(2,1),(2,4),(3,1),(3,2)
进行存放,这样有个好处,那就是存放数据时排序了,当进行order by对某个字段进行排序时,可以减少复杂度,加速进行查询; -
当用
select * from table where a=? and ?
可以使用索引(a,b)来加速查询,但是在查询时有一个原则,SQL 的where条件的顺序必须和二级索引一致,而且还遵循索引最左原则,select * from table where b=?
则无法利用(a,b)索引来加速查询。 -
辅助索引还有一个概念便是索引覆盖,索引覆盖的一个好处便是辅助索引不包含行记录,因此其大小远远小于聚簇索引,利用辅助索引进行查询可以减少大量的 IO 操作。
索引的优缺点及建议
优点:
-
对于等值查询,可快速定位到对于的行记录。
-
对于范围查询,可辅助缩小扫描区间。
-
当ORDER BY的列名 与 索引的列名完全一致时,可加快排序的顺序。
-
当GROUP BY的列名 与 索引的列名完全一致时,可加快分组。
-
当二级索引列中 包含了 SELECT 关键字后面写明的所有列,则在查询完成二级索引之后无需进行回表操作,直接返回即可。这种情况,称为【覆盖索引】。
缺点:
建立索引占用磁盘空间。
对表中的数据进行 增加,删除,修改 操作时,都需要修改各个索引树,特别是如果新增的行记录的主键顺序不是递增的,就会产生页分裂,页回收等操作,有较大的时间成本。
当二级索引列的值 的 不重复值的个数较少时,通过二级索引查询找到的数据量就会比较多,相应的就会产生过多的回表操作。
在执行查询语句的时候,首先要生成一个执行计划。通常情况下,一个SQL在执行过程中最多使用一个二级索引,在生成执行计划时需要计算使用不同索引执行查询时所需的成本,最后选择成本最低的那个索引执行查询。因此,如果建立太多的索引,就会导致成本分析过程耗时太多,从而影响查询语句的性能。
建议:
-
只为用于搜索,排序,分组的列创建索引。
-
索引的列需要有辨识性,尽可能地区分出不同的记录。
-
索引列的类型尽量小。因为数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以存放更多的记录,磁盘I/O带来的性能损耗也就越小。
-
如果需要对很长的字段进行快速查询,可考虑为列前缀建立索引。【alter table table_M add index idx_key1(column_n(10)) --> 将table_M表的 idx_key1列的前10个字符创建索引】
-
覆盖索引,当二级索引列中包含了SELECT关键字后面写明的所有列,则在查询完成二级索引之后无需进行回表操作,直接返回即可。因此,编写【select *】的时候,要想想是否必要
-
在查询语句中,索引列不要参与条件值计算,也是把条件值计算完成之后,再和索引列对比。【否则MYSQL会认为搜索条件不能形成合适的扫描区间来减少扫描的记录数量】