53 KiB
- innodb索引与算法
- 索引算法
- B+树索引
- 索引使用
- Multi-Range Read
- Index Condition Pushdown(ICP)
- innodb hash
- 全文检索
- 倒排索引
- innodb全文检索
- innodb全文检索示例
- match ... against
- ngram full-text parser
- ngram handle stop word
- ngram parser wildcard Search
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语法,可以选择索引的创建方式:
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
- 如果针对表中某一行数据进行频繁的更新,那么该场景不会适配第一种更新策略,故而在innodb内部维护了一个计数器stat_modified_counter,用于标识发生变化的次数,当计数器的值大于
Cardinality采样
默认情况下,innodb会针对8个叶子节点进行采样,采样过程如下:
- 取得B+树索引中叶子节点的数量,记为A
- 随机获取索引8个叶子节点,统计每个页不同记录的个数,记为P1, P2, ... , P8
- 根据采样信息给出Cardinality的预估值
(P1 + P2 + ... + P8) * A/8
索引使用
联合索引
联合索引代表对表上的多个列进行索引。
示例如下:
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 = xxxselect primary key1,key2 from table where key1 = xxxselect primary key1, primary key2, key2 from key1 = xxx
覆盖索引对于统计场景的好处
例如,对于表buy_log
create table buy_log (
userid int unsigned not null,
buy_date date,
index `idx_userid` (userid),
index `idx_userid_buy_date` (userid, buy_date)
);
预置如下数据:
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来强制使用某个索引,使用示例如下:select * from orderdetails force index (orderID) where orderid > 10000 and order id < 102000
索引提示
mysql支持索引提示,如下两种场景可能会用到索引提示:
- mysql错误的选择了某个索引,导致sql语句运行较慢
- 某个sql可选择索引非常多,此时优化器选择执行计划的耗时可能大于sql语句执行的耗时
mysql数据库中,索引提示的语法如下;
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示例如下:
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索引:
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还能够进行拆分优化,这样在拆分过程中直接能过滤掉一些不符合查询条件的数据。
例如:
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)索引,执行如下语句
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来查看倒排索引的辅助表。
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参数。可以执行如下语句
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索引:
create table fs_text (
id bigint not null auto_increment,
content longtext,
primary key (id),
fulltext index `idx_fts_fs_text` (content)
);
查询辅助表:
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中查询的数据进行合并
DOC_ID和FTS_DOC_ID
innodb使用DOC_ID作为唯一文档标识符,DOC_ID将word和word出现的文档相关联。该关联关系需要被索引表中的FTS_DOC_ID字段,如果FTS_DOC_ID未在被索引表中定义,那么innodb会在fulltext索引创建时自动添加隐藏的FTS_DOC_ID字段。
示例如下:
mysql> CREATE TABLE opening_lines (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
opening_line TEXT(500),
author VARCHAR(200),
title VARCHAR(200)
) ENGINE=InnoDB;
如果使用create fulltext语法向表中添加fulltext索引时,会出现警告信息,报告innodb正在对table进行rebuilding操作。
mysql> CREATE FULLTEXT INDEX idx ON opening_lines(opening_line);
Query OK, 0 rows affected, 1 warning (0.19 sec)
Records: 0 Duplicates: 0 Warnings: 1
mysql> SHOW WARNINGS;
+---------+------+--------------------------------------------------+
| Level | Code | Message |
+---------+------+--------------------------------------------------+
| Warning | 124 | InnoDB rebuilding table to add column FTS_DOC_ID |
+---------+------+--------------------------------------------------+
当在create table时创建了fulltext idnex,并且没有在建表语句中指定FTS_DOC_ID字段,innodb会添加一个隐藏的FTS_DOC_ID字段,并且不会返回warning信息。
当使用
alter table ... add fulltex语法添加fulltext索引时,同样会返回相同的异常信息。
比起在表中已经存在数据后向表中添加fulltext索引,在create table时指定fulltext index开销更小。innodb会创建一个隐藏的FTS_DOC_ID字段,并为FTS_DOC_ID字段创建一个唯一索引`FTS_DOC_ID_INDEX。
如果想要自己创建
FTS_DOC_ID字段,该字段类型必须为BIGINT UNSIGNED NOT NUL,示例如下所示:mysql> CREATE TABLE opening_lines ( FTS_DOC_ID BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, opening_line TEXT(500), author VARCHAR(200), title VARCHAR(200) ) ENGINE=InnoDB;
对
FTS_DOC_ID字段,目前已使用的最大值和新FTS_DOC_ID字段值的最大间隔为65535
为了避免对table的rebuild,FTS_DOC_ID字段在删除full-text index后仍然会被保留。
innodb full text deleteion handle
在对一个包含full-text index column的record进行删除操作时,将会导致大量对辅助索引表的small deletion操作,这些操作可能会导致对辅助索引表的并行访问以及竞争。
为了避免上述问题,当record从table中被删除时,被删除文档的DOC_ID将会被添加到FTS_*_DELETED表中,并且被删除的记录仍然会存在于full-text index中。
在执行查询操作返回之前,FTS_*_DELETED表中包含的信息将会被用于过滤被删除的DOC_ID。上述设计将会令删除速度变得更快。
该设计也会导致fulltext index的内容大小持续增加,如果要移除被删除record对应的full-text index内容,可以执行optimize table语句。如果开启innodb_optimize_fulltext_only=on,其仅会针对fulltext index进行优化。
innodb full-text index transaction handling
由于full-text index拥有caching和批量处理的特性,full-text index有其对应的独特事务处理。
具体来说,对full-text index的更新和插入在事务提交时才会被处理,即full-text search只对已提交的数据可见。
示例如下所示
mysql> CREATE TABLE opening_lines (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
opening_line TEXT(500),
author VARCHAR(200),
title VARCHAR(200),
FULLTEXT idx (opening_line)
) ENGINE=InnoDB;
mysql> BEGIN;
mysql> INSERT INTO opening_lines(opening_line,author,title) VALUES
('Call me Ishmael.','Herman Melville','Moby-Dick'),
('A screaming comes across the sky.','Thomas Pynchon','Gravity\'s Rainbow'),
('I am an invisible man.','Ralph Ellison','Invisible Man'),
('Where now? Who now? When now?','Samuel Beckett','The Unnamable'),
('It was love at first sight.','Joseph Heller','Catch-22'),
('All this happened, more or less.','Kurt Vonnegut','Slaughterhouse-Five'),
('Mrs. Dalloway said she would buy the flowers herself.','Virginia Woolf','Mrs. Dalloway'),
('It was a pleasure to burn.','Ray Bradbury','Fahrenheit 451');
mysql> SELECT COUNT(*) FROM opening_lines WHERE MATCH(opening_line) AGAINST('Ishmael');
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
mysql> COMMIT;
mysql> SELECT COUNT(*) FROM opening_lines
-> WHERE MATCH(opening_line) AGAINST('Ishmael');
+----------+
| COUNT(*) |
+----------+
| 1 |
+----------+
如上述示例所示,在事务提交前,对full-text index的insertion和update操作都不可见,insertion和update操作直到事务提交时才会被执行。
match ... against
mysql数据库支持fulltext search查询,其语法如下:
match (col1, col2, ...) against (expr [search_modifier])
-- search modifier定义如下
search_modifier:
{
in natural language mode
| in natural language mode with query expansion
| in boolean mode
| with query expansion
}
mysql通过match against语法支持全文索引的查询,match指定了需要被查询的列,against指定了使用何种方式去查询。
natural language
全文索引查询默认采用natural language的方式进行查询,其代表查询带有指定word的文档。
对于如下语句:
select * from fts_a where body like '%Pease%'
上述查询显然无法使用B+树索引,如果换为全文索引,那么可以使用如下sql语句进行查询:
select * from fts_a where match(body) against ('Porridge' in natural language mode)
而natural language mode为默认的全文检索查询模式,故而in natural language mode修饰符可以省略,省略后如下:
select * from fts_a where match(body) against ('Porridge')
relevance
在where条件中使用match函数,其返回结果是通过relevance进行降序排序的,即相关性最高的结果放在第一位。
相关性的值为非负的浮点数值,0代表没有相关性,根据mysql官方文档可知,相关性计算依据如下四个条件:
- word是否在文档中出现
- word在文档中出现次数
- word在索引列中的数量
- 多少个文档中包含word
如果用户想要查看相关性,可以使用如下语句:
select fts_doc_id, body, match(body) against ('porridge' in natural language mode) as relevance from fts_a`
stopword
对innodb的全文检索,还应该考虑如下因素:
- 如果查询的word在
stop word中,那么忽略该字符串查询- 如果查询的
word位于stopword中,那么不对该词进行查询,例如the,对于stopword其相关性为0
- 如果查询的
- 查询
word的长度是否位于区间[innodb_ft_min_token_size,innodb_ft_max_token_size]之间innodb_ft_min_token_size和innodb_ft_max_token_size用于控制查询字符串的长度,当长度小于innodb_ft_min_token_size或大于innodb_ft_max_token_size时,会忽略该词的搜索。innodb_ft_min_token_size的默认值为3,innodb_ft_max_token_size的默认值为84
Boolean
innodb支持in boolean mode修饰符,当使用该修饰符时,查询字符串的前后字符会拥有特殊含义。
示例如下:
select * from fts_a where match(body) against ('+Pease -hot' in boolean mode)
上述示例代表文档中要包含Pease但是不包含hot。
boolean全文检索支持如下的操作符种类:
+代表word必须存在-代表word必须被排除- 如果不存在操作符,代表该word是可选的,但是word出现时relevance更高
@distance代表查询的多个单词之间,间距是否在distance之间,distance单位为字节。该全文检索的查询也被称为Proximity Search。- 例如
match (body) against ('"Pease pot"@30' in boolean mode)代表字符串Pease和Pot之间距离在30字节之内
- 例如
>表示出现该word增加相关性<表示出现该word降低相关性~表示允许出现该单词,但是出现时相关性为负*表示以该单词开头的单词,例如lik*表示可以为lik,like,likes"表示短语- full-text engine将会把短语分割为多个word,并且对
words执行全文检索。并且,非单词字符不需要完全匹配,短语搜索只需要包含和短语完全相同的单词,例如test phrase可以匹配test, phrase。
- full-text engine将会把短语分割为多个word,并且对
+/-
select * from fts_a where match(body) against ('+Pease +hot' in boolean mode)
上述示例返回既有Pease又有hot的文档
no operator
select * from fts_a where match(body) against ('Pease hot' in boolean mode)
上述示例返回有Pease或有hot的文档
Proximity Search
select * from fts_a where match(body) against ('"Pease pot" @10' in boolean mode)
query expansion
innodb支持全文检索的拓展查询。有时用户的查询关键词太短,此时用户需要implied knowledge。
例如,用户在对单词database进行查询时,还希望查询的不仅是包含database的文档,还包含MySQL、Oracle、DB2等单词,此时,可以使用query expansion来启用全文检索的implied knowledge。
通过with query expansion或in natural language mode with query expansion可以开启blind query expansion,该查询分为两阶段:
- 根据搜索的单词进行全文索引查询
- 根据第一阶段的结果再进行分词,并且按分词再进行全文索引查找
由于query expansion全文检索可能带来非常多的非相关性查询结果,因此在使用时用户应该相当小心。
ngram full-text parser
mysql中内置的full-text parser使用空格作为word之间的分隔符,对部分语言如中文,日文并不适用。为了解决该限制,mysql提供了ngramfull-text parser,其支持中文、日语、汉语。
ngram full-text parser支持innodb和myisam存储引擎。
ngram代表给定文本中n个字符的连续序列,ngram parser会将一个文本tokenize为一系列连续的n characters序列。
例如,根据不同的
n,通过gram parser,可以将字符串abcdtokenize为如下结果n=1 'a' 'b' 'c' 'd' n=2 'ab' ‘bc' 'cd' n=3 ‘abc' 'bcd' n=4 'abcd'
ngram parser是内置的server plugin,其会在server启动时自动加载。
配置ngram_token_size
ngram parser默认的ngram token size1为2,例如,当ngram token size为2时,ngram parser将字符串“abc def”转化为4个token"ab bc de ef"。
ngram_token_size参数可用于配置ngram token size。其最小值为1,最大值为10.
通常,ngram token size被设置为想要查找的最长token长度,当ngram token size越小时,将会产生更小的full-text search index,查询也会变得更快。
使用ngram parser创建fulltext Index
如下示例显示了如何创建ngram parser对应的full-text index:
mysql> USE test;
mysql> CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body) WITH PARSER ngram
) ENGINE=InnoDB CHARACTER SET utf8mb4;
mysql> SET NAMES utf8mb4;
INSERT INTO articles (title,body) VALUES
('数据库管理','在本教程中我将向你展示如何管理数据库'),
('数据库应用开发','学习开发数据库应用程序');
mysql> SET GLOBAL innodb_ft_aux_table="test/articles";
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE ORDER BY doc_id, position;
如果要向已经存在的表中添加full-text index且使用ngram parser,示例如下:
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT
) ENGINE=InnoDB CHARACTER SET utf8mb4;
ALTER TABLE articles ADD FULLTEXT INDEX ft_index (title,body) WITH PARSER ngram;
# Or:
CREATE FULLTEXT INDEX ft_index ON articles (title,body) WITH PARSER ngram;
ngram parser space handling
ngram parser在处理时消除空格,示例如下:
ab cd会被转化为ab和cda bc会被转化为bc
示例如下:
create table ngram_t (
id bigint auto_increment not null,
c1 text,
c2 text,
primary key(id),
fulltext `idx_ft_c1_c2` (c1, c2) with parser ngram
);
insert into ngram_t(c1, c2)
values
('突发!泽连斯基最新表态,称将与特朗普举行会谈', '与特朗普举行会谈'),
('如能换取乌克兰加入北约愿立即辞职','乌克兰总统泽连斯基称将与美国总统特朗普举行会谈,如能换取乌克兰加入北约愿立即辞职');
执行上述语句后,执行如下语句,分词信息如下所示:
set global innodb_ft_aux_table='innodb_demo/ngram_t';
select * from information_schema.INNODB_FT_INDEX_CACHE limit 20;
| WORD | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT | DOC_ID | POSITION |
|---|---|---|---|---|---|
| ,如能换 | 3 | 5 | 2 | 3 | 118 |
| ,如能换 | 3 | 5 | 2 | 5 | 118 |
| ,称将与 | 2 | 4 | 2 | 2 | 33 |
| ,称将与 | 2 | 4 | 2 | 4 | 33 |
| !泽连斯 | 2 | 4 | 2 | 2 | 6 |
| !泽连斯 | 2 | 4 | 2 | 4 | 6 |
| 与特朗普 | 2 | 4 | 2 | 2 | 42 |
| 与特朗普 | 2 | 4 | 2 | 2 | 25 |
| 与特朗普 | 2 | 4 | 2 | 4 | 42 |
| 与特朗普 | 2 | 4 | 2 | 4 | 25 |
| 与美国总 | 3 | 5 | 2 | 3 | 82 |
| 与美国总 | 3 | 5 | 2 | 5 | 82 |
| 举行会谈 | 2 | 5 | 4 | 2 | 54 |
| 举行会谈 | 2 | 5 | 4 | 2 | 25 |
| 举行会谈 | 2 | 5 | 4 | 3 | 106 |
| 举行会谈 | 2 | 5 | 4 | 4 | 54 |
| 举行会谈 | 2 | 5 | 4 | 4 | 25 |
| 举行会谈 | 2 | 5 | 4 | 5 | 106 |
| 乌克兰加 | 3 | 5 | 2 | 3 | 12 |
| 乌克兰加 | 3 | 5 | 2 | 3 | 121 |
ngram handle stop word
natural language mode
对于natural language mode,被搜索的词将会被转化为ngram的并集,例如abc(假设ngram token size为2)将会被转化为ab bc。
如果存在两个文档,一个文档包含ab,另一个文档包含abc,搜索词ab bc将会匹配两个文档。
boolean mode
对于boolean mode,search item将会被转化为ngram phase search,例如abc将会被转化为ab bc,如果两个文档一个包含ab,另一个包含abc,那么phase ab bc只会匹配包含abc的文档。
ngram parser wildcard Search
使用ngram praser时fulltext index只包含ngrams,使用通配符时会按照如下行为进行执行:
前缀小于ngram token size
如果前缀小于ngram token size,那么查询将会返回所有以prefix item开始的行。例如,当ngram_token_size为2时,查询a*将会返回所有以a开头的行
前缀大于ngram token size
如果前缀大于ngram token size,那么前缀将会转化为ngram phase,并且wildcard将会被忽略。例如,当ngram_token_size为2,查询abc*将会转换为ab bc