648 lines
37 KiB
Markdown
648 lines
37 KiB
Markdown
- [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)
|
||
- [Index Condition Pushdown(ICP)](#index-condition-pushdownicp)
|
||
- [关闭ICP](#关闭icp)
|
||
- [开启ICP](#开启icp)
|
||
- [innodb hash](#innodb-hash)
|
||
- [innodb中的哈希](#innodb中的哈希)
|
||
- [自适应hash索引](#自适应hash索引)
|
||
- [全文检索](#全文检索)
|
||
- [倒排索引](#倒排索引)
|
||
- [inverted file index](#inverted-file-index)
|
||
- [full inverted index](#full-inverted-index)
|
||
- [innodb全文检索](#innodb全文检索)
|
||
- [辅助表](#辅助表)
|
||
- [FTS Index Cache (全文检索索引缓存)](#fts-index-cache-全文检索索引缓存)
|
||
- [查看指定倒排索引的辅助表分词信息](#查看指定倒排索引的辅助表分词信息)
|
||
- [FTS Index Cache更新和写盘时机](#fts-index-cache更新和写盘时机)
|
||
- [分词删除](#分词删除)
|
||
- [OPTIMIZE TABLE](#optimize-table)
|
||
- [innodb\_ft\_cache\_size](#innodb_ft_cache_size)
|
||
- [FTS Document id](#fts-document-id)
|
||
- [innodb全文检索示例](#innodb全文检索示例)
|
||
- [innodb full-text design](#innodb-full-text-design)
|
||
- [innodb full-text index tables](#innodb-full-text-index-tables)
|
||
- [辅助索引表(auxiliary index table)](#辅助索引表auxiliary-index-table)
|
||
- [table\_id hex](#table_id-hex)
|
||
- [index\_id hex](#index_id-hex)
|
||
- [公共索引表(common index table)](#公共索引表common-index-table)
|
||
- [innodb full-text index cache](#innodb-full-text-index-cache)
|
||
- [innodb\_ft\_cache\_size](#innodb_ft_cache_size-1)
|
||
- [innodb\_ft\_total\_cache\_size](#innodb_ft_total_cache_size)
|
||
- [full-text查询](#full-text查询)
|
||
|
||
|
||
# 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`.
|
||
|
||
## Index Condition Pushdown(ICP)
|
||
可以通过`optimizer_switch`中的flag来控制`ICP`是否开启,如`index_condition_pushdown`为`on`,代表icp开启。
|
||
|
||
例如,表`people`包含`(zip_code, last_name, first_name)`索引,执行如下语句
|
||
```sql
|
||
select * from people where zipcode = '95054' and last_name like '%asahi%' and address like '%street%';
|
||
```
|
||
|
||
### 关闭ICP
|
||
如果ICP优化没有开启,那么数据库首先会根据索引查询`zipcode`为95054的记录,然后查询出后再根据查询出的结果过滤where的后两个条件`last_name like '%asahi%' and address like '%street%'`。
|
||
|
||
### 开启ICP
|
||
当开启ICP后,数据库会将where的部分过滤条件放在存储引擎层,在索引获取数据同时就会进行where条件的过滤。
|
||
|
||
## innodb hash
|
||
在innodb中,采用除法散列的哈希方法,通过`k % m`将关键字`k`映射到`m`个槽中的一个,即
|
||
```
|
||
hash(k) = k % m
|
||
```
|
||
### innodb中的哈希
|
||
innodb存储引擎采用哈希算法对字典进行查找,冲突机制采用链表方式,哈希函数采用`k % m`的方式进行散列。
|
||
|
||
对于缓冲池页的哈希表,缓冲池中的page页都有一个chain指针,指向相同哈希值的页。而对于`m`的取值,其规则如下:
|
||
- m的取值应略大于2倍的缓冲池页数量的质数
|
||
- 例如,若`innodb_buffer_pool_size`大小为10M,那么缓冲池页数量为 `10 * 1024 / 16 = 640`,可容纳240个缓冲页,故而,对于缓冲池页内存的哈希表来说,需要分配的`槽个数m`为大于`640 * 2`的质数,即`1399`。
|
||
|
||
### 自适应hash索引
|
||
自适应hash索引可以通过参数`innodb_adaptive_hash_index`来进行开启或关闭,并且,自适应hash索引的使用情况可以通过`show engine innodb status`来进行查看:
|
||
```
|
||
-------------------------------------
|
||
INSERT BUFFER AND ADAPTIVE HASH INDEX
|
||
-------------------------------------
|
||
Ibuf: size 1, free list len 0, seg size 2, 0 merges
|
||
merged operations:
|
||
insert 0, delete mark 0, delete 0
|
||
discarded operations:
|
||
insert 0, delete mark 0, delete 0
|
||
0.00 hash searches/s, 320660.58 non-hash searches/s
|
||
```
|
||
可以根据`hash searches/s`和`non-hash searches/s`来查看自适应hash索引的使用情况。
|
||
|
||
## 全文检索
|
||
### 倒排索引
|
||
全文检索通常使用`倒排索引`(inverted index)来实现,和B+树索引一致,倒排索引也是一种索引结构。
|
||
|
||
倒排索引在辅助表(auxiliary table)中存储了`单词`和`单词自身在一个或多个文档中所在位置`之间的映射关系。通常,倒排索引通过关联数组可以实现,拥有两种表现形式:
|
||
- inverted file index: {单词,单词所在文档id}
|
||
- full inverted index: {单词, (单词所在文档id, 在文档中的具体位置)}
|
||
|
||
#### inverted file index
|
||
对于inverted file index,其存储的数据结构如下所示:
|
||
| Number | Text | Documents |
|
||
| :-: | :-: | :-: |
|
||
| 1 | code | 1,4 |
|
||
| 2 | days | 3,6 |
|
||
| 3 | hot | 1,4 |
|
||
|
||
其中,`Documents`存储的是包含查询关键字的文档id数组。
|
||
|
||
> 对于inverted file index,其仅保存文档的id
|
||
|
||
#### full inverted index
|
||
对于full inverted index,其存储的是`(文档id,文档中位置)`的`pair`。
|
||
|
||
full inverted index的存储结构示例如下所示;
|
||
| Number | Text |Documents |
|
||
| :-: | :-: | :-: |
|
||
| 1 | code | (1:6), (4:8) |
|
||
| 2 | days | (3:2), (6:2) |
|
||
| 3 | hot | (1:3), (4:4) |
|
||
|
||
> full inverted index 相较于 inverted file index,除了存储文档id之外,还存储单词所在文档的位置信息。
|
||
|
||
### innodb全文检索
|
||
innodb支持full inverted index形式的全文检索。
|
||
|
||
在innodb中,将`(DocumentId, Position)`视为`ilist`。故而,在全文检索的表中,存在两个字段,`word`和`ilist`,并在`word`字段设有索引。`并且,innodb在ilist中存放了position信息,故而可以支持proximity search`。
|
||
|
||
#### 辅助表
|
||
innodb中,倒排索引需要将`word`存放在辅助表中,并且,为了提高全文检索的并行能力,共存在`6`张辅助表,每张表根据word的Latin进行分区。
|
||
|
||
> 辅助表为持久化的表,存放在磁盘中。
|
||
|
||
#### FTS Index Cache (全文检索索引缓存)
|
||
FTS Index Cache为红黑树结构,根据`(word, ilist)`进行排序。
|
||
|
||
在插入数据时,即使插入数据已经更新了对应的数据库表,但是对`全文索引`更新可能仍位于`FTS Index Cache`中,即辅助表尚未被更新。Innodb会批量对辅助表进行更新,而非每次插入后都立即更新索引表。
|
||
|
||
当对全文检索进行查询时,辅助表首先会将FTS Index Cache中对应word字段合并到辅助表中,然后再在辅助表中执行查询操作。
|
||
|
||
> 上述FTS Index Cache操作类似Insert Buffer,其能提高innodb的性能,并且其由红黑树排序后再执行批量插入,其产生的辅助表相对较小。
|
||
|
||
#### 查看指定倒排索引的辅助表分词信息
|
||
innodb允许用户查看指定倒排索引辅助表的分词信息,可以通过设置`innodb_ft_aux_table`来查看倒排索引的辅助表。
|
||
|
||
```sql
|
||
set global innodb_ft_aux_table='{schema_name}/{table_name}';
|
||
```
|
||
执行完上述语句后,可以在`information_schema.innodb_ft_index_table`中查询表的分词信息。
|
||
|
||
#### FTS Index Cache更新和写盘时机
|
||
Innodb在事务提交时将分词写入到`FTS Index Cache`中,然后再根据批量更新将`FTS Index Cache`写入到磁盘。
|
||
|
||
在事务提交时,FTS Index Cache中数据会同步到磁盘的辅助表中。但是,当数据库发生宕机时,FTS Index Search中的数据可能尚未被同步到磁盘中。
|
||
|
||
在上述宕机情况下,下次数据库重启时,若用户对表进行全文检索操作(插入或查询),innodb会自动读取未完成的文档,并然后进行分词操作,再次将分词结果放入到FTS Index Cache中。
|
||
|
||
#### 分词删除
|
||
对于文档中分词的删除操作,在事务提交时,不删除辅助表中的数据,而只是删除`FTS Index Cache`中的记录。对于被删除的记录,会根记录其FTS Dcoument Id,并且将该id保存在`deleted辅助表`中。
|
||
|
||
> 在删除文档时,文档内容从一般表中被删除,但是`索引辅助表`中的数据并不会被删除,相反的,还会将`FTS_DOC_ID`添加到`deleted辅助表`中。
|
||
|
||
#### OPTIMIZE TABLE
|
||
由上述内容可知,对文档的DML并不会实际删除`索引中的数据`,只是会在`deleted辅助表`中添加`FTS_DOC_ID`,故而在应用程序运行时,`索引会变得越来越大`。
|
||
|
||
mysql允许通过`optimize table`命令来实际将已删除记录从索引中删除。
|
||
|
||
由于`optimize table`语句不仅会删除索引中的数据,还会执行其他操作,例如`Cardinality`重新统计等。
|
||
|
||
> 如果用户仅希望对倒排索引进行操作,可以设置`innodb_optimize_fulltext_only`参数。
|
||
>
|
||
> 可以执行如下语句
|
||
> ```sql
|
||
> set global innodb_optimize_fulltext_only=1;
|
||
> optimize table xxx;
|
||
> ```
|
||
|
||
如果被删除文档很多,那么optimize table可能会花费大量时间,这将对程序的运行造成影响。
|
||
|
||
用户可以通过参数`innodb_ft_num_word_optimize`来限制每次实际删除的分词数量,该参数默认值为2000.
|
||
|
||
|
||
#### innodb_ft_cache_size
|
||
可以通过`innodb_ft_cache_size`来控制FTS Index Cache的大小,默认大小为`32M`。当缓冲被填满后,会将缓存中`(word, ilist)`数据同步到位于磁盘的辅助表中。
|
||
|
||
适当增加`innodb_ft_cache_size`参数的值能够提升全文检索的性能,但是在宕机时,位同步到磁盘中的数据可能需要更长时间来进行恢复。
|
||
|
||
#### FTS Document id
|
||
在innodb存储引擎中,为了支持全文检索,必须存在一个字段和辅助表中的`word`进行对应。在innodb中,对应字段为`FTS_DOC_ID`
|
||
|
||
`FST_DOC_ID`字段的字段类型必须为`BIGINT UNSIGNED NOT NULL`,并且innodb存储引擎会自动为该列添加名为`FST_DOC_ID_INDEX`的唯一索引`unique index`。
|
||
|
||
### innodb全文检索示例
|
||
full-text index基于`text based columns`(char, varchar, text)来进行创建,用于加速针对`text based colums`列的dml操作。
|
||
|
||
一个full-text index可以定义为`create table`语句的一部分,也可以通过`alter table`或`create index`语句添加到已经存在的表中。
|
||
|
||
full-text查询通过`match() ... against`预发来触发。
|
||
|
||
#### innodb full-text design
|
||
innodb全文索引基于倒排索引进行设计。倒排索引存储了一系列的words;对每个word,都存在一个list与之对应,list中的元素为`包含word的文档`。
|
||
|
||
为了支持`proximity search`,word在list中出现的位置信息也同样被保存。
|
||
|
||
#### innodb full-text index tables
|
||
当innodb full-text index被创建时,一系列index tables都会同时被创建,示例如下所示。
|
||
|
||
创建表,并指定fulltext索引:
|
||
```sql
|
||
create table fs_text (
|
||
id bigint not null auto_increment,
|
||
content longtext,
|
||
primary key (id),
|
||
fulltext index `idx_fts_fs_text` (content)
|
||
);
|
||
```
|
||
查询辅助表:
|
||
```sql
|
||
select * from information_schema.innodb_tables where name like 'innodb_demo%'
|
||
```
|
||
|
||
其中,和full-text表相关的索引如下:
|
||
|
||
| TABLE\_ID | NAME | FLAG | N\_COLS | SPACE | ROW\_FORMAT | ZIP\_PAGE\_SIZE | SPACE\_TYPE | INSTANT\_COLS | TOTAL\_ROW\_VERSIONS |
|
||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
||
| 1822 | innodb\_demo/fs\_text | 33 | 6 | 96 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1823 | innodb\_demo/fts\_000000000000071e\_being\_deleted | 33 | 4 | 97 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1824 | innodb\_demo/fts\_000000000000071e\_being\_deleted\_cache | 33 | 4 | 98 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1825 | innodb\_demo/fts\_000000000000071e\_config | 33 | 5 | 99 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1826 | innodb\_demo/fts\_000000000000071e\_deleted | 33 | 4 | 100 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1827 | innodb\_demo/fts\_000000000000071e\_deleted\_cache | 33 | 4 | 101 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1828 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_1 | 33 | 8 | 102 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1829 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_2 | 33 | 8 | 103 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1830 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_3 | 33 | 8 | 104 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1831 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_4 | 33 | 8 | 105 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1832 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_5 | 33 | 8 | 106 | Dynamic | 0 | Single | 0 | 0 |
|
||
| 1833 | innodb\_demo/fts\_000000000000071e\_00000000000005ad\_index\_6 | 33 | 8 | 107 | Dynamic | 0 | Single | 0 | 0 |
|
||
|
||
#### 辅助索引表(auxiliary index table)
|
||
其中,以`innodb_demo/fts_000000000000071e_00000000000005ad_index`开头的6张表,用于存储倒排索引,并被称为`辅助索引表`。
|
||
|
||
当新增的文档被分割为`token`时,每个独立的`word`(也可被称为`token`)被插入到辅助索引表中,随着word被插入的还有`postion`和`DOC_ID`。
|
||
|
||
word按照`第一个字符的字符集排序权重`被排序,并且在六张辅助索引表中进行分区。
|
||
|
||
> 倒排索引被分区在6张辅助索引表中,用于支持索引的并行创建。默认情况下,2个线程执行`tokenize`, `sort`,`将word和关联数据插入到index tables`操作。
|
||
>
|
||
> 如果想要指定操作上述流程的线程数量,可以对`innodb_ft_sort_pll_degree`参数进行配置。如果要在大表上创建full-text index,可以考虑增加该参数。
|
||
|
||
##### table_id hex
|
||
辅助索引表命名通过`fts_`开头,并且后缀`index_#`。每个辅助索引表都通过`辅助索引表表名中16进制的值`来和`被索引表的table id`来进行关联。例如,上述示例中,16进制值`071e`代表十进制`1822`,而表`fs_text`的`table_id`刚好为`1822`。
|
||
|
||
##### index_id hex
|
||
在6张辅助索引表中,除了包含`table_id`的hex外,下划线后还存在一个16进制数部分,该部分为`05ad`,代表十进制`1453`,而索引`idx_fts_fs_text`的`index_id`刚好为`1453`。
|
||
|
||
> 如果是`file per table tablespace`,那么idnex table将会保存在其自己的tablespace中。
|
||
|
||
#### 公共索引表(common index table)
|
||
除了辅助索引表之外,剩下的表被称为公共索引表,用于处理删除和存储full-text index的状态。
|
||
|
||
公共索引表和辅助索引表区别如下:
|
||
- 辅助索引表:辅助索引表用于存储倒排索引的内容,其6张表都是针对索引的,若一张表内拥有两个`fulltext`索引,那么每个fulltext索引都会有其自己的6张辅助索引表
|
||
- 公共索引表:公共索引表是针对表的,不管一张表中存在多少`fulltext`索引,都只有一张公共索引表
|
||
|
||
即使在删除fulltext索引后,common index table仍然会保留,当删除fulltext索引后,为该索引创建的`FTS_DOC_ID`字段也会被保留,因为删除FTS_DOC_ID列需要对先前被索引的表进行重构。
|
||
|
||
公共索引表用于管理`FTS_DOC_ID`列:
|
||
- `fts_*_deleted`和`fts_*_deleted_cache`:
|
||
- 该表用于保存`文档已被删除,但是数据仍然没有从full-text index中移除`的文档id(DOC_ID)。
|
||
- `fts_*_deleted_cache`是`fts_*_deleted`的内存保本
|
||
- `fts_*_being_deleted`和`fts_*_being_deleted_cache`:
|
||
- 该表用于保存`文档已被删除,并且文档数据正在从full-text index中被移除`的文档id(DOC_ID)
|
||
- `fts_*_deleted_cache`是`fts_*deleted`的内存版本
|
||
- `fts_*_config`:
|
||
- 存储full-text index的内部状态,
|
||
- 其会存储`FTS_SYNCED_DOC_ID`,用于标识`已经被转化并且刷新到磁盘中`的文档。当mysql应用发生崩溃并且重启恢复时,`FS_SYNCED_DOC_ID`会标识还没有被刷新到磁盘中的文档。故而所有未被刷新到磁盘的文档都会被重新reparsed,并且添加到full-text index cache中
|
||
|
||
#### innodb full-text index cache
|
||
当文档被插入时,其会被执行`tokenize`操作,之后得到的`word`和`word关联的数据`将会被插入到full-text index中。
|
||
- 在上述过程中,即使对于大小很小的文档,也会产生对`辅助索引表`的大量小插入,会并发访问辅助索引表,产生竞争
|
||
|
||
为了避免上述问题,innodb使用full-text index cache来对index table insertions进行缓存。该内存缓存结构将会缓存插入操作,直到cache被填满并将其批量刷新到磁盘(即刷新到辅助索引表)。
|
||
|
||
> 可以在`information_schema.innodb_ft_index_cahce`来查看最近新插行的数据。
|
||
|
||
`通过cache来缓存插入,并在cache满后批量刷新到磁盘`,该策略能够避免频繁访问辅助索引表,避免在插入和更新时并发访问带来的问题。
|
||
|
||
除此之外,批量插入还能避免针对相同word的多次插入,进而将重复条目最小化。
|
||
- 在使用innodb full-text index cache时,对于相同word的插入将会被合并为一条entry并插入,其不仅能提高插入性能,同时也能令库中的辅助索引表尽可能的小。
|
||
|
||
##### innodb_ft_cache_size
|
||
`innodb_ft_cache_size`用于指定full-text index cache的大小(针对每一张表),大小的指定将会影响full-text index cache被刷新的频率。
|
||
|
||
##### innodb_ft_total_cache_size
|
||
`innodb_ft_total_cache_size`用于限制所有表`full-text index cache`大小。
|
||
|
||
|
||
##### full-text查询
|
||
full-text index cache中存储的信息和辅助索引表中相同。但是,full-text cache中只存储近期插入的行。在执行查询操作时,已经被刷新到磁盘的数据并并不会被重新带到缓存中。
|
||
|
||
对full-text中数据的查询如下:
|
||
- 直接查询辅助索引表中的数据
|
||
- 查询full-text index cache中的数据
|
||
- 将辅助索引表中查询的数据和full-text index cache中查询的数据进行合并
|
||
|