索引:提高数据库的性能,索引是物美价廉的东西了,不用加内存,不用改程序,不用调sql,只要执行正确的 create index ,查询速度就可能提高成百上千倍,但是天下没有免费的午餐,查询速度的提高是以插入,更新,删除的速度为代价的
常见索引分为: 主键索引(primary key),唯一索引(unique),普通索引(index),全文索引(fulltext)–解决中子文索引问题
案例: 先整一个海量表(800万条数据),
查询员工编号为998866的员工看看没有索引时有什么问题:
可以看到耗时接近5秒,这还是在本机一个人来操作,在实际项目中,如果放在公网中,假如同时有1000个人并发查询,那很可能就死机
解决方法:创建索引,创建索引也是需要花费时间的 此时我们可以发现,创建索引之后再进行查找,效率变高了!
Mysql与存储
MySQL 给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中,磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率比较低,在加上IO本身的特征,可以知道,如何提交效率,是 MySQL 的一个重要话题
磁盘随机访问(Random Access)与连续访问(Sequential Access)
随机访问:本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据
连续访问:如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问
因此尽管相邻的两次IO操作在同一时刻发出,但如果它们的请求的扇区地址相差很大的话也只能称为随机访问,而非连续访问,磁盘是通过机械运动进行寻址的,随机访问不需要过多的定位,故效率比较高
所谓的IO效率低指的是哪方面效率低
操作系统读取磁盘,是以块为单位的,基本单位是 4KB ,MySQL 作为一款应用软件,可以想象成一种特殊的文件系统,它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB (innoDB 存储引擎),) 即:mysql和外设交互的基本单位是16KB 磁盘这个硬件设备的基本单位是 512 字节
1)MySQL 和 磁盘进行数据交互的基本单位是 16KB ,这个基本数据单元,在 MySQL 这里叫 做 page ( 注意和系统的page区分 ),只要发生了数据的交互,哪怕只有1bit,也是需要以16KB为基本单位对数据进行交互
2)MySQL 在服务器启动的时候,会预先加载一大块空间自己进行内存管理,这块空间被称为Buffer Pool(MySQL 5.7之中为128KB),mysql对数据做的CRUD操作,根据冯诺依曼体系,mysql不可能直接去访问磁盘, 实际上,数据库中数据做的所有的访问,全部都是再内存中进行的,然后定期的将数据刷新到磁盘当中
3)Page的大小是固定的,就是16KB, 数据量有限,如果不存储数据,就能够存储更多的索引信息,目录Page能够管理更多的Page,否则目录Page管理的页数太少,整颗树的层数就越多,更深,也就意味着从根节点到叶子节点的Page更多,即需要更多的IO
mysql对数据进行操作,实际上是先将磁盘的数据(16KB为单位)加载到mysql的大块内存当中, 对数据操作完成后,先刷新到内核缓冲区当中, 然后再刷新到磁盘当中
1)由冯诺依曼体系可知,IO的时间远远大于CPU运算的时间,如果每次要哪个数据就加载哪个数据,这样IO的次数就提高了,程序所需要的时间就会提高,效率也就降低了, 因此mysql提高效率的本质是减少IO
2)根据局部性原理,我们查找的数据有很大的概率会在同一个区间之内,即同一个page之内,因此我们进行IO的时候就加载一个page到内存之中,如果下次数据在这个区间之间,就不需要进行IO,提高了程序的效率
Mysql在运行的时候,一定有大量的page需要被替换,一个时间点内,MySQL内部一定存在着大量的Page,MySQL要把这个Page也管理起来? 先描述,在组织!!
struct Page
{
struct Page* next;
struct Page* prev;
char buffer[16kb-其它字段占用的大小]
};
MySQL中的数据往往都是封装成Page结构体的形式,保存在内存中,单个大小16KB,
对Page的管理变成了对链表的管理,当有数据加载进来,先new一个节点,然后把数据拷贝到buffer里,然后把它连接到链表里面, 当这个节点的内容要刷新:把里面的数据刷新到OS里面,然后把这个节点释放掉
例如: 但我们有主键的时候: 我们乱序插入,最后得到的也会是有序的, 如果没有主键,就是我们插入的顺序
create table if not exists user (
id int primary key, --一定要添加主键哦,只有这样才会默认生成主键索引
age int not null,
name varchar(16) not null
);
--插入多条记录,注意,我们并没有按照主键的大小顺序插入哦,因为有主键,所以得到的结果是有序的
mysql> insert into user (id, age, name) values(3, 18, '杨过');
mysql> insert into user (id, age, name) values(4, 16, '小龙女');
mysql> insert into user (id, age, name) values(2, 26, '黄蓉');
mysql> insert into user (id, age, name) values(5, 36, '郭靖');
mysql> insert into user (id, age, name) values(1, 56, '欧阳锋');
问:为什么有主键的时候,数据库在插入数据时要对其进行排序呢,我们按正常顺序插入数据不是也挺好的吗
插入数据时排序的目的,就是优化查询的效率,页内部存放数据的模块,实质上也是一个链表的结构,链表的特点也就是增删快,查询修改慢,所以优化查询的效率是必须的
正是因为有序,在查找的时候,从头到后都是有效查找,没有任何一个查找是浪费的,而且,如果运气好,是可以提前结束查找过程的
通过上面的分析,我们知道,上面页模式中,只有一个功能,就是**在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能,**但是,我们也可以看到,现在的页模式内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据,
如果有1千万条数据,一定需要多个Page来保存1千万条数据,多个Page彼此使用双链表链接起来,而且每个Page内部的数据也是基于链表的,那么,查找特定一条记录,也一定是线性查找,这效率也太低了
页目录
我们在看某一本书的时候,如果我们要看<某一章节>,找到该章节有两种做法
本质上,书中的目录,是多花了纸张的,但是却提高了效率 所以,目录,是一种“空间换时间的做法”
针对单页情况的页目录:
线性有序的数据可以类比一本书,书的页码从前到后一次排布,而书有目录,目录表明了所在章节的页码范围,在查找指定知识点的时候,可以先从目录找到所在章节,到指定章节再线性查找到指定位置,
针对单页Page我们也可以添加“目录”,如果我们想查找指定位置数据,先找目录到大体位置在线性查找即可,这是一种以空间换时间的做法,比如,我们要查找id=4记录,之前必须线性遍历4次,才能拿到结果,现在直接通过目录2[3],直接进行定位新的起始位置,提高了效率
从目录开始查找就能一次性排除更多数据,当然这一切也是建立在数据有序的基础上,
所以我们也能理解为何通过键值 MySQL 会自动排序? 可以很方便引入目录, 页内目录可以快速查询内部数据
Page之间也是以链表的形式组织的,只能线性查找的话效率太低,如何提高在多个Page中的查找效率呢?
page页相连
1),MySQL 中每一页的大小只有 16KB ,单个Page大小固定,所以随着数据量不断增大, 16KB 不可能存下所有的数据,那么必定会有多个页来存储数据,在单表数据不断被插入的情况下**,**
2)MySQL 会在容量不足的时候,自动开辟新的Page来保存新的数据,然后通过指针的方式,将所有的Page组织起来,
再次新插入的数据不一定会被放在最后一个Page的末尾,也有可能放在中间某个Page的中间位置,如果我们需要靠后的Page,但为了线性遍历,必须要把之前的所有Page导入内存,这样会造成大量IO,就算需要的Page已经在内存中,线性遍历多个Page的方式也是问题,
解决思路就是给多个Page也搭配上“目录”:把多个Page看作多个记录,把Page中的最小记录的键值作为Page的键值,为其标上目录,
提升查找page页的效率:
可在Page之间,也是需要 MySQL 遍历的,遍历意味着依旧需要进行大量的IO,将下一个Page加载到内存,进行线性检测,这样就显得我们之前的Page内部的目录,有点杯水车薪了,
解决方案,用之前的思路,给Page也带上目录,
存在一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的数据,有数据,就可通过比较,找到该访问那个Page,进而通过指针,找到下一个Page,
其实目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址,
概述总结:
由于MySQL和磁盘交互的基本单位是Page,这些Page叫做数据页,我们只需要将数据保存在每个数据页中即可,加载时直接加载一整个数据页,而数据页与数据页之间通过指针连成双向链表,这样就能够获取前一个或后一个数据页,而每条数据之间通过主键进行排序,在每个数据页中也有一个目录,这样在单个数据页中的查询速度就会加快
在单表数据不断被插入的情况下, MySQL 会在容量不足的时候,自动开辟新的数据页来保存新的数据,然后通过指针的方式,将所有的数据页组织起来,但是当数据页多起来的时候,如果加载每个数据页去遍历检测的话,时间会非常慢,这时可以给每个数据页建立起对应的目录,这就是索引,而索引也是通过Page保存的,我们称之为目录页,目录页只放各个下级Page的最小键值
小结
不保存任何用户数据,只保存它所管理的所有的page的最小记录ID为什么?
数据结构 | 劣势 |
---|---|
链表 | 链表是线性结构的, 查找的时候只能线性遍历,效率太低 |
二叉搜索树 | 在某些场景下可能退化成为线性结构 |
AVL和红黑树 | 和B+树相比,B+树的层数更低,每层进行一次IO,树越矮,IO的次数越少,AVL和红黑树相对来说,比B+树更高 |
Hash | 散列随机的算法特征决定了范围查找时效率不行,只能一个一个找 |
B树 | 底层无法线性遍历,范围查找效率不高。树体存储数据导致树过高 |
B+树
B树节点,既有数据,又有Page指针,而B+,只有叶子节点有数据,其他目录页,只有键值和Page指针
为什么选择B+树
B+树非叶子节点不存储数据,这样一个节点就可以存储更多的key,可以使得树更矮,所以IO操作次数更少,B+叶子节点,全部相连,而B没有,叶子节点相连,更便于进行范围查找
上面的组织形式是以主键为索引的组织形式,也就是先按照主键进行排序放到数据页中,再用目录页将数据页组织成B+树,如果没有设置主键该以谁为索引呢?
mysql当中的表是B+树组织的;根本原因在于,任何一张表,一个用户如果没有创建主键mysq会给这个表默认形成一个主键, 当我们没有设置主键的时候,InnoDB会优先选取一个唯一键作为索引,如果表中连唯一键也没有的话,就会自动为每一条记录添加一个叫做DB_ROW_ID的列作为默认主键,该列是一个6字节的自增数值,随着插入而自增,但是这个主键我们看不到,
换句话说,为什么主键必须是自增的,用非自增的(比如学号,身份证号)会怎么样?
由于数据页中的记录是按照主键从小到大进行串联的,自增ID决定了后来插入的记录一定会排列在上一条记录的后面,只需要简单添加next_record指针就可以了;如果当前数据页写满,那就放心地直接插入新的数据页中就可以了,
而非自增的主键则不同,它的大小顺序是不确定的,后来插入的记录有可能(而且概率相当大)插入到上一条记录之前(甚至是当前数据页之前),这就意味着需要遍历当前数据页的记录(或者先找到相关的数据页),然后找到自己的位置进行插入;如果当前数据页写满了,只能先找到适合自己位置的数据页,然后在数据页中遍历记录找到自己的合适位置进行插入, 因此使用非自增的主键插入记录花费的时间更长,
mysql本身就是一个文件系统, 在磁盘上有完整的B+树和数据,在内存中有局部高频被访问的B+树的核心Page
mysql的查找一定会伴生着mysql进行根据B+树进行page的换入和换出
InnoDB 和 MyISAM两种存储引擎索引的区别:
概念 | 存储引擎 | 解释 |
---|---|---|
聚簇索引 | InnoDB | 把索引和数放在一块 |
非聚簇索引 | MyISAM | 把索引和数据分离, 叶节点不存储数据,只存指针 |
MyISAM 最大的特点是,将索引Page和数据Page分离,也就是叶子节点没有数据,只有对应数据的地址,这种用户数据与索引数据分离的索引方案,叫做非聚簇索引,
InnoDB 的数据和索引是放在一起的,这种用户数据与索引数据在一起索引方案,叫做聚簇索引
它们之间有如下差别:
创建主键以后,MySQL会自动创建主键索引,
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); #把id设为主键
主键索引的特点:
创建唯一键以后,MySQL会自动创建唯一索引,
create table user4(id int primary key, name varchar(30) 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));
create index idx_name on user10(name);
此时查询索引可以发现:
普通索引的特点:
show keys from 表名 \G;
show index from 表名\G;
desc 表名;
例子:
mysql> show keys from t4\G;
*************************** 1. row ***************************(第一个主键)
Table: t4 <- 表名
Non_unique: 0 <- 如果索引不能包括重复值则为0,如果可以则为1,也就是平时所说的唯一索引
Key_name: PRIMARY <- 索引的名字
Seq_in_index: 1 <- 索引中的列序列号,从1开始
Column_name: id <- 索引是那个(索引的列名)
Collation: A <- 列以什么方式存储在索引中,大概意思就是字符序
Cardinality: 0 <- 基数的意思,表示索引中唯一值的数目的估计值
Sub_part: NULL <-前置索引的意思,如果列只是被部分地编入索引,则为被编入索引的字符的数目,如果整列被编入索引,则为NULL
Packed: NULL <-指示关键字如何被压缩,如果没有被压缩,则为NULL
Null: <-如果列含有NULL,则含有YES
Index_type: BTREE <- 以B+树的形式构建索引
Comment:
Index_comment: <- 注释的意思
mysql> desc t6;
+-------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| id | int(11) | YES | MUL | NULL | | (MUL)表示索引
| name | char(10) | YES | MUL | NULL | |
| grade | int(11) | YES | MUL | NULL | |
+-------+----------+------+-----+---------+-------+
alter table 表名 drop primary key;
alter table 表名 drop index 索引名;
比如:上述手动创建一个索引名为 idx_name 的普通索引
alter table user10 drop index idx_name;
第三种方法方法:drop index 索引名 on 表名;
drop index idx_name on user8;
对文章字段或有大量文字的字段进行检索时,会使用到全文索引
MySQL提供全文索引机制,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文,不支持中文,如果对中文进行全文检索,可以使用sphinx的中文版
例子:创建表并插入数据
查询有没有database数据
explain 命令获取 select 语句的执行计划, 通过 explain 我们可以知道以下信息:表的读取顺序,数据读取操作的类型,哪些索引可以使用,哪些索引实际使用了,表之间的引用,每张表有多少行被优化器查询等信息
如何使用全文索引
此时再次使用explain查看
注意: 要使用全文本搜索的时候要注意,你的表引擎用的是不是MyISAM
没有索引时,搜索数据:
将empno字段设置为主键,然后再次搜索数据:
如果不使用主键搜索,使用ename: 由于ename字段并不是索引,所以查找的会很慢
将ename设置为普通索引,此时按照ename的查找速度会大大加快:
此时将主键删除,然后再用ename进行查找:
我们可以发现:即使主键已经删除了,普通索引还是在的,并且主键删除的时间很长 主要是因为主键删除以后,DB_ROW_ID就会成为主键,那Page就会重新排列,并且普通索引叶子节点的主键值也会变为DB_ROW_ID,
索引覆盖
如果一个表中两个字段,比如id和name,两者为复合索引
索引最左匹配原则
索引下推
’%李’
的人的信息,把%加在name字段前面的时候,是无法利用索引的顺序性来进行快速比较的,也就是说这条查询语句中只有id字段可以使用索引进行快速比较和过滤,所以会筛选出所有id为10的主键,然后进行回表操作,如果id为10的信息过多,就会产生多次回表操作索引下推就是过滤的动作由下层的存储引擎层通过使用索引来完成,而减少不必要的回表操作