参考地址:www.bilibili.com/video/BV1iq…
为什么使用索引
假如给数据使用 二叉树 这样的数据结构进行存储,如下图所
索引及其优缺点
索引概述
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。
索引的本质: 索引是数据结构。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。这些数据结构以某种方式指向数据, 这样就可以在这些数据结构的基础上实现 高级查找算法 。
优点
(1)类似大学图书馆建书目索引,提高数据检索的效率,降低 数据库的IO成本 ,这也是创建索引最主要的原因。
(2)通过创建唯一索引,可以保证数据库表中每一行 数据的唯一性 。
(3)在实现数据的参考完整性方面,可以 加速表和表之间的连接 。换句话说,对于有依赖关系的子表和父表联合查询时,可以提高查询速度。
(4)在使用分组和排序子句进行数据查询时,可以显著 减少查询中分组和排序的时间 ,降低了CPU的消耗。(怎么理解呢?其实就是已经排好序了,例如1 1 2 2 3 4 4,分组时就近就已经排好序了,order by也不需要排序了 )
缺点
增加索引也有许多不利的方面,主要表现在如下几个方面:
(1)创建索引和维护索引要 耗费时间 ,并且随着数据量的增加,所耗费的时间也会增加。
(2)索引需要占 磁盘空间 ,除了数据表占数据空间之外,每一个索引还要占一定的物理空间, 存储在磁盘上 ,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。
(3)虽然索引大大提高了查询速度,同时却会 降低更新表的速度 。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
因此,选择使用索引时,需要综合考虑索引的优点和缺点。
InnoDB中索引的推演(简略看就行了,推理架构)
索引之前的查找
先来看一个精确匹配的例子:
SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;
在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能 从第一个页 沿着 双向链表 一直往下找,在每一个页中根据我们上面的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式显然是 超级耗时 的。如果一个表有一亿条记录呢?此时 索引 应运而生。
设计索引
建一个表:
mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT = Compact;
这个新建的 index_demo 表中有2个INT类型的列,1个CHAR(1)类型的列,而且我们规定了c1列为主键,这个表使用 Compact 行格式来实际存储记录的。这里我们简化了index_demo表的行格式示意图:
我们只在示意图里展示记录的这几个部分:
- record_type :记录头信息的一项属性,表示记录的类型, 0 表示普通记录、 2 表示最小记录、 3 表示最大记录、 1 暂时还没用过,下面讲。
mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT = Compact;
- next_record :记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,我们用箭头来表明下一条记录是谁。
- 各个列的值 :这里只记录在 index_demo 表中的三个列,分别是 c1 、 c2 和 c3 。
- 其他信息 :除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息
把一些记录放到页里的示意图就是:
1. 一个简单的索引设计方案
我们在根据某个搜索条件查找一些记录时为什么要遍历所有的数据页呢?因为各个页中的记录并没有规律,我们并不知道我们的搜索条件匹配哪些页中的记录,所以不得不依次遍历所有的数据页。所以如果
我们 想快速的定位到需要查找的记录在哪些数据页 中该咋办?我们可以为快速定位记录所在的数据页而 建立一个目录 ,建这个目录必须完成下边这些事:
- 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值。
- 给所有的页建立一个目录项。
所以我们为上边几个页做好的目录就像这样子:
目录项包含 当前页主键的最小值 和 页号(这一个图有一个弊端,就是目录项用数组去存的话,当目录页很多时,那么用连续的内存去存会不太好,每次插入数据后面的数据都得往后挪动,效率低,所以下面会改成链数组去存)
以 页28 为例,它对应 目录项2 ,这个目录项中包含着该页的页号 28 以及该页中用户记录的最小主
键值 5 。我们只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键
值快速查找某条记录的功能了。比如:查找主键值为 20 的记录,具体查找过程分两步:
- 先从目录项中根据 二分法 快速确定出主键值为 20 的记录在 目录项3 中(因为 12 < 20 <
209 ),它对应的页是 页9 。
- 再根据前边说的在页中查找记录的方式去 页9 中定位具体的记录。
至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为 索引 。
2. InnoDB中的索引方案
① 迭代1次:目录项纪录的页
我们把前边使用到的目录项放到数据页中的样子就是这样:
从图中可以看出来,我们新分配了一个编号为30的页来专门存储目录项记录。这里再次强调 目录项记录
和普通的 用户记录 的不同点:
- 目录项记录 的 record_type 值是1,而 普通用户记录 的 record_type 值是0。
- 目录项记录只有 主键值和页的编号 两个列,而普通的用户记录的列是用户自己定义的,可能包含 很
多列 ,另外还有InnoDB自己添加的隐藏列。
- 了解:记录头信息里还有一个叫 min_rec_mask 的属性,只有在存储 目录项记录 的页中的主键值
最小的 目录项记录 的 min_rec_mask 值为 1 ,其他别的记录的 min_rec_mask 值都是 0 。
相同点: 两者用的是一样的数据页,都会为主键值生成 Page Directory (页目录),从而在按照主键
值进行查找时可以使用 二分法 来加快查询速度。
现在以查找主键为 20 的记录为例,根据某个主键值去查找记录的步骤就可以大致拆分成下边两步:
- 先到存储 目录项记录 的页,也就是页30中通过 二分法 快速定位到对应目录项,因为 12 < 20 <
209 ,所以定位到对应的记录所在的页就是页9。
- 再到存储用户记录的页9中根据 二分法 快速定位到主键值为 20 的用户记录。
② 迭代2次:多个目录项纪录的页
从图中可以看出,我们插入了一条主键值为320的用户记录之后需要两个新的数据页:
- 为存储该用户记录而新生成了 页31 。
- 因为原先存储目录项记录的 页30的容量已满 (我们前边假设只能存储4条目录项记录),所以不得
不需要一个新的 页32 来存放 页31 对应的目录项。
现在因为存储目录项记录的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要3个步
骤,以查找主键值为 20 的记录为例:
- 确定 目录项记录页
我们现在的存储目录项记录的页有两个,即 页30 和 页32 ,又因为页30表示的目录项的主键值的
范围是 [1, 320) ,页32表示的目录项的主键值不小于 320 ,所以主键值为 20 的记录对应的目
录项记录在 页30 中。
- 通过目录项记录页 确定用户记录真实所在的页 。
在一个存储 目录项记录 的页中通过主键值定位一条目录项记录的方式说过了。
- 在真实存储用户记录的页中定位到具体的记录。
③ 迭代3次:目录项记录页的目录页(最好牢记)
如图,我们生成了一个存储更高级目录项的 页33 ,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在 [1, 320) 之间,则到页30中查找更详细的目录项记录,如果主键值 不小于320 的话,就到页32中查找更详细的目录项记录。
我们可以用下边这个图来描述它:
这个数据结构,它的名称是 B+树 。
④ B+Tree
一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放我们用户记录的那层为第 0 层,
之后依次往上加。之前我们做了一个非常极端的假设:存放用户记录的页 最多存放3条记录 ,存放目录项
记录的页 最多存放4条记录 。其实真实环境中一个页存放的记录数量是非常大的,假设所有存放用户记录
的叶子节点代表的数据页可以存放 100条用户记录 ,所有存放目录项记录的内节点代表的数据页可以存
放 1000条目录项记录 ,那么:
- 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放 100 条记录。
- 如果B+树有2层,最多能存放 1000×100=10,0000 条记录。
- 如果B+树有3层,最多能存放 1000×1000×100=1,0000,0000 条记录。
- 如果B+树有4层,最多能存放 1000×1000×1000×100=1000,0000,0000 条记录。相当多的记
录!!!
你的表里能存放 100000000000 条记录吗?所以一般情况下,我们 用到的B+树都不会超过4层 ,那我们
通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又
因为在每个页面内有所谓的 Page Directory (页目录),所以在页面内也可以通过 二分法 实现快速
定位记录。
常见索引概念
索引按照物理实现方式,索引可以分为 2 种:聚簇(聚集)和非聚簇(非聚集)索引。我们也把非聚集
索引称为二级索引或者辅助索引。
1. 聚簇索引
特点(结合着B+树的结构图来看):
- 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
-
- 页内 的记录是按照主键的大小顺序排成一个 单向链表 。
- 各个存放 用户记录的页 也是根据页中用户记录的主键大小顺序排成一个 双向链表 。
- 存放 目录项记录的页 分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键
大小顺序排成一个 双向链表 。
- B+树的 叶子节点 存储的是完整的用户记录。
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
注意:不是添加了索引才会有B+树,而是InnoDB的底层就是用B+树来存数据的
优点:
- 数据访问更快 ,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非
聚簇索引更快
- 聚簇索引对于主键的 排序查找 和 范围查找 速度非常快
- 按照聚簇索引排列顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多
个数据块中提取数据,所以 节省了大量的io操作 。
缺点(注意):
- 插入速度严重依赖于插入顺序 ,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影
响性能。因此,对于InnoDB表,我们一般都会定义一个 自增的ID列为主键
- 更新主键的代价很高 ,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为
不可更新
- 二级索引访问需要两次索引查找 ,第一次找到主键值,第二次根据主键值找到行数据
聚集索引只有一个,索引主键只能有一个
2. 二级索引(辅助索引、非聚簇索引)
回表的概念
概念:
回表 我们根据这个以c2列大小排序的B+树只能确定我们要查找记录的主键值,所以如果我们想根据c2列的值查找到完整的用户记录的话,仍然需要到 聚簇索引 中再查一遍,这个过程称为 回表 。也就是根据c2列的值查询一条完整的用户记录需要使用到 2 棵B+树!
问题:
为什么我们还需要一次 回表 操作呢?直接把完整的用户记录放到叶子节点不OK吗?
3. 联合索引(也属于非聚集索引)
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按
照 c2和c3列 的大小进行排序,这个包含两层含义:
- 先把各个记录和页按照c2列进行排序。
- 在记录的c2列相同的情况下,采用c3列进行排序
注意一点,以c2和c3列的大小为排序规则建立的B+树称为 联合索引 ,本质上也是一个二级索引。它的意
思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:
建立 联合索引 只会建立如上图一样的1棵B+树。
为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。
InnoDB的B+树索引的注意事项
1. 根页面位置万年不动
避免根节点变来变去,找不到对应的索引。
2. 内节点中目录项记录的唯一性
避免在目录项的索引值相同的情况下,不知道选择哪一页的尴尬情况。
3. 一个页面最少存储2条记录
MyISAM中的索引方案
MyISAM引擎使用 B+Tree 作为索引结构,叶子节点的data域存放的是 ****数据记录的地址 。
MyISAM索引的原理
下图是MyISAM索引的原理图。
如果我们在Col2上建立一个二级索引,则此索引的结构如下图所示(MyISAM的 二级索引 和 主键索引 是一样的):
MyISAM 与 InnoDB对比
MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的。小结两种引擎中索引的区
别:
① 在InnoDB存储引擎中,我们只需要根据主键值对 聚簇索引 进行一次查找就能找到对应的记录,而在MyISAM 中却需要进行一次 回表 操作,意味着MyISAM中建立的索引相当于全部都是 二级索引 。
② InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是 分离的 ,索引文件仅保存数据记录的地址。
③ InnoDB的非聚簇索引data域存储相应记录 主键的值 ,而MyISAM索引记录的是 地址 。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
④ MyISAM的回表操作是十分 快速 的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
⑤ InnoDB要求表 必须有主键 ( MyISAM可以没有 ) 。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。
索引的代价
索引是个好东西,可不能乱建,它在空间和时间上都会有消耗:
- 空间上的代价
每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。
- 时间上的代价
每次对表中的数据进行 增、删、改 操作时,都需要去修改各个B+树索引。 而且我们讲过,B+树每层节点都是按照索引列的值 从小到大的顺序排序 而组成了 双向链表 。不论是叶子节点中的记录,还是内节点中的记录(也就是不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些 记录移位 、 页面分裂 、 页面回收 等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作,会给性能拖后腿。
MySQL数据结构选择的合理性
重点关注:磁盘 I/O 的操作次数,就是从磁盘加载页到内存的次数。
Hash结构
上图中哈希函数h有可能将两个不同的关键字映射到相同的位置,这叫做 碰撞 ,在数据库中一般采用 链接法 (链接起来)来解决。在链接法中,将散列到同一槽位的元素放在一个链表中,如下图所示:
实验:体会数组和hash表的查找方面的效率区别
Hash结构效率高,那为什么索引结构要设计成树型呢?
Hash索引适用存储引擎如表所示:
Hash索引的适用性:
树形结构
二叉搜索树
如果我们利用二叉树作为索引结构,那么磁盘的IO次数和索引树的高度是相关的。
- 二叉搜索树的特点
- 查找规则
为了提高查询效率,就需要 减少磁盘IO数 。为了减少磁盘IO的次数,就需要尽量 降低树的高度 ,需要把
原来“瘦高”的树结构变的“矮胖”,树的每层的分叉越多越好。
AVL树(二叉平衡树)
树越矮胖越好
B-Tree
页码一是比17小的,页码二是17-35之间的,页码三是大于35的
B+Tree
B+ 树和 B 树的差异:
-
B+ 树有 k 个孩子的节点就有 k 个关键字。也就是孩子数量 = 关键字数,而 B 树中,孩子数量 = 关键字数+1。索引 / 存储引擎
-
B+ 树非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
-
B+ 树非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而 B 树中, ****非叶子节点既保存索引,也保存数据记录 。
-
B+ 树所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
思考题:
思考题一:
思考题二:
思考题:为了减少IO ,索引树会一次性加载吗?
思考题三:
思考题: B+树的存储能力如何?为何说一般查找行记录,最多只需1~3次磁盘IO
思考题四:
思考题: Hash 索引与 B+ 树索引的区别
思考题五:
思考题: Hash 索引与 B+ 树索引是在建索引的时候手动指定的吗?
小结
面试重点:
B树和B+树的区别?如, 看图识别两者,。。。
B+树和Hash索引的区别?
常见的时间复杂度,按数量级递增排列依次为:
常数阶O(1)、对数阶O(log2n)、线性阶O(n)、线性对数阶O(nlog2n)、平方阶O(n^2)、立方阶O(n^3)、k次方阶O(n^k)、指数阶O(2^n)。常见的算法时间复杂度由小到大依次为:
c < logN < n < n * LogN < n^2 < n^3 < 2^n < 3^n < n!
各时间复杂度曲线
InnoDB数据存储结构,这一章略过
121-127
数据库的存储结构:页
磁盘与内存的交互基本单位:页
页的架构图:之前的B+树的最底层,当时的数据竖着放的,现在的横着放的。
页结构的概述
链表插入和删除比较快,但是查找比较慢,使用使用目录去记录位置,就可以使用二分法快速定位对应的槽。
页的上层结构
索引的创建与设计原则
索引的声明与使用
索引的分类
MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。
- 从 功能逻辑 上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。
- 按照 物理实现方式 ,索引可以分为 2 种:聚簇索引和非聚簇索引。
- 按照 作用字段个数 进行划分,分成单列索引和联合索引。
- 普通索引
- 唯一性索引
- 主键索引
-
单列索引
-
多列(组合、联合)索引
-
全文索引(用ES)
创建索引
创建表的时候创建索引
隐式创建索引,通过约束去创建索引
CREATE TABLE dept
(
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(20)
);
CREATE TABLE emp
(
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20) UNIQUE,
dept_id INT,
CONSTRAINT emp_dept_id_fk FOREIGN KEY (dept_id) REFERENCES dept (dept_id)
);
如果显式创建表时创建索引的话,基本语法格式如下:
查看索引的命令:
- 创建普通索引
在book表中的year_publication字段上建立普通索引,SQL语句如下:
CREATE TABLE book
(
book_id INT,
book_name VARCHAR(100),
authors VARCHAR(100),
info VARCHAR(100),
comment VARCHAR(100),
year_publication YEAR,
INDEX (year_publication)
);
- 创建唯一索引 ( UNIQUE INDEX uk_idx_id )
声明有唯一索引的字段,在添加数据时,要保证唯一性,但是可以添加null,和唯一性约束一样。
CREATE TABLE test1
(
id INT NOT NULL,
name varchar(30) NOT NULL,
UNIQUE INDEX uk_idx_id (id)
);
- 主键索引 ( PRIMARY KEY )
和主键约束定义一样的方式,去定义主键索引
CREATE TABLE student
(
id INT(10) UNSIGNED AUTO_INCREMENT,
student_no VARCHAR(200),
student_name VARCHAR(200),
PRIMARY KEY (id)
);
- 创建组合索引
CREATE TABLE test3
(
id INT(11) NOT NULL,
name CHAR(30) NOT NULL,
age INT(11) NOT NULL,
info VARCHAR(255),
INDEX multi_idx (id, name, age)
);
- 创建全文索引
创建了一个给title和body字段添加全文索引的表。
CREATE TABLE `papers`
(
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`content` text,
PRIMARY KEY (`id`),
FULLTEXT KEY `title` (`title`, `content`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8;
不同于like方式的的查询:
SELECT * FROM papers WHERE content LIKE ‘%查询字符串%’;
全文索引用match+against方式查询:
SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’);
MySQL8.0索引新特性
支持降序索引
索引默认升序排列,所以经常使用降序排列的列推荐添加降序索引
CREATE TABLE ts1
(
a int,
b int,
index idx_a_b (a, b desc)
);
EXPLAIN SELECT * FROM ts1 ORDER BY a,b DESC LIMIT 5;
就是如果需要频繁用
升序索引使用降序操作
降序索引使用降序操作
隐藏索引 ( 软删除 )
在MySQL 5.7版本及之前,只能通过显式的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。
从MySQL 8.x开始支持 隐藏索引(invisible indexes) ,只需要将待删除的索引设置为隐藏索引,使
查询优化器不再使用这个索引(即使使用force index(强制使用索引),优化器也不会使用该索引),
确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。 这种通过先将索引设置为隐藏索
引,再删除索引的方式就是软删除 。
切换索引可见状态 已存在的索引可通过如下语句切换可见状态:
ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引
索引的设计原则
数据准备
第1步:创建数据库、创建表
CREATE DATABASE atguigudb1;
USE atguigudb1;
#1.创建学生表和课程表
CREATE TABLE `student_info`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`student_id` INT NOT NULL,
`name` VARCHAR(20) DEFAULT NULL,
`course_id` INT NOT NULL,
`class_id` INT(11) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
CREATE TABLE `course`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`course_id` INT NOT NULL,
`course_name` VARCHAR(40) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
第2步:创建模拟数据必需的存储函数
#函数1:创建随机产生字符串函数
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 ;
#函数2:创建随机数函数
DELIMITER //
CREATE FUNCTION rand_num(from_num INT, to_num INT) RETURNS INT(11)
BEGIN
DECLARE i INT DEFAULT 0; SET i = FLOOR(from_num + RAND() * (to_num - from_num + 1)); RETURN i;
END // DELIMITER ;
第3步:创建插入模拟数据的存储过程
# 存储过程1:创建插入课程表存储过程
DELIMITER //
CREATE PROCEDURE insert_course(max_num INT)
BEGIN
DECLARE i INT DEFAULT 0; SET autocommit = 0; #设置手动提交事务
REPEAT
#循环 SET i = i + 1; #赋值
INSERT INTO course (course_id, course_name) VALUES (rand_num(10000, 10100), rand_string(6));
UNTIL i = max_num
END REPEAT;
COMMIT; #提交事务
END // DELIMITER;
# 存储过程2:创建插入学生信息表存储过程
DELIMITER //
CREATE PROCEDURE insert_stu(max_num INT)
BEGIN
DECLARE i INT DEFAULT 0; SET autocommit = 0; #设置手动提交事务
REPEAT
#循环
SET i = i + 1; #赋值
INSERT INTO student_info (course_id, class_id, student_id, NAME)
VALUES (rand_num(10000, 10100), rand_num(10000, 10200), rand_num(1, 200000), rand_string(6));
UNTIL i = max_num END REPEAT;
COMMIT; #提交事务
END // DELIMITER ;
第4步:调用存储过程
CALL insert_course(100);
CALL insert_stu(1000000);
哪些情况适合创建索引
1. 字段的数值有唯一性的限制
2. 频繁作为 WHERE 查询条件的字段
某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在
数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。
比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。
3. 经常 GROUP BY 和 ORDER BY 的列
索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者
使用 ORDER BY 对数据进行排序的时候,就需要 对分组或者排序的字段进行索引 。如果待排序的列有多
个,那么可以在这些列上建立 组合索引 。
小结:
单独group by、order by,那么直接创建单独的索引就行了,如果两者都有的话考虑的是联合索引(否则只能进单个索引的),而且建立联合索引的顺序:group by 的列写在前面,order by 的列写在后面。(否则位置不一样就会走单个索引)。MySQL8.0 的 order by 可以指定降序排序索引。
4. UPDATE、DELETE 的 WHERE 条件列
对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就
能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或
删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更
新不需要对索引进行维护。
5.DISTINCT 字段需要创建索引
有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。
比如,我们想要查询课程表中不同的 student_id 都有哪些,如果我们没有对 student_id 创建索引,执行
SQL 语句:
SELECT DISTINCT(student_id) FROM `student_info`;
运行结果(600637 条记录,运行时间 0.683s ):
如果我们对 student_id 创建索引,再执行 SQL 语句:
SELECT DISTINCT(student_id) FROM `student_info`;
运行结果(600637 条记录,运行时间 0.010s ):
你能看到 SQL 查询效率有了提升,同时显示出来的 student_id 还是按照 递增的顺序 进行展示的。这是因
为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。
6. 多表 JOIN 连接操作时,创建索引注意事项
首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增
长会非常快,严重影响查询的效率。
其次, 对 WHERE 条件创建索引,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,
没有 WHERE 条件过滤是非常可怕的。
最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致 (否则会索引失效,因为使用了隐式转换的函数,用了函数那么索引就会失效)。比如 course_id 在
student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。
举个例子,如果我们只对 student_id 创建索引,执行 SQL 语句:
SELECT course_id, name, student_info.student_id, course_name
FROM student_info
JOIN course ON student_info.course_id = course.course_id
WHERE name = '462eed7ac6e791292a79';
运行结果(1 条数据,运行时间 0.189s ):
这里我们对 name 创建索引,再执行上面的 SQL 语句,运行时间为 0.002s 。
7. 使用列的类型小的创建索引
8. 使用字符串前缀创建索引
创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引
create table shop
(
address varchar(120) not null
);
alter table shop
add index (address(12));
问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字
段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?
先看一下字段在全部数据中的选择度:
select count(distinct address) / count(*) from shop;
通过不同长度去计算,与全表的选择性对比:
公式:
count(distinct left(列名, 索引长度))/count(*)
例如:
越接近 1 是越好的(区分度越高),否则就不太好。
select count(distinct left(address, 10)) / count(*) as sub10, -- 截取前10个字符的选择度
count(distinct left(address, 15)) / count(*) as sub11, -- 截取前15个字符的选择度
count(distinct left(address, 20)) / count(*) as sub12, -- 截取前20个字符的选择度
count(distinct left(address, 25)) / count(*) as sub13 -- 截取前25个字符的选择度
from shop;
引申另一个问题:索引列前缀对排序的影响
不对字符串列进行排序就好了。
拓展: Alibaba 《 Java开发手册》
【 强制 】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本
区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会 高达
90% 以上 ,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
9. 区分度高(散列性高)的列适合作为索引
10. 使用最频繁的列放到联合索引的左侧
这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。
最左前缀原则:
1、MySQL选择索引时,会先选择联合索引中 第一个 是该列的索引去使用。
2、在MySQL建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,(联合索引时)b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
11. 在多个字段都要创建索引的情况下,联合索引优于单值索引
1、例如前面的 GROUP BY 和 ORDER BY 的列,建立单个索引时,只会使用一个索引。
2、建立太多索引不好维护,影响 增、删、改 的效率。
限制索引的数目
哪些情况不适合创建索引
1. 在 where 和 GROUP BY 和 ORDER BY中使用不到的字段,不要设置索引
2. 数据量小的表最好不要使用索引
结论:在数据表中的数据行数比较少的情况下,比如 不到 1000 行,是不需要创建索引的。
3. 有大量重复数据的列上不要建立索引
结论:当数据重复度大,比如 高于 10% 的时候,也不需要对这个字段使用索引。
4. 避免对经常更新的表创建过多的索引
5. 不建议用无序的值作为索引
例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字
符串等。
6. 删除不再使用或者很少使用的索引
7. 不要定义冗余或重复的索引
① 冗余索引
② 重复索引
小结:
适合创建索引的情况:
-
字段的数值有唯一性的限制
-
频繁作为 WHERE 查询条件的字段
-
经常 GROUP BY 和 ORDER BY 的列
-
UPDATE 、 DELETE 的 WHERE 条件列
-
DISTINCT 字段需要创建索引
-
多表 JOIN 连接操作时,创建索引注意事项
-
使用列的类型小的创建索引
-
使用字符串前缀创建索引
-
区分度高(散列性高)的列适合作为索引
-
使用最频繁的列放到联合索引的左侧
-
在多个字段都要创建索引的情况下,联合索引优于单值索引
不适合创建索引的情况:
-
在where中使用不到的字段,不要设置索引
-
数据量小的表最好不要使用索引
-
有大量重复数据的列上不要建立索引
-
避免对经常更新的表创建过多的索引
-
不建议用无序的值作为索引
-
删除不再使用或者很少使用的索引
-
不要定义冗余或重复的索引