索引概念
常见索引分为:
主键索引(primary key)
唯一索引(unique)
普通索引(index)
全文索引(fulltext)–解决中子文索引问题。
索引的价值
小示例:先整一个海量数据表,在查询的时候,比较有无索引的差距
生成海量表的代码:
drop database if exists `bit_index`;
create database if not exists `bit_index` default character set utf8;
use `bit_index`;
-- 构建一个8000000条记录的数据
-- 构建的海量表数据需要有差异性,所以使用存储过程来创建, 拷贝下面代码就可以了,暂时不用理解
-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
begin
declare chars_str varchar(100) default
'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
declare return_str varchar(255) default '';
declare i int default 0;
while i < n do
set return_str =concat(return_str,substring(chars_str,floor(1+rand()*52),1));
set i = i + 1;
end while;
return return_str;
end $$
delimiter ;
-- 产生随机数字
delimiter $$
create function rand_num( )
returns int(5)
begin
declare i int default 0;
set i = floor(10+rand()*500);
return i;
end $$
delimiter ;
-- 创建存储过程,向雇员表添加海量数据
delimiter $$
create procedure insert_emp(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP values ((start+i)
,rand_string(6),'SALESMAN',0001,curdate(),2000,400,rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;
-- 雇员表
CREATE TABLE `EMP` (
`empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号',
`ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名',
`job` varchar(9) DEFAULT NULL COMMENT '雇员职位',
`mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号',
`hiredate` datetime DEFAULT NULL COMMENT '雇佣时间',
`sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪',
`comm` decimal(7,2) DEFAULT NULL COMMENT '奖金',
`deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号'
);
-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);
使用方法:随便在一个地方创建一个后缀为sql的文件,而后将下面代码拷贝进去,在msyql中 source一下即可
我是将其放在了 /home/lsh/c++/mysql/index_data.sql 下
执行:
注:插入数据时间一般需要挺久的,可以先等他一会
执行途中可能会出现这样的报错,可以参考这篇文章 上述代码执行报错问题
如果是配置了my.cnf的,配置好之后使用systemctl restart mysqld 重启一下mysql服务器即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FRPCnjVo-1686323426541)(null)]
查询员工编号为997878的员工
select * from emp where empno = 997878;
可以看到耗时还是很大的
我们创建索引后,再进行查询一次,对比一下查找时间
alter table emp add index(empno);
select * from emp where empno = 997878;
我们发现创建索引的时间也是需要很久的(创建对应的数据结构是需要一定时间的),而查询时间我们会发现很快
小总结:
磁盘整体结构
部分说明:
永磁铁: 机械硬盘的存储方式与磁带比较类似,磁体具有记忆的功能,永磁铁是为了保证磁性的稳定。
音圈马达: 硬盘读取数据的关键部位,主要作用是将存储在磁盘上的信息转换为电信号向外传输。
主轴: 保证电机稳定的转动,磁盘转动才能读出数据。
空气滤波片: 过滤空气硬盘透气孔中进入的空气,保证硬盘内部清洁,同时还可以防止硬盘内部的零件氧化,确保硬盘安全使用。
磁盘: 硬盘一般都是铝合金制作的,主要是用来存储文件的。
磁头: 用来读取盘片上的信息。
串行接口: 用来连接电脑与硬盘的接口,起到传输的作用。
再来看看磁盘中一个盘片
扇区
数据库文件,本质其实就是保存在磁盘的盘片当中。也就是上面的一个个小格子中,就是我们经常所说的扇区。当然,数据库文件很大,也很多,一定需要占据多个扇区。
说明:
下面就来谈谈如何定位扇区
定位扇区
磁盘具体图如下:
相关概念
os与磁盘io交互的基本单位
补充:理解IO请求本质是什么
我们都知道系统中一定会存在很多的IO的请求,所以os肯定是要对这些IO请求进行管理的,怎么管理呢? 先描述再组织 – os使用对应的结构体描述IO请求,而后使用数据结构对这些结构体进行管理
所以说所谓的IO请求中os眼里实际上就是一个又一个结构体或者说是IO请求描述块
所以说我们向磁盘发送io请求的本质就是,os描述磁盘的结构体里面存储一个组织IO请求描述块的数据结构,对IO请求进行管理
由上面我们知道,我们要找到一个扇区所在之处,先得先知道其在那个盘面(通过磁头定位),而后再定位其在哪个磁道,而后再通过扇区编号就可以定位某个扇区;其中定位扇区所在磁道的过程也叫“寻道”
随机访问:本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据。
连续访问:如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问。
因此尽管相邻的两次IO操作在同一时刻发出,但如果它们的请求的扇区地址相差很大的话也只能称为随机访问,而非连续访问。
磁盘是通过机械运动进行寻址的,连续访问不需要过多的定位,故效率比较高
而其中俩者访问的本质区别就是,随机访问下次进行IO依旧需要做“寻道”,而连续访问再次进行IO操作时则不需要“寻道”
理解mysql软件在什么位置
我们都知道mysql就是一款用来管理文件的应用软件,在操作系统角度来说其实就是对应的代码和数据,而我们知道程序是需要在CPU中执行的,而和CPU直接交互的就是我们的内存(冯诺伊曼体系结构),所以说我们使用mysql时,mysql对应的代码和数据一定是被加载到内存中的,所以说在操作系统视角,mysql就是处于内存中的代码和数据;
而我们知道mysql是专门用来管理文件的应用软件,也可以理解为一种特殊的文件管理系统,而我们的文件实际是存储于磁盘中的,所以我们的mysql是肯定会和磁盘进行IO交互的
mysql和磁盘交互的基本单位
mysql和磁盘之间存在IO交互,就一定存在基本的IO大小,而mysql是具有更高的IO的场景,需要更高的IO效率,所以mysql和磁盘之间的IO交互是更大的,一般是16KB; 这个基础数据单元,在mysql中也叫做page (数据页) ,而我们知道磁盘的基本单位是512字节,这个IO交互是怎么实现的呢?
实际上,数据库文件也是文件,打开数据库在操作系统下就是一个打开的文件,就会有对应的文件描述符,内核级别缓冲区;而我们知道用户层的数据实际上都是先被刷新到对应的文件内核级别缓冲里去,而后再刷新到磁盘中;
所以mysql和磁盘进行IO的框架是这样的mysql - 内核缓冲区- 磁盘 ;而内核缓冲区是属于内核的,而内核和磁盘进行IO交互的单位为4KB,所以说mysql和内核缓冲区进行IO交互是以16KB进行交互的,而内核代替mysql与磁盘进行交互时,是将mysqlIO的16KB数据分成4次IO与磁盘进行交互的;即mysql实际上是以为自己是直接与磁盘以16KB大小交互的,这是因为内核级别缓冲区起到了隔离作用
说明:
MySQL 中的数据文件,是以page为单位保存在磁盘当中的。
MySQL 的 CURD 操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。
而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。
所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是Page。
为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为 BufferPool 的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。
更高的效率,一定要尽可能的减少系统和磁盘IO的次数 – 因为磁盘本身运行速度是很慢的
上面说过,创建索引的本质,实际上就是将热点数据以某种组织形式组织起来,形成对应的数据结构和算法;而所谓的组织形式就是我们上面所说的Page,就是说组织热点数据就是对page进行组织
注:下面讲解都是默认以mysql的Innodb搜索引擎进行讲解
先建立一张测试表,添加主键实际上就是在建立索引
create table if not exists user (
id int primary key, --一定要添加主键哦,只有这样才会默认生成主键索引
age int not null,
name varchar(16) not null
);
然后再按不同顺序插入数据,我们可以发现我们插入数据时,主键值是无序的
我们此时查看一下表中数据分布
我们会发现里面的数据竟然是按照主键值排序好的,为什么数据是按照主键值的排序好的呢?这实际上有page的内部结构是相关的;
MySQL 中要管理很多数据表文件,而要管理好这些文件,就需要先描述,在组织 ,我们目前可以简单理解成一个个独立文件是有一个或者多个Page构成的。
理解为什么IO交互的基本大小是page
如果mysql与磁盘进行IO交互时,采取的方案是用多少,加载多少,就上面的5条记录进行举例:
如果我们一开始要查找id = 1的记录,后面依次查找id = 2,3,4,5的记录,那我们的mysql就需要与磁盘进行5次IO – 效率是极低的
而如果我们使用的是page的方案,我们第一次查询id = 1的记录时,就将id =1 所在的page都加载到了内存中,而后的4次查询,直接在page查询即可,即只需要进行一次IO即可;所以说使用单个page页的是可以大大减少IO的次数
你怎么保证,用户一定下次找的数据,就在这个Page里面?我们不能严格保证,但是有很大概率,因为有局部性原理。往往IO效率低下的最主要矛盾不是IO单次数据量的大小,而是IO的次数。
而将一个一个page组织起来的方式就是双链表,使用链表将一个个page连接到一起,而page本质也是用来存储数据的,而page内部组织数据的方式实际上是使用单链表(为什么不使用数组呢?因为数据一般都是结构体类型的);如下图所示
而为什么会通过主键值对数据进行排序呢,实际上我们创建索引后,因为数据是按照主键值拍好序的有序链表,而我们查询数据只需拿着主键值从头向尾查找即可,没有一个查找是浪费的;但这个查找效率依旧不是很乐观
通过上面的分析,我们知道,上面页模式中,只有一个功能,就是**在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。**但是,我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。
如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的。那么,查找特定一条记录,也一定是线性查找。这效率也太低了
而我们联想一下,我们平时看书时,寻找一本书里面的某一个句话,实际上是这样的流程,通过大目录定位具体章,而后再通过小目录定位具体节,而后开始翻书查找;而索引中就是使用这种方式优化对page的组织
page内部的目录
即在page内部按照键值划分出不同组,以该组的第一个键值作为目录,目录项存储的是俩个值,一个是键值和该键值对应的记录的地址;
例如:我们现在要查找键值为4的记录
我们先去目录项中进行遍历,先遍历1,而4>1,所以4肯定在下一个目录项指向处,而最后遍历到某个大于4的目录项s1,即键值为4的记录就一定处于[3,s1]之间,所以就直接通过目录项3所指向的地址,开始遍历查找键值为4的记录;而原来的方式是需要进行4次遍历,明显目录项遍历的效率会高不少;并且我们这里举的例子都是小数据范围,而处于大数据范围时,这种遍历方式会更加的高效
但page页内目录项这种方案依旧是有以下缺点的
插入数据时,只需要维护page之间的有序性的,即数据插入的位置不一定是在第一个page内的,最坏情况是插入到最后一个page内;即插入数据时,也是需要遍历page里面的目录项,寻找合适的插入位置进行插入,其中还需要线性遍历不同page;这种插入方式效率是非常低的
其次就是我们上面这种方案只是在page页内的效率会很高,而如果我们需要查询的数据是分布在链表中间的page,这就意味着我们需要遍历完前面的所有page里面的目录项最后才能找到对应的记录;而mysql中的page是需要将对应的page加载到内存中,而后对该page进行目录项的遍历;
而将page加载到内存中,这意味着可能某次查询操作就会包含大量的IO的操作
而解决方案就是继续套娃
page和page之间的目录项
使用一个目录项来指向某一页,而这个目录项存放的就是将要指向的页中存放的最小数据的键值。
和页内目录不同的地方在于,这种目录管理的级别是页,而页内目录管理的级别是行。
其中,每个目录项的构成是:键值+指针
而一个目录页就能管理大概管理2KB个page(将目录项按照8字节大小计算),这种管理方案效率是非常高的
其实目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
可是,我们每次检索数据的时候,该从哪里开始呢?虽然顶层的目录页少了,但是还要遍历啊?不用担心,可以在加目录页
而上面的数据结构实际上就是我们的B+树,这也就是索引的底层原理;
注:
上面的目录页使用链表连接只是为了方便,实际情况是没有的,只有底层的Page页才使用了双链表进行组织
小总结:
链表
使用链表的最优情况依旧是线性遍历,所以不可行
二叉搜索树
二叉搜索存在当插入的数据是有序时,会退化成链表,也即线性结构,所以不可行
AVL ,红黑树
虽然说AVL树和红黑树是近似平衡,查询数据的效率也是log(N) 级别的,但维护的层数相比于B+树来说会多很多,而层数越多,就意味着系统和磁盘会有更多的IO交互;虽然AVL和红黑树是可行的,但并不是最优解
HASH
官方的索引实现方式中, MySQL 是支持HASH的,不过 InnoDB 和 MyISAM 并不支持.Hash跟进其算法特征,决定了虽然有时候也很快(O(1)),不过,在面对范围查找就明显不行(因为哈希的数据是散乱排布的,对于范围查找也只能细分为一个数据一个数据的查找,而B+树底层数据是使用链表连接起来的,进行范围查找时,只需查找左端点即可,后面直接遍历链表即可),另外还有其他差别,有兴趣可以查一下。
B 树 vs B + 树
感兴趣的小伙伴可以进入这个网站模拟一下B+树和B树[数据结构模拟网站](Data Structure Visualization (usfca.edu))
使用B树和B+树构建索引结构的示意图
区别之处
B树不行的原因
MyISAM 存储引擎-主键索引
MyISAM 引擎同样使用B+树作为索引结构,但叶节点的data域存放的是数据记录的地址。
下图为 MyISAM 表的主索引,Col1 为主键。
其中MyISAM最大的特点就是,将索引page和数据page分离,也就是叶子节点没有数据,只有对应数据的地址,相较于InnoDB索引,InnoDB是将索引和数据放在一起的,所以MyISAM数据page的设计是比InnoDB更优的;
而其中MyISAM这种用户数据和索引数据分离的方案就是非聚簇索引
InnoDB这种用户数据和索引数据发在一起的方案,就叫做聚簇索引
当然, MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做辅助(普通)索引
而聚簇索引和非聚簇索引最大区别就在于普通索引的存储数据方案上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rCjKlFhw-1686323426525)(null)]
所以通过辅助索引,找到目标记录,需要俩次遍历:首先检测辅助索引获得主键值,然后用主键到主索引中检索获得记录,这样的过程,就叫做回表查询
而至于为什么InnoDB针对这种普通索引的场景,不采取给叶子节点也附上数据,就是因为这样太浪费空间了
其实创建主键就是在创建我们的主键索引;创建唯一键的本质就是创建特殊的普通索引;俩者都具有唯一性,符合索引的结构特征
方式一:
-- 在创建表的时候,直接在字段名后指定 primary key
create table user1(id int primary key, name varchar(30));
方式二:
-- 在创建表的最后,指定某列或某几列为主键索引
create table user2(id int, name varchar(30), primary key(id));
方式三:
create table user3(id int, name varchar(30));
-- 创建表以后再添加主键
alter table user3 add primary key(id);
主键索引的特点:
方式一:
-- 在表定义时,在某列后直接指定unique唯一属性。
create table user4(id int primary key, name varchar(30) unique);
方式二:
-- 创建表时,在表的后面指定某列或某几列为unique
create table user5(id int primary key, name varchar(30), unique(name));
方式三:
create table user6(id int primary key, name varchar(30));
-- 创建表后再创建唯一键索引
alter table user6 add unique(name);
唯一键索引的特点
方式一:
create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name) --在表的定义最后,指定某列为索引
);
方式二:
create table user9(id int primary key, name varchar(20), email varchar(30));
alter table user9 add index(name); --创建完表以后指定某列为普通索引
方式三:
create table user10(id int primary key, name varchar(20), email varchar(30));
-- 创建一个索引名为 idx_name 的索引
create index idx_name on user10(name);
普通索引的特点
注: 创建唯一键索引和普通索引都是需要搭配主键索引的(InnoDB特性)
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引。MySQL提供全文索引机制,但是有要求,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中文。如果对中文进行全文检索,可以使用sphinx的中文版(coreseek)
例:
先创建出一个张带有全文索引的表
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body) -- 全文索引
)engine=MyISAM;
插入一批数据
INSERT INTO articles (title,body) VALUES
('MySQL Tutorial','DBMS stands for DataBase ...'),
('How To Use MySQL Well','After you went through a ...'),
('Optimizing MySQL','In this tutorial we will show ...'),
('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
('MySQL vs. YourSQL','In the following database comparison ...'),
('MySQL Security','When configured properly, MySQL ...');
查询database数据
select * from articles where body like '%database%' ;
虽然可以查询到对应的数据,但这种方式并没有使用全文索引,而是线性遍历所有数据,寻找匹配的
使用explain分析sql语句
语法:explain sql语句
正确使用全文索引的方式
SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database');
再使用explain分析该语句,我们发现这条查询使用到的索引是title
explain详解可参考这篇博客 explain详解
方法一:
show keys from 表名;
方法二:
show index from 表名;
方法三:
desc 表名; -- 信息比较简略
方法一:-删除主键索引
alter table 表名 drop primary key;
方法二:其他索引的删除
alter table 表名 drop index 索引名; --索引名就是show keys from 表名中的Key_name 字段
方法三:
drop index 索引名 on 表名;
注: 我们会发现创建索引时都是需要括号的,而我们删除索引是不需要带括号的;
比较频繁作为查询条件的字段应该创建索引
唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件
更新非常频繁的字段不适合作创建索引
不会出现在where子句中的字段不该创建索引
复合索引 --就是我们之前所说的复合主键
索引最左匹配原则
索引覆盖
具体可观看这篇博客 复合索引探究
s from 表名;
* 方法二:
```sql
show index from 表名;
方法三:
desc 表名; -- 信息比较简略
方法一:-删除主键索引
alter table 表名 drop primary key;
方法二:其他索引的删除
alter table 表名 drop index 索引名; --索引名就是show keys from 表名中的Key_name 字段
方法三:
drop index 索引名 on 表名;
注: 我们会发现创建索引时都是需要括号的,而我们删除索引是不需要带括号的;
比较频繁作为查询条件的字段应该创建索引
唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件
更新非常频繁的字段不适合作创建索引
不会出现在where子句中的字段不该创建索引
复合索引 --就是我们之前所说的复合主键
索引最左匹配原则
索引覆盖
具体可观看这篇博客 复合索引探究