# 【NO.486】后端开发【一大波干货知识】数据库之mysql索引原理详解

## 1.索引

索引分类：主键索引、唯一索引、普通索引、组合索引、以及全文索引（elasticsearch）；

### 1.1.主键索引

非空唯一索引，一个表只有一个主键索引；在innodb中，主键索引的B+树包含表格信息

```
PRIMARY KEY(key) // 括号中的key为列名
```

### 1.2.唯一索引

不可以出现相同的值，可以有NULL值（NULL值也不能重复）

```
UNIQUE(key) // 括号中的key为列名
```

### 1.3.普通索引

允许出现相同的索引内容

```
INDEX(key)
-- OR
KEY(key[,...])
```

### 1.4.组合索引

对表上的多个列进行索引

```
INDEX idx(key1,key2[,...]);
UNIQUE(key1,key2[,....]);
PRIMARY KEY(key1,key2[,....]);
```

### 1.5.全文索引

将存储在数据库当中的整本书和整篇文章中的任意内容信息查找出来的技术；

关键词 FULLTEXT;

在短字符串中用 LIKE % ；在全文索引中用 match 和 against ；

### 1.6.主键选择

innodb 中表是索引组织表，每张表有且仅有一个主键；

1.如果显示设置 PRIMARY KEY ，则该设置的key为该表的主键；

2.如果没有显示设置，则从非空唯一索引中选择；

\1. 只有一个非空唯一索引，则选择该索引为主键；

\2. 有多个非空唯一索引，则选择声明的第一个为主键；

没有非空唯一索引，则自动生成一个 6 字节的 _rowid 作为主键；

## 2.约束

为了实现数据的完整性，对于innodb，提供了以下几种约束，primary key，unique key，

foreign key， default, not null；

### 2.1.外键约束（不建议使用）

外键用来关联两个表，来保证参照完整性；MyISAM存储引擎本身并不支持外键，只起到注释作用；而innodb完整支持外键；

外键约束具有事务性。

```
create table parent ( 
    id int not null, 
    primary key(id) 
) engine=innodb; 
create table child ( 
    id int, 
    parent_id int, 
    foreign key(parent_id) references parent(id) ON DELETE CASCADE ON UPDATE CASCADE 
) engine=innodb; 
-- 被引用的表为父表，引用的表称为子表，上面例子中child为子表，parent为父表； 
-- 外键定义时，可以设置行为 ON DELETE 和 ON UPDATE，行为发生时的操作可选择： 
-- CASCADE 子表做同样的行为 ，如父表删除添加等，子表也会有相同变化
-- SET NULL 更新子表相应字段为 NULL ，如父表删除对应行，子表会设为null
-- NO ACTION 父类做相应行为报错 ，如父表做出改变，子表报错
-- RESTRICT 同 NO ACTION
```

### 2.2.约束与索引的区别

创建主键索引或者唯一索引的时候同时创建了相应的约束；但是约束时逻辑上的概念；索引是一个数据结构既包含逻辑的概念也包含物理的存储方式；

## 3.索引实现

### 3.1.B+树

索引是基于B+树实现的

B+树全称：多路平衡搜索树，减少磁盘访问次数；用来组织磁盘数据，以页为单位，物理磁盘页一般为4K，innodb 默认页大小为 16K；对页的访问是一次磁盘io，缓存中会缓存常访问的页；

特征：非叶子节点只存储索引信息，叶子节点存储具体数据信息；叶子节点之间互相连接，方便范围查询；

每个索引对应着一个 B+ 树；

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/15/20221215173557_81077.jpg)

为了支持范围查询：

所有的非父节点都会存储一个p，用于存储同一层中上一个节点与下一个节点的磁盘地址。比如查找索引值在1~22之间的数据，就可以通过页5->页6->页7，页2->页3的顺序来找数据，这样能够减少回溯，避免每一次都要有这样的过程（访问完叶节点 -> 返回父节点 -> 找下一个叶节点）

### 3.2.B+树层高

B+树的一个节点对应一个数据页；B+树的层越高，那么要读取到内存的数据页越多，io次数越多；

innodb一个节点16kB；

假设：

key为10byte且指针大小6byte，假设一行记录的大小为1kB；

那么一个非叶子节点可存下16kB/16byte=1024个（key+point）；每个叶子节点可存储1024行数据；

结论：

2层B+树叶子节点1024个，可容纳最大记录数为： 1024 * 16 = 16384；

3层B+树叶子节点1024 * 1024，可容纳最大记录数为：1024 * 1024 * 16 = 16777216；

4层B+数叶子节点1024 * 1024 * 1024，可容纳最大记录数为：1024 * 1024 * 1024 * 16 =

17179869184;

注意： 若数据超过500w行的时候，需要进行分表分库，不要将数据都放在一个表中

### 3.3.关于自增id

如果自增id超过了最大值会报错；

若担心自增值会过大，可以使用bigint类型，bigint的范围为(-263, 263-1)。

### 3.4.聚集索引

定义：按照主键构造的 B+ 树；叶子节点中存放数据页；数据也是索引的一部分；

innodb是这样子的

```
# table id name 
select * from user where id >= 18 and id < 40;
```

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/15/20221215173557_52476.jpg)

使用myisam创建数据库，会在mysql对应路径下生成*.frm, *.myd, *.myi文件。

*.frm：表信息文件

*.myd：数据文件（堆构建的）

*.myi：索引文件 （B+树构建的，叶子节点里面存储的是索引值+对应数据的磁盘地址）

通过索引文件找到对应数据的地址，再到数据文件中查找的过程称为回表查询

使用myisam创建数据库，会在mysql对应路径下生成*.frm和*.idb文件，其中*.idb存放的是索引和数据。

### 3.5. 辅助索引

定义：叶子节点不包含行记录的全部数据；辅助索引的叶子节点中，除了用来排序的 key 还包含一个bookmark ；该书签存储了聚集索引的 key；

辅助索引是按照非主键索引构造的。

```
-- 某个表 包含 id name lockyNum; id是主键，lockyNum存储辅助索引; 
select * from user where lockyNum = 33;
```

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/15/20221215173558_66946.jpg)

**解析：**

有两棵B+树。

一颗是根据id创建的聚集索引，叶子节点数据由多个主键索引+行数据组成

一颗是根据lockyNum创建的辅助索引，叶子节点数据由多个辅助索引+bookmark所组成。（bookmark对应一个主键）

根据lockyNum查找数据时，找到对应的辅助索引，再通过对应的bookmark去聚集索引中找具体的数据。从辅助索引跳到聚集索引也称为回表查询。

### 3.6.索引存储方式

innodb由段、区、页组成；段分为数据段、索引段、回滚段等；区大小为 1 MB（一个区由64个连续页构成）；页的默认值为16k；页为逻辑页，磁盘物理页大小一般为 4K 或者 8K；为了保证区中的页的连续，存储引擎一般一次从磁盘中申请 4~5 个区；

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/15/20221215173559_83765.jpg)

### 3.7.最左匹配原则

对于组合索引，从左到右依次匹配，遇到 > < between like 就停止匹配；

有where，group by，having，order by， join on的时候会尝试通过索引来查找

如k1，k2构成组合索引：

当单独使用k2时，会全表查询；

当同时使用k1，k2时并且没有 > < between like ，会用辅助索引查找；

### 3.8.覆盖索引

从辅助索引中就能找到数据，而不需通过聚集索引查找；利用辅助索引树高度一般低于聚集索引树；较少磁盘 io；

即避免回表查询，select的时候不要全表展示出来，可以只展示出辅助索引中的数据，这样就不需要回表查询，或者只用主键进行匹配。

### 3.9.索引失效

1、select … where A and B 若 A 和 B 中有一个不包含索引，则索引失效；

2、索引字段参与运算，则索引失效；例如： from_unixtime(idx) = ‘2021-04-30’;

3、索引字段发生隐式转换，则索引失效；例如： ‘1’ 隐式转换为 1 ；

4、LIKE 模糊查询，通配符 % 开头，则索引失效；例如： select * from user where name like ‘%Mark’;

5、在索引字段上使用 NOT <> != 索引失效；如果判断 id <> 0 则修改为 idx > 0 or idx < 0 ；

6、组合索引中，没使用第一列索引，索引失效；

7、in + or 索引失效；单独的in 是不会失效的；not in 肯定失效的；

### 3.10.索引原则

1、查询频次较高且数据量大的表建立索引；索引选择使用频次较高，过滤效果好的列或者组合；

2、使用短索引；节点包含的信息多，较少磁盘io操作；比如：smallint，tinyint；

3、对于很长的动态字符串，考虑使用前缀索引；

4、对于组合索引，考虑最左侧匹配原则和覆盖索引；

5、尽量选择区分度高的列作为索引；该列的值相同的越少越好；

6、尽量扩展索引，在现有索引的基础上，添加复合索引；最多6个索引

7、不要 select * ； 尽量只列出需要的列字段；方便使用覆盖索引；

8、索引列，列尽量设置为非空；

9、可选：开启自适应 hash 索引或者调整 change buffer；

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/15/20221215173559_23943.jpg)

## 4.innodb体系结构

### 4.1. Buffer Pool

Buffer pool 缓存表和索引数据；采用 LRU 算法（原理如下图）让 Buffer pool 只缓存比较热的数据 ；

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/15/20221215173600_16342.jpg)

### 4.2.Change buffer

Change buffer 缓存非唯一索引的数据变更（DML操作），Change buffer 中的数据将会异步merge 到磁盘当中；

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/15/20221215173601_69195.jpg)

原文链接：https://zhuanlan.zhihu.com/p/499640513

作者：[Hu先生的Linux](https://www.zhihu.com/people/huhu520-10)