『 数据库 』MySQL索引深度解析:从数据结构到B+树的完整指南
文章目录
- 1 前要
- 1.1 索引示例
- 2 硬件理解
- 3 MySQL 与磁盘交互的基本单位
- 4 简单理解索引
- 4.1 如何理解 page
- 4.2 为什么MySQL的IO交互需要以Page为单位进行
- 4.3 Slots 槽
- 4.4 理解多个 Page
- 4.5 为何不能采用其他数据结构充当索引
- 5 聚簇索引与非聚簇索引
- 5.1 回表查询
- 6 索引操作
- 6.1 创建主键索引 与 创建唯一键索引
- 6.2 查看索引
- 6.3 创建普通索引
- 6.4 删除索引
- 6.5 索引创建的原则
- 7 全文索引
1 前要
当 MySQL 服务运行在本地时, 本质上是在内存中, 而数据库中的操作都是内存与磁盘间的IO操作, 索引也如此;
而数据库的操作"增删查改"本身属于算法的一种, CPU与内存相互配合进行这些操作本身就会有时间开销;
通常情况下提高算法效率的主要因素包括:
-
组织数据的方式
-
算法本身
这里抛开算法本身不谈, 主要谈组织数据的方式;
- 以顺序表与链表为例
在数据结构中, 顺序表与链表都为线性的数据结构, 采用物理或者逻辑的方式使得数据与数据之间达成线性结构, 线性结构主要的优势是在查询(遍历)上, 其时间复杂度只需要O(N)级, 但在其他方面上由于组织数据的结构不同, 其他方面上将存在明显差异;
-
头部插入
-
顺序表
对于顺序表而言, 头部插入的时间复杂度为
O(N), 本质原因是, 顺序表为顺序结构, 当插入一个数据时, 其余的数据都将向后挪动一位, 需要挪动N位, 因此时间复杂度为O(N);
-
链表
对于链表而言, 头部插入时间复杂度为
O(1), 本质原因是, 链表在进行插入时只需要将新插入的节点作为头节点, 该头节点的next指针指向原本的头节点即可;
-
-
尾部插入
-
顺序表
对于顺序表而言, 尾部插入只需要直接通过下标访问并赋值(插入)即可, 时间复杂度为
O(1);
-
链表
对于链表而言, 尾部插入无法像顺序表一样直接通过下标索引的方式来对对应的控件进行访问, 因此其需要不停向后进行遍历, 直至找到尾结点(其
next指针指向NULL), 再进行插入, 因此时间复杂度为O(N);
-
而本质上的索引即为, 将数据根据所定义索引的列, 通过一个新的数据结构的形式将其进行组织, 以提高搜索的效率;
当然, 索引本质上是通过一个新的数据结构将所指定的列进行管理, 这个新的数据结构在一定程度上提高了查询速度, 但相对的会牺牲掉一些增/删/改的效率;
常见的索引有以下类型:
- 主键索引 (
Primary Key) - 唯一键索引 (
Unique) - 普通索引 (
Index) - 全文索引 (
Fulltext)
1.1 索引示例
假设存在一张表:
-
表结构
mysql> show create table student\G *************************** 1. row ***************************Table: student Create Table: CREATE TABLE `student` (`ID` int NOT NULL,`Name` varchar(50) NOT NULL COMMENT '学生姓名',`sex` enum('Male','Female') NOT NULL COMMENT '性别(仅支持Male/Female)' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='学生信息表' -
表数据
在该表中插入
8000000条数据;SET @old_autocommit = @@autocommit; SET @old_unique_checks = @@unique_checks; SET @old_foreign_key_checks = @@foreign_key_checks;SET autocommit = 0; SET unique_checks = 0; SET foreign_key_checks = 0;INSERT INTO student (ID, Name, sex) SELECT num + 1 AS ID,CONCAT('Student_', num + 1) AS Name,ELT(1 + FLOOR(RAND() * 2), 'Male', 'Female') AS sex FROM (SELECT t0.n + t1.n*10 + t2.n*100 + t3.n*1000 + t4.n*10000 + t5.n*100000 + t6.n*1000000 AS numFROM (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t0CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t4CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t5CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t6WHERE t0.n + t1.n*10 + t2.n*100 + t3.n*1000 + t4.n*10000 + t5.n*100000 + t6.n*1000000 < 8000000 ) AS numbers;
假设进行一次查询(查询出ID为7999999的学生)时, 对应的结果为:
mysql> select * from student where ID=7999999;
+---------+-----------------+--------+
| ID | Name | sex |
+---------+-----------------+--------+
| 7999999 | Student_6409802 | Female |
+---------+-----------------+--------+
1 row in set (7.12 sec)mysql> select * from student where ID=7999999;
+---------+-----------------+--------+
| ID | Name | sex |
+---------+-----------------+--------+
| 7999999 | Student_6409802 | Female |
+---------+-----------------+--------+
1 row in set (5.94 sec)mysql> select * from student where ID=7999999;
+---------+-----------------+--------+
| ID | Name | sex |
+---------+-----------------+--------+
| 7999999 | Student_6409802 | Female |
+---------+-----------------+--------+
1 row in set (5.87 sec)
可以看出此次查询的结果虽然最终趋于6s左右, 但仍因数据量过大而比较慢;
通常建立普通索引的语句可以使用:
ALTER TABLE table_name ADD INDEX(column);
当使用该语句对该表创建索引后再次去查找:
mysql> alter table student add index(ID);
Query OK, 0 rows affected (23.90 sec)
Records: 0 Duplicates: 0 Warnings: 0mysql> select * from student where ID=7999999;
+---------+-----------------+--------+
| ID | Name | sex |
+---------+-----------------+--------+
| 7999999 | Student_6409802 | Female |
+---------+-----------------+--------+
1 row in set (0.00 sec)
可以看到, 尽管在创建索引的过程中, 由于数据量较大, 因此创建索引的时间开销也相对较大, 但在索引创建过后, 再次执行SELECT对表内数据进行查询, 时间开销由原来的6S上下变为了0.00;
无论以ID为条件查询几次, 时间开销也不会太大;

这里我们选择查询的数据是索引列, 但若以其他的条件赋给WHERE进行查询, 那么同样的时间开销会很大;

很显然, 在建立索引时, 我们是以某一(些)列为条件创建索引, 因此所创建的数据结构的标准是以索引列为基准, 当查询其他条件的数据时, 由于其并不具备索引, 因此无法像索引列条件一样如此高效;
然而在删除DELETE/插入INSERT INTO/更新UPDATE 操作将会更耗时, 就像上文所说的, 建立索引相当于新建立的了一个数据结构, 这个数据结构或许对于查询效率会变得更高, 但其也会牺牲掉一些其他性能;
然而这些性能牺牲并不能很清晰的在少量数据中体现出来;
我们使用原先的语句重新创建一张表(student2)并插入同样体量的数据后, 不对该表创建索引, 对两张表同时新插入1000000条数据并观察效率;

从目前的结果来看, 对应的student2表已经被创建, 且相比student表而言, 该表并不存在索引, 现在对两张表各插入1000000数据并观察时间开销情况;
对应的插入语句为:
-- 保存当前数据库配置
SET @old_autocommit = @@autocommit;
SET @old_unique_checks = @@unique_checks;
SET @old_foreign_key_checks = @@foreign_key_checks;-- 关闭自动提交、唯一性检查、外键检查(提升插入效率)
SET autocommit = 0;
SET unique_checks = 0;
SET foreign_key_checks = 0;-- 插入100万条数据(ID:8000001 ~ 9000000)
INSERT INTO student (ID, Name, sex)
SELECT num + 1 AS ID,CONCAT('Student_', num + 1) AS Name,ELT(1 + FLOOR(RAND() * 2), 'Male', 'Female') AS sex
FROM (-- 先计算完整的num值,再在外部筛选(解决别名解析问题)SELECT num FROM (SELECT t0.n + t1.n*10 + t2.n*100 + t3.n*1000 + t4.n*10000 + t5.n*100000 + t6.n*1000000 AS numFROM -- 补全所有UNION SELECT,无省略(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t0CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t4CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t5CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t6) AS temp-- 筛选100万条数据(num:8000000 ~ 8999999)WHERE num >= 8000000 AND num < 9000000
) AS numbers;-- 提交事务
COMMIT;-- 恢复原始数据库配置
SET autocommit = @old_autocommit;
SET unique_checks = @old_unique_checks;
SET foreign_key_checks = @old_foreign_key_checks;
-
student表
-
student2表
可以看出, 虽然插入一百万条数据的时间开销只间隔了两秒, 但具有索引的表的插入的效率确实要比没有索引的表要低, 至于更新与删除也是相同, 此处不进行演示;
2 硬件理解
MySQL为存储服务, 主要存储的都是数据, 而通常数据都存储在磁盘中;
相比于其他的外设, 磁盘的IO效率通常比较低;
磁盘相关的内容可回顾博客内容:
- 『 Linux 』文件系统
对于系统而言, 在MySQL中所创建的表实际上就是磁盘空间实实在在的一个表文件;
因此实际上对表的增删改查操作实际上就是在对磁盘进行IO操作;
从/var/lib/mysql路径可以看出, 所创建的表通常以文件的形式存储在本地磁盘当中;

在地址访问时, 通常有两种访问的形式:
-
CHS磁头找到磁盘中柱面与对应的磁道, 最终找到对应的扇区编号从而进行访问;
-
LBA一种线性地址, 通过物理或者逻辑的方式使得存储的空间在一定程度上能成为线性结构以增强数据的存储的聚合性从而间接提高查找的效率;
通常情况下, 在软件层面中, 使用的更多的方式是LBA的方式, 而一般在硬件层面中(如磁盘)所使用的方式则为CHS;
软件层面将LBA的形式通过某种方式转化为CHS再交由磁盘进行读取(IO);
而在数据库当中, 我们所要存储的数据通常有成百上千条记录或是更多, 这些记录虽然在文件或是数据库中所表现的形式通常为线性结构, 然而实际上在硬件层面上, 这些成百上千至万的数据都以4kb(扇区大小)的形式零零散散的分布在磁盘中的各个位置, 磁头来回的寻道操作将会极大的增加磁盘IO的时间开销, 因此我们常说磁盘的IO效率要慢得多;
3 MySQL 与磁盘交互的基本单位
根据前置知识我们知道, 通常情况下, OS与磁盘进行交互时, 通常是以数据块block的形式进行交互, 根据磁盘扇区大小不同(512b, 4kb, 1kb), 的形式进行存储, 这里以4kb为例;
而实际上在MySQL中的存储方式通常是以页page为单位进行存储, 页的大小可以在MySQL的conf文件中进行修改, 但以InnoDB引擎下, 默认的大小为16kb;
同时, MySQL作为应用层, 无法直接与磁盘这种硬件层直接进行交互, 其中操作系统OS将作为他们的媒介;
因此, MySQL的IO对象通常为操作系统OS, 假设这里的页为16kb, 作为MySQL应用层将告诉OS自己需要存储n * 16kb(1page)大小的数据, 而OS与磁盘之间的交互以block(4kb)进行存储;
这样MySQL就完全与磁盘进行解耦, MySQL向OS发出"指令", 而OS负责落地, 以MySQL 的视角来看, 是自己向磁盘存储了n * 16kb(1page)大小的数据;

通常情况下, 我们可以使用下面的方式来查看对应的page大小(以InnoDB为例);
SHOW GLOBAL STATUS LIKE 'innodb_page_size';

这里的单位为byte, 计算出对应的kb大小结果为:
16384 ÷ 1024 = 16 kb
根据上文中已有的内容我们可以知道, 实际上MySQL中的数据文件都是以page为单位保存在磁盘中;
对应的CURD操作则需要通过计算, 从而才能找到需要操作的位置与数据, 而涉及到计算, 就必须要CPU进行参与, 为了便于CPU参与, 数据通常需要移动到内存当中;
所以在某些时间段中, 部分数据将会同时存在于内存与磁盘当中, 当内存中的CURD操作结束后, 才会以特定的方式将内存中的这些数据重新写入至磁盘当中;
在MySQL中, 为了更好的方面进行上述操作从而减少IO次数, MySQL服务器在内存中运行时, 将会申请一块被称为 Buffer Pool缓冲池的大内存空间(通常为128M)来进行各种缓存, 从而更好的与磁盘进行IO操作从而一定量的减少IO次数;
4 简单理解索引
假设存在一张学生表:
*************************** 1. row ***************************Table: student
Create Table: CREATE TABLE `student` (`ID` int NOT NULL,`Name` varchar(50) NOT NULL COMMENT '学生姓名',`sex` enum('Male','Female') NOT NULL COMMENT '性别(仅支持Male/Female)',PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='学生信息表'
可以看到在这张表中, 存在主键为ID;
对该表进行几条数据插入, 其中插入时ID字段将以乱序的形式进行插入;
mysql> insert into student values(2, 'ZhangSan',1);
Query OK, 1 row affected (0.00 sec)mysql> insert into student values(5, 'LiSi',1);
Query OK, 1 row affected (0.00 sec)mysql> insert into student values(3, 'WangWu',2);
Query OK, 1 row affected (0.00 sec)mysql> insert into student values(1, 'ZhaoLiu',2);
Query OK, 1 row affected (0.00 sec)mysql> insert into student values(4, 'LiuYi',2);
Query OK, 1 row affected (0.00 sec)
插入后对结果进行查询:

可以看到, 在插入过程中, 我们并没有按照顺ID的顺序对数据进行插入, 而实际在插入后, 数据根据ID进行了排列;
而实际上在此前学的主键约束实际上即为主键索引(聚簇索引);
主键通常分为聚簇索引和二级索引, 在InnoDB中可以没有二级索引, 但一定要存在聚簇索引, 通常聚簇索引的优先级为如下:
-
PrimaryKey > Unique Not Null > ROWID(默认隐藏)
其中ROWID为当建表时不存在主键与唯一键时所创建出来的隐藏列, 该列会充当聚簇索引的角色;
当当前已有其他聚簇索引存在时增加一个新的优先级高的索引时, MySQL将会做以下操作:
- 废除原有聚簇索引
- 重新以新加的优先级较高的索引为聚簇索引重新建表
- 将优先级低的索引重新建表为二级索引
4.1 如何理解 page
在MySQL中, 一定需要且同时存在大量的page, 当同一种数据存在大量时, 就一定要将这个数据进行管理;
管理的方式通常为"先描述后组织";
这意味着实际上在MySQL中的page实际上也是一种结构体struct, 根据这些数据的同一属性描述出这个结构体(块)的基本属性, 类似*next, *prev指针以及需要存储的数据的空间, 所有的属性加在一起与部分数据的内存对齐, 最终出现了一个大小为16kb的结构体;
这些被实例化出来的page实例将被一种特定的数据结构进行管理, 这种特定的数据结构即为 —— “B+树”;
通常一个page的结构大致为如下:

根据B+Tree的结构, 其中间节点不存储数据, 只存储路径进行导航, 只有叶子结点存储数据, 且所有的叶子结点以链表的形式进行存储, 这本身是B+Tree的优势, 因此大部分的关系型数据库都采用这种数据结构来对数据进行存储;
4.2 为什么MySQL的IO交互需要以Page为单位进行
在计算机中存在一种原理被称为局部性原理(包括 “空间局部性" 与 “时间局部性” );
-
空间局部性
当数据在访问某块内存时大概率也会访问其周边内存的数据;
-
时间局部性
最近访问的数据很可能很快再次访问;
因此, 以上述的五条数据为例, 若是逐次依照每条数据进行查找将会增加IO次数从而降低效率;
而若是上述的五条数据同时存储在同一个page当中, 由于该page已经被加载进内存当中, 下一次的查询只需要在内存中去进行而不需要再重新在磁盘当中将对应的记录进行读取从而减少IO次数;
通常情况下, IO效率低下的主要矛盾不是IO单次数据的大小, 而是IO的次数;
4.3 Slots 槽
在上文提到, 实际上索引是建立新的多个page来管理的, 相当于目录的角色, 而在page中也存在页内目录的角色, 这个角色被称为槽, 即slots;
无论是聚簇索引还是二级索引所创建的B+Tree所管理的page页内都存在slots槽, 这个槽通常大小为2bytes, 其记录着组的边界偏移地址, 其中每4-8条记录将存在一个slot;
由于页内是有序的, 当查找对应的数据到对应的页中时, 只需要按照二分的方式对数据进行查找即能加快查找的效率;
在上文中, 我们在存在主键约束的表中乱序插入一堆数据, 可最终SELECT的结果为, 其根据主键约束字段为键值变为了有序, 这个本质上是为了更好的引入Slots槽(页内目录的这一说);
4.4 理解多个 Page
在上文中, 我们解释了单个Page, 即通过两个指针与一块用于存放记录的空间与其他属性共同组成了一个16kb大小的页;
其包含了Prev*与Next*指针用于指向前一个或后一个节点, 这种结构看似类似于链表的结构;

但实际上若干个Page并不采用纯链表这种线性结构进行存储;
我们知道, 链表查找的时间复杂度为 “O(N)”, 针对内存而言这并不是一个非常低效的, 然而MySQL属于关系型数据库, 其数据将存储在磁盘之中, 当出现成百上千条数据时, 使用**“O(N)”**时间复杂度的效率同时对磁盘进行IO操作, 那么将会大大降低效率;
因此索引并不采用这种线性结构, 而是使用树形结构;
以InnoDB为例, 所创建出的索引通常是使用B+ Tree结构并采用链表的优化将若干个page进行存储;
而索引作为数据库中的一个数据结构, 同样需要空间, 并且其同样属于数据库中数据的一环, 因此需要持久化存储, 因此实际上索引也有一个或多个Page进行管理;
与目录的原理相同, 当一本书需要翻到具体某个内容的某一页通常有几种方法?
-
从第一页开始翻直至翻到对应的内容
-
创建目录来管理所有的内容, 当需要查找时通过目录找到对应的位置直接进行翻页
很明显第二种方式的效率要比第一种方式的效率要高得多, 通过空间换时间的方式;
而索引恰恰就是使用这种方式从而加快了查询的速度;
同时page以B+Tree而言存在两种page, 分别为:
- 叶子结点
- 非叶子结点
通常叶子结点page存储的是slots, 键值, 以及具体的数据(具体的数据根据索引的类型而不同, 聚簇索引管理行级数据, 二级索引则是键值对应的聚簇索引指针);
通过这样的存储形式, 可以让整体的树形结构变得更加的矮胖(其中间节点因存储单个数据大小较小, 因此能存储的数据会更多);

总结来说, 叶子结点存储数据, 非叶子节点只进行导航;
4.5 为何不能采用其他数据结构充当索引
-
链表
链表是一种线性结构, 通常情况下, 在查找一个数据时需要遍历链表长度次数, 因此时间复杂度为
O(N);通常情况下这个时间复杂度并不是很高, 但由于MySQL数据库是一个可以管理海量数据的结果, 当该结构中存在成千上万条数据后, 遍历的速度将会变慢, 同时, 数据库的操作通常是磁盘中文件的操作, 多次的遍历又涉及到多次的磁盘
IO, 磁盘IO本身又是低效的, 因此无法使用这种线性结构; -
二叉搜索树
二叉搜索树是二叉树的一种, 与二叉树不同的是, 二叉搜索树保持一个规则, “左孩子不大于右节点不大于右孩子”;
而若是在一棵二叉搜索树中有序插入一组数据, 那么其将会退化为线性结构;

-
AVL && RBTree红黑树和
AVL树同样是二叉树形结构, 但其并不会向搜索二叉树一样出现极端情况, 其通常会根据规则进行平衡, 但相对而言其层数会相对较高;而对于数据库而言, 其的每一次查询都伴随着磁盘
IO, 在上文中提到, 实际上效率的影响不是单次IO的数据量, 而是IO的次数, 红黑与AVL作为二叉树, 其在相同数据体量上高度一定要高于B+Tree, 而层级上B+Tree作为一棵矮胖树有着明显的优势; -
Hash在MySQL中, 是支持
Hash的, 但InnoDB与MyISAM引擎并不支持;Hash的查询在某些时候非常高效, 时间复杂度一度能来到常数级, 但其对范围查询的支持并不会很高; -
BTreeBTree与B+Tree类似, 同样是一棵平衡多叉树;但相较于
B+Tree而言,BTree允许其在非叶子节点中存储数据, 这也意味着非叶子节点能存储数据, 但对应的Slots将会变少, 增加整棵树的高度, 因此最终还是采用B+Tree这种高度较低的树作为数据库的数据管理模式;
5 聚簇索引与非聚簇索引
在上文中我们提到了两种索引方式, 分别为:
- 聚簇索引
- 非聚簇索引
聚簇索引通常将所有数据以物理的逻辑直接进行排序, 其每个叶子节点所存放的数据即为该行数据的整行数据;
而非聚簇索引的叶子结点所存储的内容并不是数据, 而是Key值所对应的映射指针;

再以存储引擎为例, 通常MyISAM采用的是非聚簇索引, 其同样采用B+Tree进行管理, 但唯一不同的是其叶子节点不存储数据本身, 而是对应行数据的映射地址;
反观InnoDB存储引擎, 其采用了聚簇索引与非聚簇索引的合并, 分为聚簇索引和二级索引, 其中聚簇索引为优先级最高的索引方式, 而二级索引则为聚簇索引以外的索引方式;
对于InnoDB而言, 表结构中必须存在一个聚簇索引, 可以不存在二级索引(非聚簇索引);
对于索引而言, 在上文 “4 简单理解索引” 中已经进行了介绍;
5.1 回表查询
在上文中提到, 在InnoDB中, 聚簇索引所存储的内容为表中一整行的数据, 因此当索引时采用聚簇索引, 将可以直接查询到对应的结果;
而二级索引中, 其所存储的内容为索引列的值与该值所在聚簇索引的指针;
因此当使用二级索引进行查询且未查询命中时(查询的字段范围要大于二级索引索引列内容时), 需要通过其存储的指针访问对应的聚簇索引结果, 并进行下一步的查询, 这种操作被称为回表查询;
6 索引操作
6.1 创建主键索引 与 创建唯一键索引
创建主键索引与创建主键约束相同, 因为实际上主键PrimaryKey既是索引页是约束;
通常有三种方式:
-
创建表时在字段后指明主键
CREATE TABLE [IF NOT EXISTS] table_name(column column_type PRIMARY KEY[, ...] ); -
创建表时在表后指明主键
CREATE TABLE [IF NOT EXISTS] table_name(column column_type [, ...] ) PRIMARY KEY(column); -
创建表后通过修改表来添加主键
-- 创建表 CREATE TABLE [IF NOT EXISTS] table_name(column column_type [, ...] );-- 添加主键 ALTER TABLE table_name ADD PRIMARY KEY (column);
主键就是主键, 主键既是索引页是约束, 至于主键索引可以参考前文 『 数据库 』MySQL复习(表的约束) ;
唯一键索引也是如此, 唯一键既是索引页是约束;
6.2 查看索引
查看索引通常有三种方式进行查询;
-
show indexSHOW INDEX FROM table_name;
-
show keysSHOW KEYS FROM table_name;
-
desc table_nameDESC table_name;
从
Key中看到具体某一列有索引, 使用该方式所展示数来的信息较为简短, 但同样可以观察到索引的信息;
6.3 创建普通索引
-
创建表时在表的最后一列通过
INDEX( column )创建CREATE TABLE [IF NOT EXISTS] table_name(column type [, ...]INDEX(column) );
-
创建表后再通过
ALTER指定某一列为普通索引-- 创建表 CREATE TABLE [IF NOT EXISTS] table_name(column type [, ...] );-- 增加索引 ALTER TABLE table_name ADD INDEX( column );
-
创建一个索引名为
idx_name的索引并指向某张表的某一列CREATE INDEX idx_name ON table_name(column);
这种方式也可以为
UNIQUE唯一键索引取别名:CREATE UNIQUE INDEX idx_name ON table_name (column);
当某一列需要创建索引且该列有重复数值时, 优先创建普通索引, 普通索引只是二级索引并无约束;
6.4 删除索引
删除索引的方式需要注意, 通常主键约束是整张表的属性, 因此在删除主键索引时可以直接使用alter ... drop Primary key的方式进行删除;

除此之外其他的索引删除方式通常通过drop index idx_name的方式进行删除, 也包括UNIQUE与普通索引INDEX;

6.5 索引创建的原则
通常情况下选择查询比较频繁的字段作为索引, 同时在选择对应的列创建索引时, 该列不能出现唯一性太差, 否则索引形同虚设;
其次选择对应的为索引的列不能更新的过于频繁, 否则更新数据时将会触及到B+Tree的调整, 当数据量大时, 数据的新插入或修改使得树形结构重新调整平衡或者分裂新页所导致的时间开销是十分庞大的;
同时对于不会出现在WHERE子句中的字段通常不建议其作为索引, 因为索引本质上是为查询做优化的, 而不再WHERE子句中出现说明其查询要求并不大, 因此不建议其创建索引;
7 全文索引
当对文章字段或者大量文字内容的字段进行检索时, 将会使用全文索引;
MySQL中支持全文索引, 但通常情况下, 全文索引所支持的存储引擎为MyISAM, 同时默认的全文索引支持英文不支持中文;
若是有对中文进行全文索引的需求, 可以使用Sphinx的中文版(coreseek);
假设存在对应的表与数据为如下:
-- 创建表
CREATE TABLE articles (id INT AUTO_INCREMENT PRIMARY KEY,title VARCHAR(255) NOT NULL,content TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 插入英文数据(包含关键词如 "MySQL" "index")
INSERT INTO articles (title, content) VALUES
('MySQL Index Introduction', 'MySQL fulltext index is used for text search, supporting tokenization and relevance ranking.'),
('Database Optimization', 'Using FULLTEXT index can speed up LIKE queries and avoid full table scans.'),
('InnoDB Engine', 'InnoDB supports fulltext index starting from version 5.6.'),
('Search Technology', 'Inverted index is the core of fulltext search, MySQL uses ngram for Chinese tokenization.'),
('Example Application', 'In blog systems, fulltext index helps quickly find article content.');-- 添加全文索引(针对 content 字段)
ALTER TABLE articles ADD FULLTEXT INDEX idx_content (content);
通过select * from...查看结果为:

该表结构通过show index ... 查看对应的全文索引结果为:

当我们查找数据时, 如content中是否含有"index", 对应的结果为:

但是这种情况我们并没有使用到所谓的全文索引, 可以通过explain来查看该语句是否用到索引;
explain select * from articles where content like '%index%';

其中KEY为NULL表示这个语句中并未使用任何索引;
而实际上, 全文索引的使用方式为:
SELECT column FROM tabl_name WHERE MATCH(FULLTEXT_column1 [, FULLTEXT_column2 ...]) AGAINST ('text_val');

正常查询到结果, 同时可以使用explain工具来分析该SQL语句:

可以观察到全文索引被使用;
