@ -1309,7 +1309,29 @@ SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
**索引意义**: 索引能极大的减少存储引擎需要扫描的数据量, 索引可以把随机IO变成顺序IO。索引可以帮助我们在进行分组、排序等操作时, 避免使用临时表。正确的创建合适的索引是提升数据库查询性能的基础。
## 索引结构
**优势**
- ** 大大减少服务器需要扫描的数据量**
- ** 可以提高数据检索的效率( 将随机I/O变成顺序I/O) , 降低数据库的I/O成本**
- ** 通过索引列对数据进行排序, 降低数据排序的成本, 降低了CPU的消耗**
**劣势**
- ** 索引会占据磁盘空间**
- ** 索引虽然会提高查询效率,但是会降低更新表的效率**。比如每次对表进行增删改操作, MySQL不仅要保存数据, 还有保存或者更新对应的索引文件
## 数据结构
索引的数据结构:
- Hash表
-
### Hash索引
@ -1338,28 +1360,38 @@ SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
### R-Tree索引
空间数据索引。
### B-Tree索引
**背景**: 二叉查找树查询的时间复杂度是O(logN), 查找速度最快和比较次数较少。但用于数据库索引, 当数据量过大, 不可能将所有索引加载进内存, 使用二叉树会导致磁盘IO过于频繁, 最坏的情况下磁盘IO的次数由树的高度来决定。
B-Tree(平衡多路查找树)对二叉树进行了横向扩展,能很好解决红黑树中遗留的高度问题,使树结构更加**矮胖**, 使得一次IO能加载更多关键字, 对比在内存中完成, 减少了磁盘IO次数, 更适用于大型数据库, 但是为了保持自平衡, 插入或者删除元素都会导致节点发生裂变反应, 有时候会非常麻烦。
![索引结构-B-Tree ](images/Database/索引结构-B-Tree.png )
![索引-B树结构 ](images/Database/索引-B树结构.png )
**案例分析**: 模拟下查找key为29的data的过程
**案例分析**: 模拟下查找key为10的data的过程
![B-Tree案例分析 ](images/Database/B-Tree案例分析.png )
- 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作第**1次**】
- 磁盘块1存储17, 35和三个指针数据。我们发现17< 29 < 3 5, 因 此 我 们 找 到 指 针 p 2
- 根据p2指针, 我们定位并读取磁盘块3 。【磁盘IO操作**2次**】
- 磁盘块3存储26, 30和三个指针数据。我们发现26< 29 < 30 , 因 此 我 们 找 到 指 针 p2
- 根据p2指针, 我们定位并读取磁盘块8 。【磁盘IO操作**3次**】
- 磁盘块8中存储28, 29。我们找到29, 获取29 所对应的数据data
- 磁盘块1存储15, 45和三个指针数据。我们发现10< 1 5, 因 此 我 们 找 到 指 针 p 1
- 根据p1指针, 我们定位并读取磁盘块2 。【磁盘IO操作**2次**】
- 磁盘块2存储7, 12和三个指针数据。我们发现7< 10 < 12 , 因 此 我 们 找 到 指 针 p2
- 根据p2指针, 我们定位并读取磁盘块6 。【磁盘IO操作**3次**】
- 磁盘块6中存储8, 10。我们找到10, 获取10 所对应的数据data
**存在问题**
- ** 对范围查找没有更简单的方法 **。可以用B+Tree解决
- ** 不支持范围查询的快速查找 **。可以用B+Tree解决
- ** 每行数据量很大时, 会导致B-Tree深度较大, 进而影响查询效率**。可以用B+Tree解决
@ -1374,18 +1406,31 @@ B+树是B-树的变体, 也是一种多路搜索树。其定义基本与B-树
- 为所有叶子结点增加一个链指针
- 所有关键字都在叶子结点出现
![索引结构-B+Tree ](images/Database/索引结构-B+Tree.png )
![索引-B+Tree结构 ](images/Database/索引-B+Tree结构.png )
**剖析**: 如上图, 在叶子节点上注意是MySQL已经有成双向箭头( 原生B+Tree是单向的) , 而且从左到右是递增顺序的, 所以很好的解决了 > 和 < 这类查找问题。
**案例分析:等值查询**
B+的搜索与B-树也基本相同, 区别是B+树只有达到叶子结点才命中( B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找 。
假如我们查询值等于9的数据。查询路径磁盘块1->磁盘块2->磁盘块6 。
B+的特性:
![B+树索引-等值查询 ](images/Database/B+树索引-等值查询.png )
- 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的
- 不可能在非叶子结点命中
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
- 第一次磁盘IO: 将磁盘块1加载到内存中, 在内存中从头遍历比较, 9< 15 , 走 左 路 , 到 磁 盘 寻 址 磁 盘 块 2
- 第二次磁盘IO: 将磁盘块2加载到内存中, 在内存中从头遍历比较, 7< 9 < 12 , 到 磁 盘 中 寻 址 定 位 到 磁 盘 块 6
- 第三次磁盘IO: 将磁盘块6加载到内存中, 在内存中从头遍历比较, 在第三个索引中找到9, 取出data, 如果data存储的行记录, 取出data, 查询结束。如果存储的是磁盘地址, 还需要根据磁盘地址到磁盘中取出数据, 查询终止。( 这里需要区分的是在InnoDB中Data存储的为行数据, 而MyIsam中存储的是磁盘地址。)
**案例分析:范围查询**
假如我们想要查找9和26之间的数据。查找路径是磁盘块1->磁盘块2->磁盘块6->磁盘块7。
![案例分析-B+树-范围查询 ](images/Database/案例分析-B+树-范围查询.png )
- 首先查找值等于9的数据, 将值等于9的数据缓存到结果集。这一步和前面等值查询流程一样, 发生了三次磁盘IO
- 查找到15之后, 底层的叶子节点是一个有序列表, 我们从磁盘块6, 键值9开始向后遍历筛选所有符合筛选条件的数据
- 第四次磁盘IO: 根据磁盘6后继指针到磁盘中寻址定位到磁盘块7, 将磁盘7加载到内存中, 在内存中从头遍历比较, 9< 25 < 26 , 9 < 26 < = 26 , 将 data 缓 存 到 结 果 集 。
- 主键具备唯一性(后面不会有< =26的数据) , 不需再向后查找, 查询终止。将结果集返回给用户
@ -1426,7 +1471,7 @@ B*树定义了非叶子结点关键字个数至少为(2/3)M, 即块的最低使
### 普通索引
**普通索引(单列索引)**: 单列索引是最基本的索引,它没有任何限制。
**单列索引是最基本的索引,它没有任何限制,允许在定义索引的列中插入重复值和空值 。**
```sql
-- 直接创建索引
@ -1454,7 +1499,7 @@ alter table `表名` drop index 索引名;
### 唯一索引
唯一索引和普通索引类似,主要的区别在于,**唯一索引限制列的值必须唯一,但允许存在空值(只允许存在一条空值)**。如果在已经有数据的表上添加唯一性索引的话:
**索引列中的值必须是唯一的,但是允许为空值(只允许存在一条空值)。**
```mysql
-- 创建单个索引
@ -1473,7 +1518,7 @@ ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...);
### 主键索引
主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:
**索引列中的值必须是唯一的,不允许有空值。**
```sql
-- 主键索引(创建表时添加)
@ -1498,7 +1543,7 @@ alter table `order` add primary key(`orderId`);
### 组合索引
**复合索引(组合索引)**:复合索引是在多个字段上创建的索引。复合索引遵守“**最左前缀**”原则**, ** 即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。
**在多个字段上创建的索引**。组合索引遵守“**最左前缀**”原则, 即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。
```sql
-- 创建一个复合索引
@ -1512,9 +1557,7 @@ alter table table_name add index index_name(col_name,col_name2,...);
### 全文索引
在一般情况下,模糊查询都是通过 like 的方式进行查询。但是,对于海量数据,这并不是一个好办法,在 like "value%" 可以使用索引,但是对于 like "%value%" 这样的方式,执行全表查询,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情,所以 like 进行模糊匹配性能很差。
这种情况下,需要考虑使用全文搜索的方式进行优化。全文搜索在 MySQL 中是一个 FULLTEXT 类型索引。**FULLTEXT 索引在 MySQL 5.6 版本之后支持 InnoDB, 而之前的版本只支持 MyISAM 表**。目前只有char、varchar, text 列上可以创建全文索引。
**只能在文本类型 `CHAR` 、`VARCHAR`、`TEXT` 类型字段上创建全文索引**。字段长度比较大时, 如果创建普通索引, 在进行like模糊查询时效率比较低, 这时可以创建全文索引。 MyISAM和InnoDB中都可以使用全文索引。
```mysql
-- 创建表的适合添加全文索引
@ -1533,6 +1576,165 @@ ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(col_name);
## 索引实现
### MyIsam索引
MyISAM的数据文件和索引文件是分开存储的。MyISAM使用B+树构建索引树时,叶子节点中存储的键值为索引列的值,数据为索引所在行的磁盘地址。
#### 主键索引
![在这里插入图片描述 ](images/Database/20201024114325883.png )
表user的索引存储在索引文件`user.MYI`中,数据文件存储在数据文件 `user.MYD` 中。简单分析下查询时的磁盘IO情况:
**场景一:根据主键等值查询数据**
![在这里插入图片描述 ](images/Database/20201024114404727.png )
- 先在主键树中从根节点开始检索, 将根节点加载到内存, 比较28< 75 , 走 左 路 。 ( 1 次 磁 盘 IO )
- 将左子树节点加载到内存中, 比较16< 28 < 47 , 向 下 检 索 。 ( 1 次 磁 盘 IO )
- 检索到叶节点, 将节点加载到内存中遍历, 比较16< 28 , 18 < 28 , 28 = 28。查找到值等于30的索引项。( 1次磁盘IO)
- 从索引项中获取磁盘地址, 然后到数据文件user.MYD中获取对应整行记录。( 1次磁盘IO)
- 将记录返给客户端。
**磁盘IO次数: 3次索引检索+记录数据检索。**
**场景二:根据主键范围查询数据**
![在这里插入图片描述 ](images/Database/20201024114510253.png )
- 先在主键树中从根节点开始检索, 将根节点加载到内存, 比较28< 75 , 走 左 路 。 ( 1 次 磁 盘 IO )
- 将左子树节点加载到内存中, 比较16< 28 < 47 , 向 下 检 索 。 ( 1 次 磁 盘 IO )
- 检索到叶节点, 将节点加载到内存中遍历比较16< 28 , 18 < 28 , 28 = 28<47。查找到值等于28的索引项。
- 根据磁盘地址从数据文件中获取行记录缓存到结果集中。( 1次磁盘IO)
- 我们的查询语句时范围查找,需要向后遍历底层叶子链表,直至到达最后一个不满足筛选条件。
- 向后遍历底层叶子链表, 将下一个节点加载到内存中, 遍历比较, 28< 47 = 47 , 根 据 磁 盘 地 址 从 数 据 文 件 中 获 取 行 记 录 缓 存 到 结 果 集 中 。 ( 1 次 磁 盘 IO )
- 最后得到两条符合筛选条件,将查询结果集返给客户端。
**磁盘IO次数: 4次索引检索+记录数据检索。**
**备注:**以上分析仅供参考, MyISAM在查询时, 会将索引节点缓存在MySQL缓存中, 而数据缓存依赖于操作系统自身的缓存, 所以并不是每次都是走的磁盘, 这里只是为了分析索引的使用过程。
#### 辅助索引
在 MyISAM 中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而辅助索引的键值可以重复。查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树中检索数据。
### InnoDB索引
#### 主键索引
每个InnoDB表都有一个主键索引(聚簇索引) , 聚簇索引使用B+树构建, 叶子节点存储的数据是整行记录。一般情况下, 聚簇索引等同于主键索引, 当一个表没有创建主键索引时, InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下:
- 在表上定义主键PRIMARY KEY, InnoDB将主键索引用作聚簇索引
- 如果表没有定义主键, InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引
- 如果以上两个都没有, InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增
除聚簇索引之外的所有索引都称为辅助索引。在中InnoDB, 辅助索引中的叶子节点存储的数据是该行的主键值都。 在检索时, InnoDB使用此主键值在聚簇索引中搜索行记录。
主键索引的叶子节点会存储数据行,辅助索引只会存储主键值。
![在这里插入图片描述 ](images/Database/202010241146330.png )
**场景一:等值查询数据**
- 先在主键树中从根节点开始检索, 将根节点加载到内存, 比较28< 75 , 走 左 路 。 ( 1 次 磁 盘 IO )
- 将左子树节点加载到内存中, 比较16< 28 < 47 , 向 下 检 索 。 ( 1 次 磁 盘 IO )
- 检索到叶节点, 将节点加载到内存中遍历, 比较16< 28 , 18 < 28 , 28 = 28。查找到值等于28的索引项, 直接可以获取整行数据。将改记录返回给客户端。( 1次磁盘IO)
**磁盘IO数量: 3次。**
![在这里插入图片描述 ](images/Database/20201024114716460.png )
#### 辅助索引
除聚簇索引之外的所有索引都称为辅助索引, InnoDB的辅助索引只会存储主键值而非磁盘地址。以表user_innodb的age列为例, age索引的索引结果如下图:
![在这里插入图片描述 ](images/Database/20201024114750255.png )
底层叶子节点的按照( age, id) 的顺序排序, 先按照age列从小到大排序, age列相同时按照id列从小到大排序。使用辅助索引需要检索两遍索引: 首先检索辅助索引获得主键, 然后使用主键到主索引中检索获得记录。
**场景一:等值查询的情况**
```sql
select * from t_user_innodb where age=19;
```
![在这里插入图片描述 ](images/Database/2020102411481097.png )
根据在辅助索引树中获取的主键id, 到主键索引树检索数据的过程称为**回表**查询。
**磁盘IO数: 辅助索引3次+获取记录回表3次**
#### 组合索引
还是以自己创建的一个表为例:表 abc_innodb, id为主键索引, 创建了一个联合索引idx_abc(a,b,c)。
```sql
CREATE TABLE `abc_innodb`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` varchar(10) DEFAULT NULL,
`d` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_abc` (`a`, `b` , `c` )
) ENGINE = InnoDB;
```
组合索引的数据结构:
![在这里插入图片描述 ](images/Database/20201024114900213.png )
**组合索引的查询过程:**
```sql
select * from abc_innodb where a = 13 and b = 16 and c = 4;
```
![在这里插入图片描述 ](images/Database/20201024115012887.png )
**最左匹配原则**
最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。
在组合索引树中, 最底层的叶子节点按照第一列a列从左到右递增排列, 但是b列和c列是无序的, b列只有在a列值相等的情况下小范围内递增有序, 而c列只能在a, b两列相等的情况下小范围内递增有序。
就像上面的查询, B+树会先比较a列来确定下一步应该搜索的方向, 往左还是往右。如果a列相同再比较b列。但是如果查询条件没有a列, B+树就不知道第一步应该从哪个节点查起。
可以说创建的idx_abc(a,b,c)索引,相当于创建了(a)、( a,b) ( a,b,c) 三个索引。、
组合索引的最左前缀匹配原则: 使用组合索引查询时, mysql会一直向右匹配直至遇到范围查询(>、< 、between、like)就停止匹配。
#### 覆盖索引
覆盖索引并不是说是索引结构, 覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候, 我们只可以拿到主键值, 相当于获取数据还需要再根据主键查询主键索引再获取到数据。但是试想下这么一种情况, 在上面abc_innodb表中的组合索引查询时, 如果我只需要abc字段的, 那是不是意味着我们查询到组合索引的叶子节点就可以直接返回了, 而不需要回表。这种情况就是覆盖索引。可以看一下执行计划:
**覆盖索引的情况:**
![在这里插入图片描述 ](images/Database/20201024115203337.png )**未使用到覆盖索引:**
![在这里插入图片描述 ](images/Database/20201024115218222.png )
## 失效场景
**场景一: where语句中包含or时, 可能会导致索引失效**