Files
rikako-note/mysql/mysql文档/mysql_索引.md
2025-02-24 19:09:16 +08:00

54 KiB
Raw Permalink Blame History

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 tablelock模式为share那么在执行过程中不会有写事务的发生不需要进行DML日志的记录。

Cardinality

并不是所有的字段都适合创建B+树索引。例如对于性别字段、类型字段等它们的选择范围都很小这种字段不适合创建B+树索引。

性别字段中只有MF两种可选值,故而执行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

索引使用

联合索引

联合索引代表对表上的多个列进行索引。

示例如下:

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

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_aidx_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那么此时查询类型为RANGEmysql优化器会将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设置为onmrr_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_pushdownon代表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/snon-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。故而,在全文检索的表中,存在两个字段,wordilist,并在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 columnschar, varchar, text来进行创建用于加速针对text based colums列的dml操作。

一个full-text index可以定义为create table语句的一部分,也可以通过alter tablecreate index语句添加到已经存在的表中。

full-text查询通过match() ... against预发来触发。

innodb full-text design

innodb全文索引基于倒排索引进行设计。倒排索引存储了一系列的words对每个word都存在一个list与之对应list中的元素为包含word的文档

为了支持proximity searchword在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被插入的还有postionDOC_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_texttable_id刚好为1822

index_id hex

在6张辅助索引表中除了包含table_id的hex外下划线后还存在一个16进制数部分该部分为05ad,代表十进制1453,而索引idx_fts_fs_textindex_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_*_deletedfts_*_deleted_cache
    • 该表用于保存文档已被删除但是数据仍然没有从full-text index中移除的文档idDOC_ID
    • fts_*_deleted_cachefts_*_deleted的内存保本
  • fts_*_being_deletedfts_*_being_deleted_cache
    • 该表用于保存文档已被删除并且文档数据正在从full-text index中被移除的文档id(DOC_ID)
    • fts_*_deleted_cachefts_*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操作,之后得到的wordword关联的数据将会被插入到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的rebuildFTS_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_sizeinnodb_ft_max_token_size用于控制查询字符串的长度,当长度小于innodb_ft_min_token_size或大于innodb_ft_max_token_size时,会忽略该词的搜索。
    • innodb_ft_min_token_size的默认值为3innodb_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)代表字符串PeasePot之间距离在30字节之内
  • >表示出现该word增加相关性
  • <表示出现该word降低相关性
  • ~表示允许出现该单词,但是出现时相关性为负
  • *表示以该单词开头的单词,例如lik*表示可以为liklikelikes
  • "表示短语
    • full-text engine将会把短语分割为多个word并且对words执行全文检索。并且,非单词字符不需要完全匹配,短语搜索只需要包含和短语完全相同的单词,例如test phrase可以匹配test, phrase
+/-
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的文档

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 expansionin 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可以将字符串abcd tokenize为如下结果

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会被转化为abcd
  • a 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 modesearch item将会被转化为ngram phase search,例如abc将会被转化为ab bc,如果两个文档一个包含ab,另一个包含abc那么phase ab bc只会匹配包含abc的文档。

使用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

示例

对于泽连斯基最新表态字符串,其实际被分割为泽连斯基,连斯基最,斯基最新,基最新表,最新表态这些word故而对于boolean mode下的phrase场景其匹配如下

  • "泽连斯基 连斯基最":能够匹配字符串
  • "泽连斯基 最新表态":无法匹配字符串