文章目录
- 一、存储引擎
- 1.1 查看存储引擎
- 1.2 修改默认存储引擎
- 1.3 常见存储引擎
- 1.4 存储引擎的特点
- 1.4.1 InnoDB 存储引擎
- 1.4.2 MyISAM 存储引擎
- 1.4.2.1 MyISAM与InnoDB对比
- 1.4.2.2 批量插入性能测试
- 1.4.2.3 MyISAM压缩表
- 1.4.3 Merge 存储引擎
- 1.4.4 Memory 存储引擎
- 二、索引
- 2.1 索引概述
- 2.2 索引的优缺点
- 2.3 索引的类型
- 2.3.1 数据结构角度
- 2.3.2 Hash
- 2.3.3 B+Tree
- 2.3.3.1 二叉树
- 2.3.3.2 平衡二叉树
- 2.3.3.3 B-Tree
- 2.3.3.4 B+Tree
- 2.3.2 InnoDB数据页
- 2.3.3 物理存储角度
- 2.3.3.1 聚集索引与非聚集索引
- 2.3.3.2 索引组织表
- 2.3.3.3 聚集索引说明
- 2.3.3.4 MySIAM引擎索引底层实现原理图
- 2.3.3.5 InnoDB的主键策略
- 2.4 其他索引
- 2.4.1 覆盖索引
- 2.4.1.1 覆盖索引概念
- 2.4.1.2 覆盖索引应用
- 2.4.1.3 覆盖索引总结
- 2.4.2 前缀索引
- 2.4.2.1 前缀索引概念
- 2.4.2.2 前缀索引的应用
- 2.4.3 全文索引
- 2.4.3.1 全文索引概念
- 2.4.3.2 匹配长度
- 2.4.3.2 全文索引的模式
- 1)自然语言处理模式
- 2)布尔处理模式
- 2.4.3.3 N-gram中文分词
- 2.4.4 一级索引与二级索引
- 2.4.5 辅助索引
- 2.5 索引列的离散性
- 2.6 索引的分类
- 2.7 索引的设计
一、存储引擎
数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySQL的核心就是插件式存储引擎。
Oracle,SQL Server等数据库只有一种存储引擎。MySQL提供了插件式的存储引擎架构。所以MySQL存在多种存储引擎,可以根据需要使用相应引擎,或者编写存储引擎;
1.1 查看存储引擎
show engines;
查看当前数据库默认使用什么存储引擎:
show variables like '%storage_engine%';
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>
1.3 常见存储引擎
特点 | InnoDB | MyISAM | MEMORY | MERGE | NDB |
---|---|---|---|---|---|
存储限制 | 64TB | 有 | 有 | 没有 | 有 |
事务安全 | 支持 | ||||
锁机制 | 行锁、表锁 | 表锁 | 表锁 | 表锁 | 行锁 |
B树索引 | 支持 | 支持 | 支持 | 支持 | 支持 |
哈希索引 | 支持 | ||||
全文索引 | 支持(5.6版本之后) | 支持 | |||
集群索引 | 支持 | ||||
数据索引 | 支持 | 支持 | 支持 | ||
索引缓存 | 支持 | 支持 | 支持 | 支持 | 支持 |
数据可压缩 | 支持 | ||||
空间使用 | 高 | 低 | N/A | 低 | 低 |
内存使用 | 高 | 低 | 中等 | 低 | 高 |
批量插入速度 | 低 | 高 | 高 | 高 | 高 |
支持外键 | 支持 |
在各种常用存储引擎中,只有InnoDB支持事务与外键。
1.4 存储引擎的特点
1.4.1 InnoDB 存储引擎
InnoDB存储引擎是MySQL的默认存储引擎。InnoDB存储引擎提供了具有提交、回滚、崩溃恢复能力的事务安全。而且InnoDB对事务处理的能力,也是其他存储引擎不能比拟的。并且会占用更多的磁盘空间以保留数据和索引。
- InnoDB 存储引擎特点
- 事务:支持事务的ACID
- 外键:支持外键
- 锁粒度:行锁、表锁
- 存储形式为:.frm 表定义文件 .ibd 数据文件和索引文件
1.4.2 MyISAM 存储引擎
在MySQL5.1及之前的版本,MySIAM是默认的存储引擎。MyISAM是基于ISAM引擎发展起来的,增加了许多有用的扩展。MyISAM的优势在于占用空间小,处理速度快。缺点是不支持事务的==完整性和修改并发==。
1.4.2.1 MyISAM与InnoDB对比
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数据的存储路径:
show variables like '%dir%';
- Linux下的:
其中datadir就是MySQL存放数据的目录
cd /var/lib/mysql
- 再查看我在Winodws本机的:
MyISAM不支持事务、也不支持外键,但其访问速度快,对事务完整性没有要求。
MySIAM表支持三种不同存储格式:
1)静态表
静态表是默认的存储格式,静态表中的字段都是非变长的字段(char),优点是:存储非常迅速,出现故障容易恢复;缺点是:占用的空间通常比动态表多。
2)动态表
动态表的字段是变长的(varchar),优点是:占用的空间相对较少,但是频繁地更新删除记录会产生碎片,需要定期改善性能,并且出现故障的时候恢复相对比较困难。
3)压缩表
MyISAM可以使用myisampack工具压缩表数据,压缩表占用磁盘空间小,每个记录是被单独压缩的,所以只有非常小的访问开支。
1.4.2.2 批量插入性能测试
CREATE TABLE `user_myisam` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- 存储引擎为myisam
CREATE TABLE `user_innodb` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- 存储引擎为innodb
-- 创建存储过程批量插入数据
create procedure innodb_insert(count int)
begin
declare i int default 1;
while i<=count do
INSERT INTO user_innodb values(i,uuid());
set i=i+1;
end while;
end;
-- 创建存储过程批量插入数据
create procedure myisam_insert(count int)
begin
declare i int default 1;
while i<=count do
INSERT INTO user_myisam values(i,uuid());
set i=i+1;
end while;
end;
- 执行innodb批量插入5W条记录查看其性能如下:
call innodb_insert(50000);
- 执行MyISAM批量插入5W条记录查看其性能如下:
call myisam_insert(50000);
对比发现innodb存储引擎插入5W条记录需要花费7.32s,而MyISAM存储引擎只需要0.73s。
1.4.2.3 MyISAM压缩表
我们知道MyISAM存储引擎中,数据的存储文件为.frm(表结构文件)、.myd(数据文件)、.myi(索引文件),而在Myisam引擎中,是可以对表数据文件进行压缩的。
使用myisampack压缩命令压缩数据文件
myisampack user_myisam # 不用加后缀
再次查看数据文件大小:
MySIAM存储引擎的业务场景:
- 不需要事务支持的业务(例如转账就不行)。
- 一般为读数据比较多的应用,读写都频繁场景不适合(锁机制问题)
- 以读为主的业务,例如:数据库系统表、日志、图片信息数据库
- 使用读写分离的MySQL读库可以使用MyISAM。
1.4.3 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.4.4 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.1 索引概述
索引是帮助数据库快速查询数据的一种数据结构,在数据库中,数据库系统除了存储数据之外还维护着一种特殊的数据结构,这种数据结构以某种方式指向数据,这样就可以在这种数据结构上实现高级算法来查询数据,这种数据结构就是索引。
- 索引示意图:
如图所示,索引能够帮我们快速的定位到数据的具体位置,高效查询。一般来说,索引本身也很大,不可能全部存储在内存当中,因此索引往往以索引文件的形式存储在磁盘上。索引是用来提供高性能数据库的常用工具。
2.2 索引的优缺点
- 优点
1) 类似于书籍的目录索引,提高数据检索的效率,降低数据库的IO成本。
2) 通过索引列对数据进行排序,降低数据排序的成本,降低CPU的消耗。
- 缺点
1) 实际上索引也是一张表,该表中保存了主键与索引字段,并指向实体类的记录,所以索引列也是要占用空的。
2)索引虽然大大提升了查询效率,但同时也降低了更新表的速度,如果对表进行了insert/update/delete等语句时,数据库不仅要保存数据,还需更新索引文件,每次更新添加了索引列的字段,都会调整索引的结构。
2.3 索引的类型
索引是在MySQL的存储引擎层中实现的,而不是在服务器层实现的。所以每种存储引擎的索引都不一定完全相同,也不是所有的存储引擎都支持所有的索引类型的。
2.3.1 数据结构角度
- 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.3.2 Hash
当表中的某一列建立了hash索引时,首先使用hash算法将列的键值计算为hash值,然后根据计算的hash值来选择存储对应的位置;hash索引底层采用一张hash表建立索引与列的关系;通常情况下,使用hash索引查询一次即可定位要查询的行,但是当hash冲突时hash表的某个槽位将会存储很多的键值,查询性能会变低;
1)新增数据时,先根据键值计算出hash值,通过hash值一个映射到一个槽位中;当多个数据计算的hash值一致时(hash冲突),可能映射到同一个槽位,这个时候可以通过链表来解决hash冲突问题;
2)当根据hash索引查询数据时,首先计算hash值,通过hash值去hash表中查询数据;
2.3.3 B+Tree
2.3.3.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.3.3.2 平衡二叉树
平衡二叉树(AVL树)在符合二叉查找树的条件下,还满足任何节点的两个子树的高度最大差为1。
我们按照顺序依次插入6,3,7,2,4,1,5
数据,观察AVL树与普通二叉树的演变:
2.3.3.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。
2.3.3.4 B+Tree
MySQL索引数据结构对经典的B-Tree进行了优化。在原B-Tree的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能。并且B+Tree数据结构将数据全部存储在最底层的叶子节点,这样可以保证每个磁盘块能够存储更多的指针,查询数据更广。而B-Tree想要存储B+Tree同样的数据智能增加树的高度,导致性能降低;
通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。
2.3.2 InnoDB数据页
- 操作系统的局部性原理
当我们要做数据处理时,需要把磁盘中的数据调出到内存中来进行处理,这个过程会产生磁盘的IO(输入输出),由于磁盘IO是非常昂贵的操作,所以计算机操作系统对此做了优化。每一次IO时,不仅仅把当前磁盘地址的数据加载到内存,同时也把相邻数据也加载到内存缓冲区中;
计算机磁盘存储数据最小单元是扇区,一个扇区通常大小为512字节;而文件系统(NTFS/EXT/FAT等)的最小的操作单位是块(页),一个块的大小是4K;也就是说我所使用的文件系统中1个块是由连续的8个扇区组成;每次磁盘IO读取数据时,其大小为一个块,也就是4K;
通过如下指令可以查询:
fsutil fsInfo ntfsInfo C:
- InnoDB数据页管理
在InnoDB中,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB,可通过参数innodb_page_size
将页的大小设置为4K、8K、16K,在MySQL中可通过如下命令查看页的大小:
show variables like 'innodb_page_size';
Tips:若已经初始化mysql实例,仍要进行修改,则会无法启动mysql服务,需要删除已经存在的所有的ibdata和ib_logfile文件
而系统一个磁盘块的存储空间往往没有这么大(4K),因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块(4个)来达到页的大小16KB。InnoDB在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。
- B-Tree:
B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。
- B+Tree:
在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。
Tips:InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为
10^3
)。也就是说一个深度为3的B+Tree索引可以维护10^3 _ 10^3 _ 10^3 = 10亿 条记录。
2.3.3 物理存储角度
2.3.3.1 聚集索引与非聚集索引
索引按照物理存储结构划分分为聚集索引和非聚集索引
**聚集索引:**也叫聚簇索引(ClusterIndex),一般来说是以主键创建的索引,一张表只能有一个聚集索引,而且只有InnoDB能够创建聚集索引。
非聚集索引:也叫普通索引、辅助索引(Secondary Index)、二级索引等,除了聚集索引外的索引都是非聚集索引。
聚集索引的B+Tree叶子节点上存储的是整行的数据,而非聚集索引的B+Tree索引树上只会存储当前索引列的数据与主键索引列的数据,并不会存放整行数据,当需要通过非聚集索引去检索一行数据时,首先非聚集索引通过索引树找到主键,然后通过主键去主键建立的B+Tree上查询出整行的数据;
- 聚集索引与非聚集索引示意图:
回表查询需要把聚集索引加载进内存,首先加载的应该是根节点,而不是直接定位到叶子节点
通过上面的索引图我们能够很直观的发现,普通索引(非聚集索引)的查询如果查询到了其他列的数据,那么需要借助主键索引来查询整行的数据,这个过程也称为回表查询;
2.3.3.2 索引组织表
其实,MySQL中的表的所有的数据都是按照主键排序的方式存储在一颗B+Tree上(InnoDB引擎),这颗B+Tree也叫聚集索引,所以MySQL叫索引组织表(InnoDB引擎);
索引组织表的数据按主键排序手段被存储在B+Tree索引中,除了存储主键列值外还存储非主键列的值。普通索引只存储索引列,索引组织表就是索引(主键索引)存储表的所有列的值
在MySQL中,聚集索引就是我们看到的表,表就是一颗聚集索引;
2.3.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.3.4 MySIAM引擎索引底层实现原理图
虽然MyISAM和InnoDB的索引底层采用的都是B+Tree数据结构,但MyISAM和InnoDB索引的底层实现稍有不同:
MyISAM没有聚集索引,MyISAM建立的都是普通索引(即使是主键);
MyISAM引擎是没有聚集索引的,因为MyISAM引擎不属于索引组织表,即有单独的空间存储表数据,表数据并不是和主键建立的B+Tree存储在一起的;MyISAM表通过任何索引来查询数据都需要回表查询(前提是查询到了索引列之外的数据);
2.3.3.5 InnoDB的主键策略
我们知道InnoDB表是属于索引组织表,整表的数据都需要根据主键(聚集索引)来排序,并且数据也是存储在主键(聚集索引)的B+Tree上的;那么万一我们在表中没有创建主键(聚集索引)那么怎么办呢?
答:InnoDB必须要有且只有一个(聚集索引),不能没有(聚集索引)
主键(聚集索引)对于InnoDB实在是太重要了,InnoDB不能没有他,如果你不创建他会根据规则来选出较为合适的一列来做聚集索引,实在不行他就帮你创建一个隐藏的列作为聚集索引,规则如下:
(1)如果表定义了主键,则该列就是聚集索引;
(2)如果表没有定义主键,则第一个**not null unique**
列是聚集索引;
(3)以上条件都不满足:InnoDB会创建一个隐藏的**row-id**
作为聚集索引;
2.4 其他索引
2.4.1 覆盖索引
2.4.1.1 覆盖索引概念
覆盖索引(或称索引覆盖):查询的数据刚好在索引树上,不需要回表查询。很显然,聚簇索引就是一种覆盖索引,因为聚簇索引中包含了数据行的全部数据,而非聚集索引的话,要看SQL语句查询的列是否在索引树上,如果不在则需要回表查询;简单的说就是查询的列要被所使用的索引覆盖,换句话说就是查询的列要在索引树上,不需要回表查询。
2.4.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';
2.4.1.3 覆盖索引总结
覆盖索引:查询的数据被索引树覆盖了,即:查询的数据在索引树上,不需要回表查询;
2.4.2 前缀索引
2.4.2.1 前缀索引概念
前缀索引也是一种概念,或者说是操作索引的一种技巧;当索引列的数据是非常大时,那么该列建立的索引会非常大,而且检索速度也会很慢,这个时候我们考虑能否让该列的前面几个字符拿出来建立索引,而不是整列是数据建立索引,因此前缀索引的概念就由此产生;
我们知道前缀索引其实就是拿出该列数据的前几个字符出来建立索引以降低索引的大小,以及加快索引的速度的一种==技巧性索引,但是毕竟前面几个字符不能够代替整列数据,有可能重复,我们应该尽量的减低重复的概率,降低重复概率==,这样的前缀索引检索速度更快;
2.4.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 ;
-- ----------------------------
-- 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';
2.4.3 全文索引
2.4.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函数中指定具体要搜索的内容;
2.4.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
的
2.4.3.2 全文索引的模式
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 ;
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);
2.4.3.3 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 ;
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
2.4.4 一级索引与二级索引
关于一级索引的定义:索引和数据存储是在一起的,都存储在同一个B+Tree中的叶子节点。一般主键索引都是一级索引。
关于二级索引的定义:二级索引树的叶子节点存储的是本列索引值和主键值;而不是整行数据。在使用二级索引检索整行数据时,需要借助一级索引;也就是说,在找到索引后,得到对应的主键,再回到一级索引中找主键对应的数据记录。
一级索引就是聚集索引,二索引就是非聚集索引
2.4.5 辅助索引
辅助索引就是辅助我们查询的索引,一般指的就是非聚集索引(二级索引)
2.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
2.6 索引的分类
MySQL的索引实在是太多了,这么多索引我们该怎么记??以及该如何区分??
我们看待事物的角度不同,索引也可以分为以下角度:
- 数据结构角度
- B+Tree索引
- R-Tree索引(空间索引)
- Hash索引
- Full-text索引(全文索引)
- 物理存储角度
- 聚集索引 (一级索引)
- 非聚集索引(二级索引)
- 逻辑角度
- 主键索引
- 普通索引(单列索引)
- 多列索引(复合索引)
- 唯一索引
- 非唯一索引
- 空间索引
- 业务角度
- 覆盖索引
- 前缀索引
- 辅助索引(非聚集索引)
2.7 索引的设计
索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率,更高效的使用索引。
- 尽量选择离散性高的列建立索引(优化器选择性好)
- 采用联合索引(联合索引底层原理)
- 严格避免索引列的失效(索引失效)
- 注意最左前缀法则