Files
rikako-note/mysql/mysql文档/mysql_索引.md
2025-02-17 12:56:00 +08:00

365 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [innodb索引与算法](#innodb索引与算法)
- [索引算法](#索引算法)
- [二分查找](#二分查找)
- [B+树索引](#b树索引)
- [聚簇索引](#聚簇索引)
- [非数据页](#非数据页)
- [辅助索引](#辅助索引)
- [辅助索引查找](#辅助索引查找)
- [辅助索引查找分析](#辅助索引查找分析)
- [索引创建](#索引创建)
- [algorithm](#algorithm)
- [lock](#lock)
- [online DDL](#online-ddl)
- [Cardinality](#cardinality)
- [Cardinality](#cardinality-1)
- [innodb存储引擎的Cardinality](#innodb存储引擎的cardinality)
- [Cardinality采样](#cardinality采样)
- [索引使用](#索引使用)
- [联合索引](#联合索引)
- [覆盖索引](#覆盖索引)
- [覆盖索引对于统计场景的好处](#覆盖索引对于统计场景的好处)
- [优化器选择不使用索引](#优化器选择不使用索引)
- [辅助索引不能覆盖](#辅助索引不能覆盖)
- [索引提示](#索引提示)
- [Use index](#use-index)
- [Multi-Range Read](#multi-range-read)
- [multi-range read优势](#multi-range-read优势)
- [范围查询和join查询优化](#范围查询和join查询优化)
- [拆分键值对](#拆分键值对)
- [未开启MRR](#未开启mrr)
- [开启MRR优化](#开启mrr优化)
- [mrr控制](#mrr控制)
- [read\_rnd\_buffer\_size](#read_rnd_buffer_size)
# innodb索引与算法
innodb存储引擎支持如下集中常见的索引
- B+树索引
- 对于B+树索引,其只能找到被查找数据所在的页,然后数据库把页读入到内存中,再在内存中进行查找
- 全文索引
- 哈希索引
- innodb中哈希索引是自适应的innodb存储引擎会根据目前表的使用情况自动生成哈希索引
## 索引算法
### 二分查找
如前所示mysql数据页中存在page directory而page directory中slots是按照主键顺序来进行排放的。
`对于数据的查找是通过对page directory中的槽进行二分查找得到的`
## B+树索引
inoodb中索引大多是B+树的实现,`且具有高扇出的特性`。故而,在数据库中,`B+树的高度一般都在2~4层即在查找某一行记录时最多只需要2~4次IO。`
innodb中B+树索引可以分为聚簇索引和辅助索引其都是基于B+树实现,`所有的数据都存放在叶子节点中`
> 聚簇索引和辅助索引的区别在于叶子节点中是否存放的是整行的记录信息。
### 聚簇索引
在innodb中表是由索引组织的即表中的数据按照主键的顺序存放。
聚簇索引是按照每张表的主键构建一颗B+树,而叶子节点存放的则是整张表的行记录数据,`故而,聚簇索引的叶子节点也被称为数据页`
> 和B+树一样,每个数据页(叶子节点)都通过双向链表来进行链接。
> `每张表只能拥有一个聚簇索引`,并且,在多数情况下,查询优化器倾向于采用聚簇索引。`因为聚簇索引能够直接在叶子节点获取到行记录。`
#### 非数据页
在非数据页中存放的是key以及指向数据页的页号。
聚簇索引的存储并非是物理上连续的,`而是通过双向链表来维护逻辑上的顺序`
> 数据页与数据页之间通过双向链表来维护顺序排序,而位于同一个页中的记录之间也是通过双向链表来维护排序的。
对于聚簇索引,其`按照主键排序进行查找``按主键进行范围查找`时,查询速度很快。
### 辅助索引
对于辅助索引叶子节点并不包含行记录的全部数据。在辅助索引的叶子节点中叶子节点除了包含辅助索引的key外`每个叶子节点中的索引行还包含一个书签bookmark`。bookmark可以告知inodb存储引擎哪里能找到和索引相对应的行数据。
> 实际上innodb存储引擎的辅助索引其bookmark就是相应行数据的聚簇索引key`。
辅助索引并不会影响行数据在聚簇索引上的组织,故而每张表可以含有一个聚簇索引和多个辅助索引。
#### 辅助索引查找
当通过辅助索引来查找数据时innodb会遍历辅助索引的B+树数据结构,并从叶子节点中获取指向的主键索引值,之后再通过主键索引来查找到完整的行记录。
#### 辅助索引查找分析
如果聚簇索引和辅助索引的B+树高度同样为3那么当根据辅助索引来查找关联的主键时需要花费3次IO。此时再根据主键来从聚簇索引中获取对应行数据需要另外3次IO。
故而根据辅助索引来获取行记录大概需要6次IO。
### 索引创建
通过alter table语法可以选择索引的创建方式
```sql
alter table tbl_name
| add {index|key} [index_name]
[index_type] (index_col_name, ...)
algorithm [=] {default|inplace|copy}
lock [=] {default|none|shared|exclusive}
```
#### algorithm
通过`algorithm`可以指定创建或删除索引的算法:
- copy通过创建临时表的方式定义新的表结构并将原表中的数据移动到新的表最后删除原表并将临时表重命名
- 该算法将会耗费大量事件,并且在执行时表不可被访问
- inplace索引创建和删除操作不采用临时表
- default: 按照`old_alter_table`参数来判断是通过inplace还是copy来创建或删除索引默认`old_alter_table`为off即是采用inplace算法
#### lock
lock部分代表索引创建或删除时对表的加锁情况
- none执行索引的创建或删除操作时对目标表不添加任何的锁在执行过程中事务仍然能够进行读写操作不会阻塞。该加锁情况能够获取最大程度的并发度
- share在执行过程中对表添加s锁`s锁会和x锁以及ix锁发生冲突但是能够兼容s锁和is锁`
- 故而在加锁方式为share的场景下执行索引的创建或删除时其他事务仍然能够执行读取操作但是写操作则是会被阻塞。
- 如果存储引擎不支持share模式会返回相应的错误信息
- exclusive在exclusive模式下执行索引的创建和删除操作时会对目标表添加一个x锁此时其他事务对该表的读和写操作都不会被执行
- default
- default模式下首先会判断当前操作能否使用none
- 如果不能则判断是否能够使用shared
- 如果仍然不能那么最后会判断能否使用exclusive
#### online DDL
innodb存储引擎实现online DDL的原理是在执行索引的创建或删除操作时`insert, update, delete`这些DML写操作的日志写入到一个缓存中等到索引创建完成后再将这些DML语句重做到表上日志内容类似于redo log
该缓存的大小可以通过`innodb_online_alter_log_max_size`来进行控制,默认大小为`128M`
如果再执行索引创建和更新的表比较大,并且在`online alter`执行过程中存在大量的写事务,那么当`innodb_online_alter_log_max_size`指定的空间不足以存放时,会抛出错误。
如果发生上述错误,可以考虑增加`inodb_online_alter_log_max_size`参数的值;也可以设置`alter table``lock`模式为`share`那么在执行过程中不会有写事务的发生不需要进行DML日志的记录。
### Cardinality
并不是所有的字段都适合创建B+树索引。例如对于性别字段、类型字段等它们的选择范围都很小这种字段不适合创建B+树索引。
> 性别字段中只有`M`和`F`两种可选值,故而执行`select * from student where sex = 'F'`这类sql可能会得到表中50%的数据这种情况下为性别字段添加B+树索引完全没有必要。
而对于`bigint`类型的id字段其选择范围很广且没有重复这类字段适合使用B+树索引。
#### Cardinality
通过`show index`语句可以查看索引是否拥有高选择性,`Cardinality`列代表索引中`不重复记录数量的预估值`
`Cardinality`是一个预估值,并不是准确的值,且`Cardinality / rows_in_table`的值应该尽可能的接近1。如果该值非常小那么用户应考虑该索引的创建是否有必要。
#### innodb存储引擎的Cardinality
在生产环境下索引的操作可能十分频繁如果每次操作都对cardinality进行统计那么将会带来巨大的开销。`故而数据库针对Cardinality的统计都是通过采样来进行的`
在innodb中针对cardinality统计信息的更新发生在两个操作中`insert, update`。innodb针对cardinality的更新策略为
- 表中1/16的数据已经发生变化
- 自上一次统计cardinality信息时起表中1/16的数据已经发生过变化此时需要重新统计cardinality
- stat_modified_counter >= 2000,000,000
- 如果针对表中某一行数据进行频繁的更新那么该场景不会适配第一种更新策略故而在innodb内部维护了一个计数器stat_modified_counter用于标识发生变化的次数当计数器的值大于`2000,000,000`也需要更新cardinality
#### Cardinality采样
默认情况下innodb会针对8个叶子节点进行采样采样过程如下
- 取得B+树索引中叶子节点的数量记为A
- 随机获取索引8个叶子节点统计每个页不同记录的个数记为P1, P2, ... , P8
- 根据采样信息给出Cardinality的预估值`(P1 + P2 + ... + P8) * A/8`
## 索引使用
### 联合索引
联合索引代表对表上的多个列进行索引。
示例如下:
```sql
create table t (
id bigint not null auto_increment,
a int not null,
b int not null,
primary key(id),
index idx_a_b (a, b)
);
```
对于上述联合索引`(a,b)`,各查询语句使用索引的情况如下:
- `where a = xxx and b = xxx`
- 该语句会使用联合索引
- `where a = xxx`
- 该语句同样可以使用联合索引
- `where b = xxx`:
- 该语句无法使用联合索引
### 覆盖索引
innodb存储引擎支持覆盖索引即通过辅助索引即可得到查询记录无需再次查询聚簇索引。
> 通常来说,辅助索引中叶子节点的记录大小要远小于聚簇索引中叶子节点的记录大小。故而,在范围统计的场景下(`select count(1)`),辅助索引叶子节点中,一个页包含更多的记录,表扫描时需要读取的页数页更少。
>
> 故而在部分场景下使用覆盖索引能够节省大量的io操作。
对于辅助索引而言,其叶子节点的记录中包含`(primary key1, primary key2, ..., key1, key2)`等信息,故而,如下语句都可以通过`一次辅助联合索引查询来完成`:
- `select key2 from table where key1 = xxx`
- 会命中辅助索引且辅助索引中包含key2的值故而无需再次查询聚簇索引
- `select primary key2,key2 from table where key1 = xxx`
- `select primary key1,key2 from table where key1 = xxx`
- `select primary key1, primary key2, key2 from key1 = xxx`
#### 覆盖索引对于统计场景的好处
例如对于表buy_log
```sql
create table buy_log (
userid int unsigned not null,
buy_date date,
index `idx_userid` (userid),
index `idx_userid_buy_date` (userid, buy_date)
);
```
预置如下数据:
```sql
insert into buy_log(userid, buy_date)
values (1, '2022-01-01'),
(2,'2022-03-04'),
(3, '2024-12-31'),
(4, '2024-07-11'),
(5, '2025-01-01');
```
执行`explain select count(*) from buy_log;`语句时由于where条件为空其并不会命中索引但是语句执行的结果如下所示
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | buy\_log | null | index | null | idx\_userid | 4 | null | 5 | 100 | Using index |
上述返回结果中,`possible_keys`列为null但是实际执行时优化器却选择了`idx_userid`索引Extra列值为`Using index`,代表优化器选择了覆盖索引。
> 由于覆盖索引中记录大小更小故而使用覆盖索引能够节省io提升性能。
并且,对于`explain select count(*) from buy_log where buy_date >= '2023-01-01' and buy_date <= '2024-01-01';`语句,其返回结果如下
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | buy\_log | null | index | idx\_userid\_buy\_date | idx\_userid\_buy\_date | 8 | null | 5 | 20 | Using where; Using index |
正常来说,`idx_userid_buy_date`索引组成为`(userid, buy_date)`,当根据`buy_date`来进行查找时,不应该命中索引。
但是,由于辅助索引中同一页包含记录更多,故而上述语句仍然使用了`idx_userid_buy_date`覆盖索引。
### 优化器选择不使用索引
在某些情况下当使用explain语句进行分析时会发现优化器没有选择索引去查找数据而是通过全盘扫描聚簇索引。这种情况通常发生在范围查找join连接等操作时。
#### 辅助索引不能覆盖
对于辅助索引不能进行覆盖的场景,优化器仅当`查询少量数据`时选择辅助索引。
优化器不选择辅助索引的原因是,`根据辅助索引再次查询聚簇索引时,会带来大量的随机读`。由于辅助索引和聚簇索引的排序不同在辅助索引中连续的数据很可能在聚簇索引中是分散的。如果使用辅助索引那么在根据主键查询聚簇索引时可能带来大量的随机读主键可能随机分布在各个数据页中这样会带来大量的io。
故而,`当辅助索引需要读取大量的记录,并且辅助索引不能覆盖时,优化器会倾向直接在聚簇索引中进行顺序查找。`顺序读通常要远快于随机读。
> 如果mysql server实例部署在固态硬盘上随机读取的速度很快同时确认使用辅助索引的性能更佳可以使用`force index`来强制使用某个索引,使用示例如下:
>
> ```sql
> select * from orderdetails force index (orderID) where orderid > 10000 and order id < 102000
> ```
### 索引提示
mysql支持索引提示如下两种场景可能会用到索引提示
- mysql错误的选择了某个索引导致sql语句运行较慢
- 某个sql可选择索引非常多此时优化器选择执行计划的耗时可能大于sql语句执行的耗时
mysql数据库中索引提示的语法如下
```sql
table_name [[as] alias] [index_hint_list]
-- 其中index_hint_list的含义如下
index_hint_list:
index_hint[, index_hint]...
-- index_hint的含义则如下
index_hint:
USE {INDEX|KEY}
[{FOR {JOIN | ORDER BY | GROUP BY}}] ([index_list])
| IGNORE {INDEX | KEY}
[{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list)
| FORCE {INDEX|KEY}
[{FOR {JOIN | ORDER BY | GROUP BY}}] (index_list)
-- index_list含义如下
index_list:
index_name, [, index_name]...
```
创建`t_demo`示例如下:
```sql
create table t_demo (
a int not null,
b int not null,
key idx_a (a),
key idx_b (b)
);
insert into t_demo(a,b)
values
(1,1),
(1,2),
(2, 3),
(2, 4),
(1,2);
```
### Use index
执行如下语句`explain select * from t_demo where a=1 and b = 2;`,其返回执行计划如下:
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | t\_demo | null | index\_merge | idx\_a,idx\_b | idx\_b,idx\_a | 4,4 | null | 1 | 83.33 | Using intersect\(idx\_b,idx\_a\); Using where; Using index |
易知优化器选择了使用`idx_a``idx_b`两个索引来完成该查询且Extra列包含了`Using intersect(b,a)`,代表查询结果根据两个索引得到的结果进行求交集的数学运算。
可以通过`Use Index`来提示使用`idx_a`索引:
```sql
explain select * from t_demo use index (idx_a) where a=1 and b = 2;
```
得到结果如下:
| id | select\_type | table | partitions | type | possible\_keys | key | key\_len | ref | rows | filtered | Extra |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | SIMPLE | t\_demo | null | ref | idx\_a | idx\_a | 4 | const | 3 | 20 | Using where |
上述结果显示执行方案选用了`idx_a`
> 在使用`use index`语法之后,仍然有可能出现优化器没有使用该索引的情况,此时可以使用`force index`语法,来强制指定使用的索引。
## Multi-Range Read
mysql支持Multi-Range Read优化用于减少随机的磁盘访问将随机访问转化为较为顺序的数据访问。对于io-bound的sql查询其能带来性能的较大提升。
mutli-range read优化适用于`range, ref, eq_ref`类型的查询。
### multi-range read优势
- MRR能够令数据访问变得较为顺序在查询辅助索引时对于得到的查询结果按照主键进行排序并按照主键排序的顺序进行书签查找
- 减少缓冲池中页被替换的次数
- 批量处理对于key的查询操作
### 范围查询和join查询优化
对于innodb的范围查询和join查询MRR工作方式如下
- 将查询到的辅助索引键值存放在缓冲中,此时缓存中的数据按照辅助索引排序
- 将缓存中的键值按照rowId进行排序
- 根据rowId的排序来实际访问数据文件
> 当innodb的缓冲池不足以存放表中所有数据时频繁的离散读写会导致缓冲池中的页被换出又被不断读入。
>
> `如果按照主键顺序进行访问,可以将该类行为降至最低,页面不会被换出缓冲区后又被读入`。
### 拆分键值对
除了上述优化外对某些范围查询mrr还能够进行拆分优化这样在拆分过程中直接能过滤掉一些不符合查询条件的数据。
例如:
```sql
select * from t where key_part1 >= 1000 and key_part1 < 2000 and key_part2 = 1000;
```
表t存在`(key_part1, key_part2)`的联合索引。
#### 未开启MRR
如果未开启mrr那么此时查询类型为`RANGE`mysql优化器会将key_part位于`[1000, 2000)`范围内的全部数据都取出即是key_part2不为1000。在去除后再按key_part2条件对取出数据进行过滤。
> 这将会导致有大量无用数据被取出。如果存在大量数据且key_part2不为1000那么使用mrr将会有大量性能提升。
#### 开启MRR优化
在使用了MRR优化后优化器会先对查询条件进行拆分`key_part1 >= 1000 and key_part1 < 2000 and key_part2 = 1000`条件拆分为`(1000, 1000), (1001, 1000), ..., (1999, 1000)`,在根据拆分后的条件进行查询。
#### mrr控制
是否开启mrr可以通过参数`optimizer_switch`中的flag来控制当mrr为`on`代表启用mrr优化。
`mrr_cost_based`标记代表`是否通过cost based的方式来选择是否启用mrr`
如果将`mrr`设置为`on``mrr_cost_based`设置为`off`代表总是启用mrr优化。
#### read_rnd_buffer_size
`read_rnd_buffer_size`用来控制键值的缓冲区大小当大于该值时则执行器则对已经缓存的数据根据rowId来进行排序并通过rowId来获取数据。该值默认为`256K`.