02【存储引擎、索引】
第1章 存储引擎
1.1 存储引擎概述
MySQL的存储引擎是指数据库管理系统(DBMS)中用于存储和检索数据的具体实现方式,数据库管理系统使用存储引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能。
Oracle,SQL Server等数据库只有一种存储引擎。MySQL提供了插件式的存储引擎架构。所以MySQL存在多种存储引擎,可以根据需要使用相应引擎。
存储引擎决定了MySQL底层组织数据的方式,不同的存储引擎之间大致有如下特点:
- 数据存储方式:存储引擎决定了数据的物理存储方式,影响数据的读取和写入效率。
- 事务支持:一些存储引擎支持ACID(原子性、一致性、隔离性、持久性)事务,能够保证数据的一致性和完整性,而有些则不支持。
- 锁机制:不同的存储引擎有不同的锁定策略,影响并发操作的性能。例如,InnoDB使用行级锁,而MyISAM使用表级锁。
- 索引方式:存储引擎提供不同类型的索引,影响查询性能和速度。
- 数据恢复:某些存储引擎提供数据恢复功能,能够在系统崩溃后恢复数据。
1.1.1 查看存储引擎
show engines;

查看当前数据库默认使用什么存储引擎:
show variables like '%storage_engine%';

1.1.2 修改存储引擎
可以看到MySQL默认使用的存储引擎是InnoDB,如果想要修改MySQL的存储引擎可以有如下操作
1)创建表的时候指定存储引擎
CREATE TABLE `demo1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM default CHARSET=utf8; -- 使用Myisam存储引擎
CREATE TABLE `demo2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; -- 使用InnoDB
2)基于已经创建好的表修改存储引擎
mysql> alter table demo2 engine=myisam;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql>
3)在配置文件中(my.cnf)修改文件:
[mysqld]
default_storage_engine=myisam
1.1.3 常见存储引擎
| 特点 | InnoDB | MyISAM | MEMORY | MERGE |
|---|---|---|---|---|
| 存储限制 | 64TB | 有 | 有 | 没有 |
| 事务安全 | 支持 | |||
| 锁机制 | 行锁、表锁 | 表锁 | 表锁 | 表锁 |
| B树索引 | 支持 | 支持 | 支持 | 支持 |
| 哈希索引 | 支持 | |||
| 全文索引 | 支持(5.6版本之后) | 支持 | ||
| 集群索引 | 支持 | |||
| 数据索引 | 支持 | 支持 | ||
| 索引缓存 | 支持 | 支持 | 支持 | 支持 |
在各种常用存储引擎中,只有InnoDB支持事务与外键。
1.2 InnoDB 存储引擎
InnoDB是一种兼顾高可靠性和高性能的通用存储引擎,从 MySQL 5.5 版本开始成为默认的存储引擎。它支持事务处理(ACID特性)、行级锁定和外键约束等高级数据库功能。InnoDB通过其内部的多版本并发控制(MVCC)和行级锁定机制,提供了强大的并发处理能力。但InnoDB通常会比其他存储引擎占用更多的磁盘空间以保留数据和索引。
1.2.1 InnoDB的特点
- 事务支持:支持 ACID 事务属性,可以保证事务的一致性、隔离性、持久性和原子性。
- 行级锁定:使用行级锁定机制,多个事务可以并行执行,提高了并发处理能力。
- 外键支持:支持外键约束,确保数据库中的数据完整性。
- 存储形式:.frm-表定义文件,.ibd-数据文件和索引文件(索引和数据是存放在一起的)。
- MVCC(多版本并发控制):InnoDB 实现了 MVCC 来支持读取未提交的数据,提高并发性能。
- 恢复机制:支持崩溃恢复机制,可以在数据库异常终止后自动恢复数据。
- 缓冲池:使用缓冲池来缓存表的数据和索引,减少了磁盘 I/O 操作,提高了性能。
- 自适应哈希索引:InnoDB 可以自动维护哈希索引,并根据查询的频率来创建哈希索引,提高某些查询的性能。
- 支持分区表:支持表分区,可以在大型表上实现更好的性能和管理。
- 全文索引:支持全文索引,可以在文本数据上进行快速的全文搜索。
1.3 MyISAM 存储引擎
在MySQL5.1及之前,MySIAM是默认的存储引擎。主要用于处理读操作较多的应用场景,MyISAM不支持事务操作,仅支持表级锁。MyISAM引擎以轻巧,结构简单,占用空间少,读取速度快而被广泛适用于读场景较高的场合。
1.2.1 MyISAM的特点
- 非事务型:不支持事务处理,因此不具备事务的ACID属性(原子性、一致性、隔离性、持久性)。
- 表级锁定:使用表级锁而不是行级锁,这意味着在写入(INSERT、UPDATE、DELETE)时会锁定整个表,影响读写并发性能。
- 读取速度快:MyISAM表结构简单,读取速度快,尤其适合进行大量的SELECT查询。
- 空间和性能:MyISAM对磁盘空间的利用率高,可以压缩数据以减少磁盘空间的占用,同时其查询性能也非常高效。
- 压缩功能:MyISAM 支持静态表(static tables),这类表可以在创建时被压缩,从而减少存储空间的需求。
- 存储形式:.frm-表定义文件,.myd-数据文件,.myi-索引文件(索引和数据是分开存放的)。
- 容易管理:MyISAM的数据文件和索引文件都是独立的,使得在发生问题时可以单独对数据文件或索引文件进行修复,而不需要影响整个数据库。
- 全文索引支持:提供全文搜索功能,非常适合需要执行全文检索的应用。
综上,MyISAM的总体优势在于占用空间小,处理速度快。缺点是不支持事务的完整性和修改并发。
1.2.2 MyISAM与InnoDB对比
InnoDB和MyISAM算是MySQL中最常用的两种存储引擎了,所以我们来对比一下两种存储引擎的主要区别与特点:
| Innodb | MyISAM | |
|---|---|---|
| 存储文件 | .frm-表定义文件,.ibd-数据文件和索引文件 | .frm-表定义文件,.myd-数据文件,.myi-索引文件 |
| 锁 | 行锁、表锁 | 表锁 |
| 事务 | 支持 | 不支持 |
| CRDU | 读、写 | 读多 |
| count | 扫表 | 读取行计数器 |
| 索引结构 | B+Tree | B+Tree |
| 应用场景 | 事务、并发读写 | 读取性能 |
| 外键 | 支持 | 不支持 |
详细区别如下:
- 存储文件
- Innodb:.frm-表定义文件,.ibd-数据文件和索引文件
- MyIsam:.frm-表定义文件,.myd-数据文件,.myi-索引文件
- 锁
- Innodb:锁粒度为行级锁,操作某一条数据时,只锁某一行,对其他行没有影响
- MyIsam:锁粒度为表级锁,操作某一条数据时,锁整一张表
- 事务支持:
- Innodb:支持
- MyIsam:不支持
- 外键支持:
- Innodb:支持
- MyIsam:不支持
- 应用场景
- Innodb:关注事务要求性高的应用场景,适用于高并发修改场景
- MyIsam:更关注读取的性能,不适用于高并发下频繁的修改数据
MySQL中的数据最终还是存储在磁盘中的,我们可以查看MySQL数据的存储路径:
show variables like '%dir%';
Linux下的:

其中datadir就是MySQL存放数据的目录
cd /var/lib/mysql

1.2.3 MyISAM与InnoDB性能测试
准备两张测试表,注意修改存储引擎:
CREATE TABLE `userinfo_innodb` (
`id` int(10) NOT NULL COMMENT '用户id',
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`age` int(3) NULL DEFAULT NULL COMMENT '年龄',
`phone` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`gender` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别: ‘0’-男 ‘1’-女',
`desc` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '自我介绍',
`register_time` datetime(0) NULL DEFAULT NULL COMMENT '注册时间',
`login_time` datetime(0) NULL DEFAULT NULL COMMENT '上一次登录时间',
`pic` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像地址',
`look` int(10) NULL DEFAULT NULL COMMENT '查看数',
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
create procedure userinfo_innodb_insert(count int)
begin
declare i int default 1;
while i<=count do
INSERT INTO userinfo_innodb values(
i, -- id
uuid(), -- username
CEILING(RAND()*90+10), -- age
FLOOR(RAND()*100000000000), -- phone
round(FORMAT(rand(),1)), -- gender
uuid(), -- desc
now(), -- register_time
now(), -- login_time
uuid(), -- pic
CEILING(RAND()*90+10) -- look
);
set i=i+1;
end while;
end;
执行InnoDB和MyISAM批量插入5W条记录查看其性能如下:
call userinfo_innodb_insert(50000);
call userinfo_myisam_insert(50000);

对比发现innodb存储引擎插入5W条记录需要花费5.96s,而MyISAM存储引擎只需要0.52s。
接着我们把表中的数据扩大到100w,对比MyISAM和InnoDB的查询性能:
mysql> select * from userinfo_myisam where username='a';
Empty set (0.16 sec)
mysql> select * from userinfo_myisam where username='a';
Empty set (0.17 sec)
mysql> select * from userinfo_innodb where username='a';
Empty set (0.59 sec)
mysql> select * from userinfo_innodb where username='a';
Empty set (0.55 sec)
可以看到,MyISAM的读取性能要比InnoDB快不少。
1.2.4 MyISAM的count
MyISAM不仅在普通查询上比InnoDB要快不少,而且在处理count()函数时底层所采用的策略也与InnoDB不同,这使得MyISAM在处理COUNT函数时性能要比InnoDB高出许多。
1)MyISAM处理COUNT()函数
在 MyISAM 中,表的行数被存储在一个元数据(metadata)中,这个元数据在每次对表进行写入操作时都会被更新。因此,对于一个没有正在执行的写入操作的 MyISAM 表,可以直接从元数据中获取行数,而不需要扫描整个表。这使得 COUNT(*) 查询非常快。
虽然MyISAM在处理COUNT函数时性能要优于InnoDB,但在某些情况下仍然有自身的局限性。
- 表级锁:MyISAM引擎使用的是表级锁,在执行COUNT查询时,会对整个表加锁,阻止其他写操作。这意味着在高并发环境下,写操作可能会被阻塞,从而影响性能。
- 条件统计:虽然count(*)在MyISAM中非常快,但对于带有条件的COUNT查询(如COUNT(column_name)),MyISAM会扫描整个表,检查每一行以确定满足条件的行数。这可能会导致性能下降,尤其是在大表中。
2)InnoDB处理COUNT()函数
InnoDB 不保存表的行数。这意味着 InnoDB 没有类似于 MyISAM 的元数据来存储行数。因此,对于 COUNT(*) 查询,InnoDB 需要扫描整个表来计算行数。这通常会导致比 MyISAM 更慢的响应时间,尤其是在大表上。
为了优化 COUNT(*) 查询,你可以创建一个额外的计数器表(counter table)来存储行数,并在每次插入或删除数据时更新这个计数器表。这样,你可以通过查询计数器表来快速得到行数,而不是扫描整个表。
另一种方法是在 InnoDB 表上使用一个覆盖索引来优化 COUNT(*) 查询。如果表有主键索引,并且 COUNT(*) 查询是基于主键索引的,那么 InnoDB 可以仅扫描索引来计算行数,而不需要访问实际的数据行。但是,请注意,这并不能完全避免全表扫描,因为主键索引本身可能就是非常大的。
1.2.5 MyISAM表的特点
MySIAM表支持三种不同存储格式:
1)静态表:静态表是默认的存储格式,静态表中的字段都是非变长的字段(char),优点是:存储非常迅速,出现故障容易恢复;缺点是:占用的空间通常比动态表多。
2)动态表:动态表的字段是变长的(varchar),优点是:占用的空间相对较少,但是频繁地更新删除记录会产生碎片,需要定期改善性能,并且出现故障的时候恢复相对比较困难。
3)压缩表:MyISAM可以使用myisampack工具压缩表数据,压缩表占用磁盘空间小,每个记录是被单独压缩的,所以只有非常小的访问开支。
在Myisam引擎中,是可以对表数据文件进行压缩的。

使用myisampack压缩命令压缩数据文件
myisampack user_myisam # 不用加后缀
再次查看数据文件大小:

Tips:虽然压缩表可以节省存储空间,但在读取数据时可能会导致性能下降,因为MySQL需要解压缩数据。因此,在决定使用压缩表时,需要权衡存储空间与性能之间的关系。
MySIAM存储引擎的业务场景:
- 不需要事务支持的业务(例如转账就不行)。
- 一般为读数据比较多的应用,读写都频繁场景不适合(锁机制问题)
- 以读为主的业务,例如:数据库系统表、日志、图片信息数据库
- 使用读写分离的MySQL读库可以使用MyISAM。
1.4 Merge 存储引擎
Merge 用于将一系列等同的MyISAM表以逻辑方式组合在一起,并作为一个对象引用它。这些MyISAM表必须结构完全相同。Merge 表本身没有数据,对Merge 类型的表进行查询、更新、删除的操作,就是对内部的MyISAM表进行的,Merge 表的优点在于可以突破对单个MyISAM表大小的限制,通过将不同的表分布在多个磁盘上,可以有效的改善Merge 表的访问效率。
Merge 数据表的定义里可以包括一个INSERT_METHOD选项,这个选项的可取值是NO、FIRST、LAST,他们的含义依次是禁止插入、插入到第一个子表、插入到最后一个子表。
创建三张表:
create table user_01(
id int,
username varchar(10)
) engine = myisam default charset=utf8;
create table user_02(
id int,
username varchar(10)
) engine = myisam default charset=utf8;
create table user_all(
id int,
username varchar(10)
) engine=merge union = (user_01,user_02) INSERT_METHOD=LAST default charset=utf8;
2)在表中插入数据
insert into user_01 values(1,'张三');
insert into user_01 values(2,'李四');
insert into user_02 values(10,'王五');
insert into user_02 values(11,'赵六');
3)查看表中的数据
user_01表中数据:

user_02表中数据:

user_all表中数据:

4)在user_all表中插入一条数据:
因为在创建Merge表时,INSERT_METHOD设置的是LAST,因此插入到最后面的表中。
insert into user_all values(100,'钱七');

Merge表除了在创建的时候选择合并的表,还可以在创建之后再添加其他的表(表结构必须一致)
1)创建一张新的表:
create table user_03(
id int,
username varchar(10)
) engine = myisam default charset=utf8;
2)插入数据:
insert into user_03 values(1,'小龙');
insert into user_03 values(2,'小明');
3)修改user_all表
alter table user_all union = (user_01,user_02,user_03);
4)查询user_all表:

5)在user_all插入一条新的数据,注意,此时的LAST表为user_03了;
insert into user_all values(1,'小刚');

1.5 Memory 存储引擎
Memory 存储引擎将表的数据存放在内存中。每个Memory表实际对应一个磁盘文件,格式是.frm ,该文件中只存储表的结构,而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率。Memory类型的表访问非常地快,因为他的数据是存放在内存中的,并且默认使用HASH索引 ,但是服务一旦关闭,表中的数据就会丢失。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。
- 创建一张Memory存储引擎的表
CREATE TABLE `user_memory` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`age` int(50) ,
PRIMARY KEY (`id`) -- memory表的索引默认是hash类型
) ENGINE = memory AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 ;
insert into user_memory values(1,'zs',20);
insert into user_memory values(2,'ls',30);
查看磁盘存储文件/var/lib/mysql/test

发现只有表定义文件(.frm),memory的表数据是存储在内存中的。
- 我们编写一个存储过程,批量插入300W数据
create procedure memory_insert(count int)
begin
declare i int default 1;
while i<=count do
INSERT INTO user_memory values(i,uuid(),CEILING(RAND()*90+10));
set i=i+1;
end while;
end;
- 执行存储过程
call memory_insert(3000000);

出现:ERROR 1114 (HY000): The table ‘user_memory’ is full错误
在MySQL中,有两个参数影响着memory表的使用:
max_heap_table_size:memory表能使用的最大内存大小tmp_table_size:memory表超过此参数值将转化为磁盘表(即使转换为磁盘表,服务器重启后,数据仍会丢失)。
出现上面错误的原因是MySQL给Memory表分配的默认内存大小是16MB,300W的数据远远超过了16MB的存储空间,我们可以通过修改max_heap_table_size变量,来提升Memory表的存储大小限制。
- 查看
max_heap_table_size大小。
select @@max_heap_table_size;
select @@tmp_table_size;

16777216B / 1024K / 1024M =16M
修改mysql配置文件(/etc/my.cnf),添加如下配置:
max_heap_table_size=800M
tmp_table_size=800M
重启mysql服务器:
systemctl restart mysqld
再次插入300W数据:

要注意的是,在mysql服务器重启之后,memory存储引擎的表中的数据都会被清空。
使用Memory存储引擎需要注意如下几点:
- Memory不支持BLOB或TEXT列。
- Memory表在所有客户端之间共享
- Memory表内容被存在内存中,默认大小是16MB,通过
max_heap_table_size参数维护,当超过此大小则会报错,Memory表中的内容超过tmp_table_size值则会采用磁盘表,此时速度低,重启服务器数据仍会丢失 - 当你不再需要Memory表的内容之时,要释放被Memory表使用的内存,你应该执行
DELETE FROM或TRUNCATE TABLE,或者整个地删除表(使用DROP TABLE)。
例如:
max_heap_table_size=300M
tmp_table_size=100M
说明:memory表最大能存储300M的数据,其中100M数据存放在内存中,另外200M存放在磁盘中。
第2章 索引
2.1 索引概述
2.1.1 索引的底层
索引是帮助数据库快速查询数据的一种数据结构,在数据库中,数据库系统除了存储数据之外还维护着一种特殊的数据结构,这种数据结构以某种方式指向数据,这样就可以在这种数据结构上实现高级算法来查询数据,这种数据结构就是索引。
- 索引示意图:

如图所示,索引能够帮我们快速的定位到数据的具体位置,高效查询。一般来说,索引本身也很大,不可能全部存储在内存当中,因此索引往往以索引文件的形式存储在磁盘上。索引是用来提供高性能数据库的常用工具。
2.1.2 索引的优缺点
- 优点
1) 类似于书籍的目录索引,提高数据检索的效率,降低数据库的IO成本。
2) 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗。
- 缺点
1) 实际上索引也是一张表,该表中保存了主键与索引字段,并指向实体类的记录,所以索引列也是要占用空的。
2)索引虽然大大提升了查询效率,但同时也降低了更新表的速度,如果对表进行了insert/update/delete等语句时,数据库不仅要保存数据,还需更新索引文件,每次更新添加了索引列的字段,都会调整索引的结构。
2.2 索引的数据结构
索引是在MySQL的存储引擎层中实现的,而不是在服务器层实现的。这意味着每种存储引擎的索引都不一定完全相同,也不是所有的存储引擎都支持所有的索引类型的。
索引的分类非常多,从不同的角度去看待索引,索引将会被划分为非常多的种类,我们学习索引的底层一般是从数据结构的角度来划分索引,下列是从数据结构的角度来划分的索引:
- B-Tree:常见的索引类型,B+Tree就是在此数据结构上做的优化。
- Hash:是基于哈希表实现的,只有精确匹配索引所有列的查询才会生效。对于每一行数据,存储引擎都会对所有的索引列计算一个hash code,并将有的hash code存储在索引中,同时在哈希表中保存指向每个数据行的指针。
- R-Tree:R-Tree是B-Tree在高维空间的扩展,也叫空间索引,主要用于地理空间数据类型,存储高维数据的平衡树。
- Full-text:Full-text索引就是我们常说的全文索引,类似于Lucene的文本全文检索技术,比like要高效许多。
MyISAM、InnoDB、Memory三种存储引擎对各种索引类型的支持
| 索引 | InnoDB | MyISAM | Memory |
|---|---|---|---|
| B-Tree | 支持 | 支持 | 支持 |
| Hash | 不支持 | 不支持 | 支持 |
| R-Tree | 不支持 | 支持 | 不支持 |
| Full-text | 5.6版本之后支持 | 支持 | 不支持 |
我们平常所说的索引,如果没有特别指明,都是指B+Tree(多路平衡搜索树,并不一定是二叉的)结构组织的索引。其中聚集索引、复合索引、前缀索引、唯一索引默认都是使用 B+Tree 索引,统称为索引。
2.2.1 Hash
当表中的某一列建立了hash索引时,首先使用hash算法将列的键值计算为hash值,然后根据计算的hash值来选择存储对应的位置;hash索引底层采用一张hash表建立索引与列的关系;通常情况下,使用hash索引查询一次即可定位要查询的行,但是当hash冲突时hash表的某个槽位将会存储很多的键值,查询性能会变低;
1)新增数据时,先根据键值计算出hash值,通过hash值一个映射到一个槽位中;当多个数据计算的hash值一致时(hash冲突),可能映射到同一个槽位,这个时候可以通过链表来解决hash冲突问题;
2)当根据hash索引查询数据时,首先计算hash值,通过hash值去hash表中查询数据;

Hash索引不支持排序、范围查询等操作;
2.2.2 B+Tree
1) 二叉树
B+Tree中的B代表平衡(balance),而不是二叉(binary),因为B+Tree是从最早的平衡二叉树演化而来的。在讲B+Tree之前必须先了解二叉查找树、平衡二叉树(AVLTree)和平衡多路查找树(B-Tree),B+Tree即由这些树逐步优化而来。
数据结构可视化网址:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
- 特点:左子树的键值小于右子树的键值。

对该二叉树的节点进行查找发现深度为1的节点的查找次数为1,深度为2的查找次数为2,深度为n的节点的查找次数为n,因此其平均查找次数为 (1+2+2+3+3+3+3) / 7 = 2.428次
二叉查找树可以任意地构造,同样是2,3,5,6,7,8这六个数字,也可以按照下图的方式来构造:

计算这颗树的平均查找次数:(1+2+3+4+5+6+6)/7=3.857次
这棵二叉树的查询效率明显就低了。因此若想二叉树的查询效率尽可能高,需要这棵二叉树是平衡的,从而引出新的定义——平衡二叉树,或称AVL树。
2) 平衡二叉树
平衡二叉树(AVL树)在符合二叉查找树的条件下,还满足任何节点的两个子树的高度最大差为1。
我们按照顺序依次插入6,3,7,2,4,1,5数据,观察AVL树与普通二叉树的演变:

3) B-Tree
B-Tree又叫多路平衡搜索树,一颗m叉的B-Tree特性如下:
- 树中每个节点最多包含m个孩子。
- 除根节点与叶子节点外(非根 叶子节点),每个节点至少有[ceil(m/2)]个孩子。
- 若根节点不是叶子节点(说明树的层级最少为2),则至少有两个孩子。
- 所有的叶子节点都在同一层。
- 每个非叶子节点由n个key与n+1个指针组成,其中[ceil(m/2)-1] <= n <= m-1
B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个3阶的B-Tree:

每个节点都会占用一个磁盘块的磁盘空间,当在数据检索时,会将此磁盘块加载到内存中,每个非叶子点上都有两个升序排序的键值和三个指向子树的指针,该指针存储的是子节点所在磁盘块的内存地址
以磁盘块1为例,假设现在加载了磁盘块1到内存中,我们通过磁盘块的三个指针(P1、P2、P3)就能够判断,P1指针指向的子树的数据范围为小于26,P2指针指向的子树的数据范围为26~37,P3指针指向的子树的数据范围为大于35。
- 模拟查找关键字21的过程:
1)首先加载根节点,根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
2)发现21比26小,根据磁盘块1中的P1指针找到对应的磁盘2,读入内存。【磁盘I/O操作第2次】
3)发现21比18大,根据磁盘块2中的P3指针找到磁盘块7,读入内存。【磁盘I/O操作第3次】
4)根据磁盘块7中的数据找到21。
4) B+Tree
MySQL索引数据结构对经典的B-Tree进行了优化。在原B-Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能。并且B+Tree数据结构将数据全部存储在最底层的叶子节点,这样可以保证每个磁盘块能够存储更多的指针,查询数据更广。而B-Tree想要存储B+Tree同样的数据智能增加树的高度,导致性能降低;

通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。
2.3 索引的物理存储
2.3.1 聚集索引与非聚集索引
索引按照物理存储结构划分分为聚集索引和非聚集索引
聚集索引:也叫聚簇索引(ClusterIndex),一般来说是以主键创建的索引,一张表只能有一个聚集索引,而且只有InnoDB能够创建聚集索引。
非聚集索引:也叫普通索引、辅助索引(Secondary Index)、二级索引等,除了聚集索引外的索引都是非聚集索引。
聚集索引的B+Tree叶子节点上存储的是整行的数据,而非聚集索引的B+Tree索引树上只会存储当前索引列的数据与主键索引列的数据,并不会存放整行数据,当需要通过非聚集索引去检索一行数据时,首先非聚集索引通过索引树找到主键,然后通过主键去主键建立的B+Tree上查询出整行的数据;
- 聚集索引与非聚集索引示意图:

回表查询需要把聚集索引加载进内存,首先加载的应该是根节点,而不是直接定位到叶子节点
通过上面的索引图我们能够很直观的发现,普通索引(非聚集索引)的查询如果查询到了其他列的数据,那么需要借助主键索引来查询整行的数据,这个过程也称为回表查询;
2.3.2 索引组织表
其实,MySQL中的表的所有的数据都是按照主键排序的方式存储在一颗B+Tree上(InnoDB引擎),这颗B+Tree也叫聚集索引,所以MySQL叫索引组织表(InnoDB引擎);
索引组织表的数据按主键排序手段被存储在B+Tree索引中,除了存储主键列值外还存储非主键列的值。普通索引只存储索引列,索引组织表就是索引(主键索引)存储表的所有列的值
在MySQL中,聚集索引就是我们看到的表,表就是一颗聚集索引;
2.3.3 聚集索引特点
关于聚集索引还有一个定义:数据行的物理排列顺序与该列值(主键)的逻辑顺序相同,并且一个表中只能拥有一个聚集索引;
测试表:
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`age` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, '小龙', 20);
INSERT INTO `t_user` VALUES (2, '小明', 29);
INSERT INTO `t_user` VALUES (3, '小刚', 30);
INSERT INTO `t_user` VALUES (4, '小美', 24);
INSERT INTO `t_user` VALUES (5, '小军', 26);
INSERT INTO `t_user` VALUES (6, '小龙', 19);
INSERT INTO `t_user` VALUES (7, '小康', 25);
INSERT INTO `t_user` VALUES (8, '小红', 20);
- 什么是"数据行的物理排列顺序"?
数据行的物理排列顺序就是我们眼睛直观看到的列的排列顺序

一般来说我们主键都是自增的,大小都是从小到大,因此会忽略一个问题:数据行的物理排列顺序与聚集索引的逻辑排列顺序是保持一致的
我们把"小军"的ID改为50,重新查看数据库表,发现排列顺序如下:

发现数据行的物理排列顺序默认是和主键顺序保存一致的;
这也是聚集索引在一张表中只能有一个的原因,为什么呢?因为我数据表的逻辑排列顺序要与聚集索引的排列顺序保持一致!那如果有多个聚集索引我到底跟谁保持一致呢??
而且我们知道MySQL是索引组织表的,如果聚集索引有多份,那么数据是否也会存在多份呢?这样存储效率明显下降
2.3.4 MySIAM引擎索引底层实现原理图
虽然MyISAM和InnoDB的索引底层采用的都是B+Tree数据结构,但MyISAM和InnoDB索引的底层实现稍有不同:

MyISAM没有聚集索引,MyISAM建立的都是普通索引(即使是主键);
MyISAM引擎是没有聚集索引的,因为MyISAM引擎不属于索引组织表,即有单独的空间存储表数据,表数据并不是和主键建立的B+Tree存储在一起的;MyISAM表通过任何索引来查询数据都需要回表查询(前提是查询到了索引列之外的数据);
2.3.5 InnoDB的主键策略
我们知道InnoDB表是属于索引组织表,整表的数据都需要根据主键(聚集索引)来排序,并且数据也是存储在主键(聚集索引)的B+Tree上的;那么万一我们在表中没有创建主键(聚集索引)那么怎么办呢?
答:InnoDB可以没有主键,但是必须要有且只有一个(聚集索引),不能没有(聚集索引)
聚集索引对于InnoDB实在是太重要了,InnoDB不能没有他,如果你不创建他会根据规则来选出较为合适的一列来做聚集索引,实在不行他就帮你创建一个隐藏的列作为聚集索引,规则如下:
(1)如果表定义了主键,则该列就是聚集索引;
(2)如果表没有定义主键,则第一个not null unique列是聚集索引;
(3)以上条件都不满足:InnoDB会创建一个隐藏的row-id作为聚集索引;
三、应用索引
3.1 覆盖索引
3.1.1 覆盖索引概念
覆盖索引(或称索引覆盖):查询的数据刚好在索引树上,不需要回表查询。很显然,聚簇索引就是一种覆盖索引,因为聚簇索引中包含了数据行的全部数据,而非聚集索引的话,要看SQL语句查询的列是否在索引树上,如果不在则需要回表查询;简单的说就是查询的列要被所使用的索引覆盖,换句话说就是查询的列要在索引树上,不需要回表查询。
3.1.2 覆盖索引应用
使用覆盖索引的SQL语句:只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
我们给username列加上索引:
create index idx_name on t_user(username);
- 执行如下SQL:
-- username创建的B+Tree上有值(使用了覆盖索引)
explain select username from t_user where username='xxx';
-- username创建的B+Tree上有值(使用了覆盖索引)
explain select id,username from t_user where username='xxx';
-- username创建的B+Tree上没有值,age列需要回表到聚集索引上去查询(没有使用覆盖索引)
explain select id,username,age from t_user where username='xxx';

- 小结:
覆盖索引:查询的数据被索引树覆盖了,即:查询的数据在索引树上,不需要回表查询;
3.2 前缀索引
3.2.1 前缀索引概念
前缀索引也是一种概念,或者说是操作索引的一种技巧;当索引列的数据是非常大时,那么该列建立的索引会非常大,而且检索速度也会很慢,这个时候我们考虑能否让该列的前面几个字符拿出来建立索引,而不是整列是数据建立索引,因此前缀索引的概念就由此产生;
我们知道前缀索引其实就是拿出该列数据的前几个字符出来建立索引以降低索引的大小,以及加快索引的速度的一种技巧性索引,但是毕竟前面几个字符不能够代替整列数据,有可能重复,我们应该尽量的降低重复的概率,降低重复概率,这样的前缀索引检索速度更快;
3.2.2 前缀索引的应用
- 数据准备:
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`birthday` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB default CHARSET=utf8;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '小明', '1999-10-20');
INSERT INTO `student` VALUES (2, '小军', '1999-02-21');
INSERT INTO `student` VALUES (3, '小龙', '1999-01-19');
INSERT INTO `student` VALUES (4, '小刚', '1999-06-06');
INSERT INTO `student` VALUES (5, '小红', '1999-02-05');
- 计算重复率公式:
去重后的数据 / 总的数据
-- 只查询birthday这一列
select birthday from student
-- 只查询birthday列左边num个字符
select left(birthday,7) from student
-- 在列的最左边去除num个字符后去重后,查询记录
select distinct left(birthday,num) from student;
-- 在列的最左边去除num个字符后去重后,统计总条数
select 1.0*count(distinct left(birthday,num)) from student;
-- 将去重后的数据的总条数 / 表中的总条数 = 不重复率
select 1.0*count(distinct left(birthday,num))/count(*) from student;
建立前缀索引就是计算数据到了前缀的第几个字符后,数据没有重复的;我们就可以以前面几个字符来建立索引,这样索引的大小就可以得到压缩,查询速度可以加快;
- 案例:
mysql> select 1.0*count(distinct birthday)/count(*) from student;
+---------------------------------------+
| 1.0*count(distinct birthday)/count(*) |
+---------------------------------------+
| 1.00000 |
+---------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,1))/count(*) from student; # 1 / 5 = 0.2
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,1))/count(*) |
+-----------------------------------------------+
| 0.20000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,2))/count(*) from student; # 1 / 5 = 0.2
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,2))/count(*) |
+-----------------------------------------------+
| 0.20000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,3))/count(*) from student; # 1 / 5 = 0.2
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,3))/count(*) |
+-----------------------------------------------+
| 0.20000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,4))/count(*) from student; # 1 / 5 = 0.2
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,4))/count(*) |
+-----------------------------------------------+
| 0.20000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,5))/count(*) from student; # 1 / 5 = 0.2
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,5))/count(*) |
+-----------------------------------------------+
| 0.20000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,6))/count(*) from student; # 2 / 5 = 0.4
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,6))/count(*) |
+-----------------------------------------------+
| 0.40000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,7))/count(*) from student; # 2 / 5 = 0.4
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,7))/count(*) |
+-----------------------------------------------+
| 0.80000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,8))/count(*) from student; # 4 / 5 = 0.8
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,8))/count(*) |
+-----------------------------------------------+
| 0.80000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> select 1.0*count(distinct left(birthday,9))/count(*) from student; # 5 / 5 = 1
+-----------------------------------------------+
| 1.0*count(distinct left(birthday,9))/count(*) |
+-----------------------------------------------+
| 1.00000 |
+-----------------------------------------------+
1 row in set (0.00 sec)
发现不重复率在birthday字段在第9个字符时,达到100%,也就是说,在的倒数第二个字符时,数据没有重复的。
重复概率为0,不重复概率为100%

计算出重复率最高时是在该列的低9个字符,因此我们可以把第9个字符之外的数据不用建立索引,来降低索引的大小,提高索引的检索速度;
alter table student add key(birthday(9));
下次根据birthday字段查询,会发现使用了索引查询:
explain select birthday from student where birthday='1999-02-21';

如果建立的前缀索引有重复数据会怎么样?
我们刚刚建立前缀索引时,首先要计算重复率,然后再根据得出合适的位置来建立索引,那么如果不计算重复率会怎么样??
我们把刚刚建立的索引删除,在第4个字符位置上建立索引:
alter table student drop index birthday;
alter table student add key(birthday(4));

执行如下SQL分析执行计划:
explain select birthday from student where birthday='1999';
explain select birthday from student where birthday='1999-02-21';
explain select birthday from student where birthday='1999-10-21';

我们随机执行了几条SQL语句,发现都没有走索引查询,而是全表顺序扫描,因为重复的数据太多了(占整表数据),MySQL优化器认为走索引还不如不走索引,因此选择顺序扫描查询;
我们切换个案例,把索引前缀字符切换为7:
alter table student drop index birthday;
alter table student add key(birthday(7));

执行如下SQL:
explain select birthday from student where birthday='1999-10-21';
explain select birthday from student where birthday='1999';

3.3 全文索引
3.3.1 全文索引概念
如果希望通过关键字的匹配度来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较。全文索引就是为这种场景设计的。MySQL也提供有类似于Lucene的全文检索技术,用于处理大规模数据检索,其处理速度比like高效的多
全文索引使用说明:
- MySQL 5.6 以前的版本,只有 MyISAM 存储引擎支持全文索引;MySQL 5.6 及以后的版本,MyISAM 和 InnoDB 存储引擎均支持全文索引;
- 只有字段的数据类型为 char、varchar、text 及其系列才可以建全文索引。
创建全文索引:
create fulltext index index_name on table_name(col_name);
alter table table_name add fulltext index index_name(col_name);
使用全文索引:
select * from table_name where match(fulltext_col) against('content');
match函数中指定全文索引列,asainst函数中指定具体要搜索的内容;
3.3.2 匹配长度
MySQL 中的全文索引,有两个变量,最小搜索长度和最大搜索长度,对于长度小于最小搜索长度和大于最大搜索长度的词语,都不会被索引。通俗点就是说,想对一个词语使用全文索引搜索,那么这个词语的长度必须在以上两个变量的区间内。
我们可以通过以下命令查询:
show variables like '%ft%';

其中前缀带有innodb的为innodb引擎变量,其余为myisam引擎变量
// MyISAM变量
ft_min_word_len = 4;
ft_max_word_len = 84;
// InnoDB变量
innodb_ft_min_token_size = 3;
innodb_ft_max_token_size = 84;
Tips:我们可以看出innodb的最小搜索长度为3,最大搜索长度为84;实际开发中最大搜索长度可能会设大
创建测试表:
create table test (
id int(11) not null auto_increment,
content varchar(30) not null,
PRIMARY KEY (`id`) USING BTREE,
FULLTEXT INDEX (`content`)
) engine=myisam default charset=utf8;
insert into test(content) values('a'),('aa'),('aaa'),('aaaa');
执行查询:
select * from test where match(content) against('a');
select * from test where match(content) against('aa');
select * from test where match(content) against('aaa');
select * from test where match(content) against('aaaa');
发现只有搜索aaa和aaaa时才会出现记录;

我们可以尝试修改mysql的系统配置(/etc/my.cnf):
[mysqld]
innodb_ft_min_token_size = 1
ft_min_word_len = 1
重启mysql服务:
systemctl restart mysqld
修复索引:
repair table test quick; -- myisam(修复表)
optimize table test; -- innodb(优化表)
Tips:修改完参数以后,一定要修复下索引,因为当初建立索引是基于
innodb_ft_min_token_size=3的
3.3.3 全文索引的模式
1)自然语言处理模式
in natural language mode:自然语言处理模式;默认情况下,MySQL使用 in natural language mode 修饰符,match() 函数对文本集合执行自然语言搜索,实现从一个文本集合中搜索给定的字符串
创建测试表:
CREATE TABLE `goods` (
`id` int(11) NOT NULL,
`title` varchar(255),
PRIMARY KEY (`id`) USING BTREE,
FULLTEXT INDEX (`title`)
) ENGINE = InnoDB default CHARSET=utf8;
INSERT INTO `goods`(`id`, `title`) VALUES (1, 'huawei 5G zhineng paizhao shouji');
INSERT INTO `goods`(`id`, `title`) VALUES (2, 'xiaomi 5G zhineng youxi shouji');
INSERT INTO `goods`(`id`, `title`) VALUES (3, 'xiaomi 4G fashao shouji');
INSERT INTO `goods`(`id`, `title`) VALUES (4, 'huawei 4G youxi qumianping shouji');
查询:
select * from goods where match(title) against('xiaomi 5G' in natural language mode);
select * from goods where match(title) against('xiaomi 5G'); -- 简写

2)布尔处理模式
in boolean mode:布尔处理模式;如果在against()函数中指定了in boolean mode模式,则MySQL会执行布尔全文搜索。在该搜索模式下,待搜索单词前或后的一些特定字符会有特殊的含义。
- + 必须包含该词
- - 必须不包含该词
- > 提高该词的相关性,查询的结果靠前
- < 降低该词的相关性,查询的结果靠后
- ***** 通配符,只能接在词后面
- () 分组查询
1)包含与不包含:
-- 分词查询
select * from goods where match(title) against('xiaomi 5G' in boolean mode);
-- 必须不包含5G词条
select * from goods where match(title) against('xiaomi -5G' in boolean mode);
-- 必须包含5G词条
select * from goods where match(title) against('xiaomi +5G' in boolean mode);

2)提高/降低权重(相关性)
select * from goods where match(title) against('xiaomi >4G' in boolean mode);
select * from goods where match(title) against('xiaomi <4G' in boolean mode);

3)通配符(模糊)查询:
select * from goods where match(title) against('xiao' in boolean mode);
select * from goods where match(title) against('xiao*' in boolean mode);

4)分组查询:
查询必须包含5G并且必须包含paizhao或youxi的商品,youxi商品权重降低
select * from goods where match(title) against('+5G +(paizhao <youxi)' in boolean mode);

3.3.4 N-gram中文分词
MySQL的全文搜索对于英文是基于空格的分词,由于中文没有空格,因此MySQL的全文索引对中文支持的不是很友好
中文查询测试:
INSERT INTO `test`.`goods`(`id`, `title`) VALUES (5, '华为5G智能拍照手机');
INSERT INTO `test`.`goods`(`id`, `title`) VALUES (6, '小米5G智能游戏手机');
INSERT INTO `test`.`goods`(`id`, `title`) VALUES (7, '小米4G发烧 手机'); -- 故意弄一个空格用于分词
INSERT INTO `test`.`goods`(`id`, `title`) VALUES (8, '华为4G游戏曲面屏手机');
select * from goods where match(title) against('华为');
select * from goods where match(title) against('小米');
select * from goods where match(title) against('手机');

使用ngram中文分词:
CREATE TABLE `article` (
`id` int(11) NOT NULL,
`title` varchar(255) ,
PRIMARY KEY (`id`) USING BTREE,
FULLTEXT INDEX `title`(`title`) with parser ngram -- 使用ngram中文分词器
) ENGINE = InnoDB default CHARSET=utf8;
INSERT INTO `article`(`id`, `title`) VALUES (1, '华为5G智能游戏手机');
INSERT INTO `article`(`id`, `title`) VALUES (2, '小米5G全面屏拍照手机');
INSERT INTO `article`(`id`, `title`) VALUES (3, 'vivo美颜拍照手机');
INSERT INTO `article`(`id`, `title`) VALUES (4, '新款oppo高性能音乐拍照全网通');
执行查询:
select * from article where match(title) against("拍照");

MySQL全文索引官方手册:https://dev.mysql.com/doc/refman/5.7/en/innodb-fulltext-index.html
n-gram parser:https://dev.mysql.com/doc/refman/5.7/en/fulltext-search-ngram.html
3.4 其他概念索引
3.4.1 一级索引与二级索引
关于一级索引的定义:索引和数据存储是在一起的,都存储在同一个B+Tree中的叶子节点。一般主键索引都是一级索引。
关于二级索引的定义:二级索引树的叶子节点存储的是本列索引值和主键值;而不是整行数据。在使用二级索引检索整行数据时,需要借助一级索引;也就是说,在找到索引后,得到对应的主键,再回到一级索引中找主键对应的数据记录。
Tips:一级索引就是聚集索引,二索引就是非聚集索引
3.4.2 辅助索引
辅助索引就是辅助我们查询的索引,一般指的就是非聚集索引(二级索引)
3.5 索引列的离散性
我们都知道MySQL底层是有优化器的,最终是否使用到索引,以及具体使用哪个索引是MySQL优化器根据一系列成本计算最终得出的结果,如果列的离散性越高,证明列的重复概率越低,优化器更容易选择,如果列的离散性非常底,那么优化器在选择列的时候也会有选择困难症,降低检索速度,如果列的离散性非常低,那么很有可能优化器就不选择索引,直接进行全表扫描了;
离散性计算公式:count(distinct col)/count(col);
测试数据:
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`age` int(11) NULL DEFAULT NULL,
`gender` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB;
INSERT INTO `test`.`student`(`id`, `name`, `age`, `gender`) VALUES (1, '张三', 20, '0');
INSERT INTO `test`.`student`(`id`, `name`, `age`, `gender`) VALUES (2, '李四', 18, '1');
INSERT INTO `test`.`student`(`id`, `name`, `age`, `gender`) VALUES (3, '王五', 19, '0');
INSERT INTO `test`.`student`(`id`, `name`, `age`, `gender`) VALUES (4, '赵六', 18, '1');
INSERT INTO `test`.`student`(`id`, `name`, `age`, `gender`) VALUES (5, '孙七', 20, '1');
INSERT INTO `test`.`student`(`id`, `name`, `age`, `gender`) VALUES (6, '周八', 20, '1');
计算name、age、gender的离散值:
select
(select count(distinct name)/count(1) from student) name_col,
(select count(distinct age)/count(1) from student) name_col,
(select count(distinct gender)/count(1) from student) name_col

3.6 索引的分类
MySQL的索引实在是太多了,这么多索引我们该怎么记??以及该如何区分??
我们看待事物的角度不同,索引也可以分为以下角度:
- 数据结构角度
- B+Tree索引
- R-Tree索引(空间索引)
- Hash索引
- Full-text索引(全文索引)
- 物理存储角度
- 聚集索引 (一级索引)
- 非聚集索引(二级索引)
- 逻辑角度
- 主键索引
- 普通索引(单列索引)
- 多列索引(复合索引)
- 唯一索引
- 非唯一索引
- 空间索引
- 业务角度
- 覆盖索引
- 前缀索引
- 辅助索引(非聚集索引)
3.7 索引的设计
索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率,更高效的使用索引。
- 尽量选择离散性高的列建立索引(优化器选择性好)
- 采用复合索引(联合索引底层原理)
- 严格避免索引列的失效(索引失效)
- 注意最左前缀法则

浙公网安备 33010602011771号