Innodb存储引擎-索引和算法(B+树索引、Cardinality、联合索引、覆盖索引、MRR优化、ICP优化、哈希索引、全文索引)

索引和算法

B+树索引

B+ 树索引的本质就是 B+ 树在数据库中的实现。但是B+ 索引在数据库中有一个特点是 高扇出性,因此在数据库中, B+树的高度一般都在2 ~ 4 层,这也就是说查找某一键值的行记录时最多只需要2 到4 次IO, 这倒不错。因为当前一般的机械磁盘每秒至少可以做 100 次IO , 2 ~ 4 次的IO 意味着查询时间只需0.02 ~ 0.04 秒。

数据库中的B+ 树索引可以分为 聚集索引辅助索引, 但是不管是聚集还是辅助的索引,其内部都是B+ 树的,即 高度平衡 的,叶子节点存放着所有的数据。

聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息。B + 树索引并不能找到一个给定键值的具体行。 B+ 树索引能找到的只是被查找数据行所在的页。然后数据库通过把页读入到内存,再在内存中进行查找, 最后得到要查找的数据。

聚集索引

聚簇索引不是一种索引类型,而是一种数据存储方式。一个表只能有一个聚簇索引,InnoDB表要求必须有聚簇索引,默认在主键字段上建立聚簇索引,会按照每张表的主键构造一棵B+ 树。在没有主键字段的情况下,表的第一个非空的唯一索引将被建立为聚簇索引,在前两者都没有的情况下,InnoDB将自动生成一个隐式的自增id列,并在此列上建立聚簇索引。

对于聚集索引表来说,表数据是和主键一起存储的,主键索引的叶结点存储行数据(包含了主键值),其他列,事务ID,回滚指针,二级索引的叶结点存储行的主键值。使用的是B+树作为索引的存储结构,非叶子节点都是索引关键字,但非叶子节点中的关键字中不存储对应记录的具体内容或内容地址。叶子节点上的数据是主键与具体记录(数据内容)。

由于实际的数据页只能按照一棵B+ 树进行排序,因此每张表只能拥有一个聚集索引(可以理解为主键)。在多数情况下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+ 树索引的叶子节点上直接找到数据。 此外,由于定义了数据的逻辑顺序,聚集索引能够特别快地访问针对范围值的查询。查询优化揣能够快速发现某一段范围的数据页需要扫描。

对于MyISAM的非聚集索引表来说,表数据和索引是分成两部分存储的,主键索引和二级索引存储上没有任何区别。使用的是B+树作为索引的存储结构,所有的节点都是索引,叶子节点存储的是索引+指向索引对应的记录的数据的指针。

看个例子:看一张表,这里以人为的方式让其每个页只能存放两个行记录:

CREATE TABLE t (
    a INT NOT NULL,
    b VARCHAR (8000),
    c INT NOT NULL,
    PRIMARY KEY (a) ,
    KEY idx c (c)
) ENGINE=INNODB ;
INSERT INTO t SELECT 1 , REPEAT('a', 7000) ,-1;
INSERT INTO t SELECT 2 , REPEAT( 'a', 7000) ,-2;
INSERT INTO t SELECT 3 , REPEAT('a', 7000 ),-3;
INSERT INTO t SELECT 4 , REPEAT( 'a', 7000) ,-4;

使用py_innodb+page_info工具分析表空间,可得

  • page level为0000的是数据页
  • page level为0001的页,当前B+树高度为2,因此这个页是B+树的根

该表对应的B+树索引:

通过hexdump工具观察索引的根页中所存放的数据,然后通过页尾的Page Directory来分析此页:

从00 63可以知道:该页中行开始的位置。接着通过Recorder Header来分析:0xc063开始的值为69 6e 66 69 6d 75 6d 00,就代表infimum为行记录,之前的5字节01 00 02 00 1b就是Recorder Header,分析第4位到第8位的值1代表该行记录中只有一个记录(需要记住的是,InnoDB的Page Directory是稀疏的),即infimum记录本身通过Recorder Header最后两个字节00 1b来判断下一条记录的位置,即c063+1b=c07e,读取键值可得80 00 00 01,这就是主键为1的键值(表定义时int是无符号的,因此二进制是0x80 00 00 01,而不是0x0001),80 00 00 01后的值00 00 00 04代表指向数据页的页号。同样的方式可以找到80 00 00 02和80 00 00 04这两个键值以及它们指向的数据页。

通过以上对非数据页节点的分析,可以发现数据页上存放的是完整的每行的记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。因此这棵聚集索引数的构造大致如下图所示:

聚集索引的存储并不是物理上连续的,而是逻辑上连续的。这其中有两点:

  • 前面说过的页通过双向链表链接,页按照主键的顺序排序;
  • 每个页中的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储 。

聚集索引的优点:它对于主键的排序查找和范围查找速度非常快。叶子节点的数据就是用户所要查询的数据。

  • 如用户需要查询一张注册用户的表,查询最后注册的10 位用户,由于B+ 树索引是双向链表的,用户可以快速找到最后一个数据页,并取出10 条记录。
  • 如果要查找主键某一范围内的数据,通过叶子节点的上层中间节点就可以得到页的范围,之后直接读取数据页即可。
辅助索引

对于辅助索引(Secondary Index, 也称非聚集索引),叶子节点并不包含行记录的全部数据。 叶子节点除了包含键值以外, 每个叶子节点中的索引行中还包含了一个 书签(bookmark) 。该书签用来告诉InnoDB 存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB 存储引擎表是索引组织表,此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键

辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时, InnoDB 存储引擎会遍历辅助索引并通过叶子节点的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。

如果在一棵高度为3 的辅助索引树中查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3, 那么还需要对聚集索引树进行3 次查找,最终找到一个完整的行数据所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页。

看下辅助索引和聚集索引的关系:

B+ 树索引的分裂

B+ 树索引页的分裂并不总是从页的中间记录开始,这样可能会导致页空间的浪费。并且B+ 树索引实现最为困难的部分是涉及并发的部分。

比如对于1 、2 、3 、4 、5 、6 、7 、8 、9,插入是根据自增顺序进行的,若这时插入10 这条记录后需要进行页的分裂操作,会将记录5 作为分裂点记录(split record), 分裂后得到下面两个页:P1:1 、2 、3 、4;P2:5 、6 、7 、8 、9 、10。然而由于插入是顺序的, P1 这个页中将不会再有记录被插入,从而导致空间的浪费,而 P2 又会再次进行分裂。

InnoDB 存储引擎的 Page Header 中有以下几个部分用来保存插入的顺序信息:

  • PAGE_LAST_INSERT :表示页最后插人的位置的偏移量,指向的是行记录内容;
  • PAGE_DIRECTION :最后插入的方向;
  • PAGE_N_DIRECTION:同一个方向上连续插入数。

通过这几个属性, InnoDB 存储引擎可以决定是向左还是向右进行分裂,同时决定将分裂点记录为哪一个。

  • 若插入是随机的,则取页的中间记录作为分裂点的记录;
  • 若往同一方向插入的记录数量为5,并且目前已经定位(cursor)到的记录(InnoDB时,首先需要进行定位,定位到的记录为待插入记录的前一条记录)之后还有3条记录,则分裂点的记录为定位到的记录后的第三条记录,否则分裂点记录就是待插入的记录

现在看看一个向右分裂的例子,并且定位到的记录之后还有3条记录,则分裂点记录如下图所示:

向右分裂且定位到的记录之后还有3条记录,split record为分裂点记录,最终向右分裂得到下图所示的情况:

对于下图的情况,分裂点就为插人记录本身,向右分裂后仅插人记录本身,这在自增插人时是普遍存在的一种情况:

B+ 树索引的管理

索引的创建和删除可以通过两种方法,一种是 ALTER TABLE, 另一种是 CREATE/DROP INDEX:

ALTER TABLE tbl_name
ADD {INDEX|KEY} [index_name)
[index_type] (index_col_name, ...) [index_option]...

ALTER TABLE tbl name
DROP PRIMARY KEY
DROP {INDEX|KEY} index_name

CREATE [UNIQUE] INDEX index_name
[index_type]
ON tbl_name (index_col_name, ...)

DROP INDEX index_name ON tbl_name

用户可以设置对整个列的数据进行索引,也可以只索引一个列的开头部分数据,如前面创建的表 t(eg1), 列 b 为varchar (8000), 但是用户可以只索引前 100 个字段,如:

ALTER TABLE t ADD KEY idx_b (b(1OO));

并在表t上加一个对于列 (a, c) 的联合索引 idx_a_c, 可得:

ALTER TABLE t ADD KEY idx_a_c (a,c);

对表 t 查看表中索引的信息,可以使用命令SHOW INDEX

mysql>SHOW INDEX FROM t\G;
*************************** 1. row***************************
Table : t   Non_unique: 0    Key_name: PRIMARY   Seq_in_index: 1    Column_name: a
Collation: A   Cardinality: 2   Sub_part: NULL   Packed: NULL  Null:   Index_type: BTREE   Comment:
*************************** 2. row***************************
Table: t   Non_unique: 1   Key name: idx b    Seq_in_index: 1   Column_name: b
Collation: A   Cardinality: 2   Sub_part: 100   Packed: NULL  Null: YES   Index_type : BTREE  Comment:
*************************** 3. row***************************
Table: t   Non_unique: 1   Key_name: idx_a c  	Seq_in_index: 1   Column_name: a
Collation: A  Cardinality: 2   Sub_part: NULL  Packed: NULL  Null:   Index_type: BTREE   Comment:
*************************** 4. row***************************
Table: t   Non_unique: 1	Key_name: idx_a c	Seq_in_index: 2	Column_name: c
Collation: A	Cardinality: 2	Sub_part: NULL	Packed: NULL	Null:	Index_type: BTREE	Comment:
*************************** 5. row***************************
Table: t	Non_unique: 1	Key_name: idx c		Seq_in_index: 1	Column_name: c	Collation: A
Cardinality : 2	Sub_part: NULL	Packed: NULL	Null:	Index_type: BTREE	Comment:
5 rows in set (0 .00 sec)

通过命令SHOW INDEX FROM 可以观察到表t 上有4 个索引,分别为主键索引、c列上的辅助索引、b 列的前100 字节构成的辅助索引,以及(a ,c) 的联合辅助索引。

Cardinality 值非常关键,优化器会根据这个值来判断是否使用这个索引。但是这个值并不是实时更新的,即并非每次索引的更新都会更新该值,因为这样代价太大了。因此这个值是不太准确的,只是一个大概的值。上面显示的结果主键的Cardinality 为2,但是我们的表中有4 条记录,这个值应该是4 。如果需要更新索引Cardinality 的信息,可以使用ANALYZE TABLE 命令。

(1)Fast Index Creation

MySQL 5.5 版本之前的对于索引的添加或者删除的这类DDL 操作让人诟病,过程如下:

  • 首先创建一张新的临时表,表结构为通过命令ALTER TABLE 新定义的结构。
  • 然后把原表中数据导入到临时表。
  • 接着删除原表。
  • 最后把临时表重名为原来的表名。

若用户对于一张大表进行索引的添加和删除操作,那么这会需要很长的时间。更关键的是,若有大量事务需要访问正在被修改的表,这意味着数据库服务不可用。

InnoDB 存储引擎从lnnoDB 1.0.x 版本开始支持一种称为 Fast Index Creation (快速索引创建)的索引创建方式——简称 FIC 。

对于辅助索引的创建, InnoDB 存储引擎会对创建索引的表加上一个S 锁(可以读但是不能写)。在创建的过程中,不需要重建表,因此速度较之前提高很多,并且数据库的可用性也得到了提高。删除辅助索引操作就更简单了, InnoDB 存储引擎只需更新内部视图,并将辅助索引的空间标记为可用,同时删除MySQL 数据库内部视图上对该表的索引定义即可。

注意:

  • 临时表的创建路径是通过 参数tmpdir 进行设置的。用户必须保证tmpdir 有足够的空间可以存放临时表,否则会导致创建索引失败。
  • 由于FIC 在索引的创建的过程中对表加上了S 锁,因此在创建的过程中只能对该表进行读操作,若有大量的事务需要对目标表进行写操作,那么数据库的服务同样不可用。
  • FIC 方式只限定于辅助索引,对于主键的创建和删除同样需要重建一张表。

(2)Online Schema Change

Online Schema Change (在线架构改变,简称OSC) 最早是由Facebook 实现的一种在线执行DDL 的方式。所谓“在线”是指在事务的创建过程中,可以有读写事务对表进行操作,这提高了原有MySQL 数据库在DDL 操作时的 并发性。

Facebook 采用PHP 脚本来现实OSC, 而并不是通过修改InnoDB 存储引擎源码的方式。实现OSC 步骤如下:

由于OSC 只是一个PHP 脚本,因此其有一定的局限性。例如其要求进行修改的表一定要有主键,且表本身不能存在外键和触发器。此外,在进行OSC 过程中,允许SET sql_bin_log=O, 因此所做的操作不会同步slave 服务器,可能导致主从不一致的情况。

(3)Online DDL

虽然FIC 可以让InnoDB 存储引擎避免创建临时表,从而提高索引创建的效率。但也有缺陷,比如索引创建时会阻塞表上的DML 操作。OSC 虽然解决了上述的部分问题,但是还是有很大的局限性。

MySQL 5.6 版本开始支持Online DDL (在线数据定义)操作,其允许辅助索引创建的同时,还允许其他诸如INSERT 、UPDATE 、DELETE这类DML 操作,这极大地提高了MySQL 数据库在生产环境中的可用性。

此外,不仅是辅助索引,以下这几类DDL 操作都可以通过“在线”的方式进行操作:

  • 辅助索引的创建与删除
  • 改变自增长值
  • 添加或删除外键约束
  • 列的重命名

通过新的ALTER TABLE 语法,用户可以选择索引的创建方式:

ALTER TABLE tbl name
ADD {INDEX | KEY} [index_ name]
[index_type] (index_col_name, ...) [index_option]...
ALGORITHM[=] {DEFAULT|INPLACE|COPY}
LOCK [=] {DEFAULT|NONE|SHARED|EXCLUSIVE}

ALGORITHM 指定了创建或删除索引的算法:

  • COPY 表示按照MySQL 5.1 版本之前的工作模式,即创建临时表的方式;
  • INPLACE 表示索引创建或删除操作不需要创建临时表;
  • DEFAULT 表示根据参数 old_alter_ table 来判断是通过INPLACE 还是COPY 的算法,该参数的默认值为OFF, 表示采用INPLACE 的方式。

LOCK 部分为索引创建或删除时对表添加锁的清况 可有的选择为:

  • NONE:执行索引创建或者删除操作时,对目标表不添加任何的锁,即事务仍然可以进行读写操作,不会收到阻塞。因此这种模式可以获得最大的并发度;
  • SHARE:这和之前的FIC 类似,执行索引创建或删除操作时,对目标表加上一个S 锁。对于并发地读事务,依然可以执行,但是遇到写事务,就会发生等待操作。如果存储引擎不支持SHARE 模式,会返回一个错误信息;
  • EXCLUSIVE:执行索引创建或删除操作时,对目标表加上一个X 锁。读写事务都不能进行,因此会阻塞所有的线程,这和COPY 方式运行得到的状态类似,但是不需要像COPY 方式那样创建一张临时表;
  • DEFAULT:首先会判断当前操作是否可以使用NONE 模式,若不能,则判断是否可以使用SHARE 模式,最后判断是否可以使用EXCLUSIVE 模式。也就是说DEFAULT 会通过判断事务的最大并发性来判断执行DDL 的模式。

InnoDB 存储引擎实现Online DDL 的原理是:在执行创建或者删除操作的同时,将INSERT 、UPDATE 、DELETE 这类DML 操作日志写入到一个缓存中。待完成索引创建后再将重做应用到表上,以此达到数据的一致性。

这个缓存的大小由参数innodb_online_alter_log_max_size 控制,默认的大小为128MB 。若用户更新的表比较大,并且在创建过程中伴有大量的写事务,如遇到innodb_online_alter_log_max_size 的空间不能存放日志时,会抛出类似如下的错误:

Error:1799SQLSTATE:HYOOO(ER INNODB ONLINE LOG TOO BIG)
Message: Creating index ' idx_aaa'required more than'innodb_online_alter_log_max_size'bytes of modification log. Please try again.

解决方法:

  • 调大参数innodb_ online alter_ log_ max_size, 以此获得更大的日志缓存空间。
  • 设置ALTER TABLE 的模式为SHARE(S锁), 这样在执行过程中不会有写事务发生,因此不需要进行DML 日志的记录。

由于 Online DDL 在创建索引完成后再通过重做日志达到数据库的最终一致性,这意味着在索引创建过程中, SQL 优化器不会选择正在创建中的索引。

Cardinality 值

并不是在所有的查询条件中出现的列都需要添加索引。在访问表中很少一部分时使用 B+ 树索引才有意义

比如按性别进行查询时,SQL 语句得到的结果可能是该表50% 的数据(低选择性),这时添加B+ 树索引是完全没有必要的。相反,如果某个字段的取值范围很广,几乎没有重复,即属于高选择性,则此时使用B+ 树索引是最适合的。

可以通过 SHOW INDEX 结果中的列 Cardinality 来观察。Cardinality 值非常关键,表示索引中不重复记录数量的预估值。同时需要注意的是,Cardinality 是一个预估值,而不是一个准确值,基本上用户也不可能得到一个准确的值。在实际应用中,Cardinality/n_rows_in_table 应尽可能地接近1。如果非常小,那么用户需要考虑是否还有必要创建这个索引。故在访问高选择性属性的字段并从表中取出很少一部分数据时,对这个字段添加 B+ 树索引是非常有必要的。

如:表member 大约有500 万行数据。usemick 字段上有一个唯一的索引。这时如果查找用户名为David 的用户,将会得到如下的执行计划:

mysql>EXPLAIN SELECT* FROM member
-> WHERE usernick='David'\G;
*************************** 1. row***************************
id: 1
select_type: SIMPLE
table: member
type: canst
possible_keys: usernick
key: usernick
key_len: 62
ref: canst
rows: 1
Extra:
1 row in set (0.00 sec)

可以看到使用了usernick 这个索引,这也符合之前提到的高选择性,即SQL语句选取表中较少行的原则。

建立索引的前提是列中的数据是高选择性的,这对数据库来说才具有实际意义。然而数据库是怎样来统计Cardinality 信息的呢? 因为MySQL 数据库中有各种不同的存储引擎,而每种存储引擎对于B+ 树索引的实现又各不相同,所以对Cardinality 的统计是放在存储引擎层进行的

索引的更新操作可能是非常频繁的。如果每次索引在发生操作时,也就是 INSERT 和 UPDATE 时,就对其进行Cardinality 的统计,那么将会给数据库带来很大的负担。并且表非常大时,那么统计一次Cardinality 信息所需要的时间可能非常长。数据库对于 Cardinality 的统计都是通过 采样(Sample) 的方法来完成的

InnoDB 存储引擎内部对更新Cardinality 信息的策略为:

  • 表中1/16 的数据已发生过变化:自从上次统计Cardinality 信息后,表中1/16 的数据已经发生过变化,这时需要更新Cardinality 信息;
  • stat_modified_counter > 2 000 000 000 :当发生变化的次数大于2 000 000 000 时,就需要更新Cardinality 信息。因为如果对表中某一行数据频繁地进行更新操作,这时表中的数据实际并没有增加,实际发生变化的还是这一行数据,则第一种更新策略就无法适用这这种情况。

InnoDB 存储引擎内部是怎样来进行Cardinality 信息的统计和更新操作的呢?同样是通过 采样 的方法。默认InnoDB 存储引擎对 8 个叶子节点(Leaf Page) 进行采用。采样过程如下:

  • 取得B+ 树索引中叶子节点的数量,记为A ;
  • 随机取得B+ 树索引中的8 个叶子节点。统计每个页不同记录的个数,即为 P1,P2, …, P8 ;
  • 根据采样信息给出Cardinality 的预估值。

在InnoDB存储引擎中,Cardinality值是通过对8个叶子节点预估而得的,不是一个实际精确的值。再者,每次对Cardinality 值的统计,都是通过随机取8个叶子节点得到的,这同时又暗示了另一个 Cardinality 现象,即每次得到的Cardinality 值可能是不同的

SHOW INDEX FROM OrderDetails 会触发MySQL 数据库对于Cardinality 值的统计,因此表的内容没有发生改变,但是Cardinality值发生改变了。

当然,有一种情况可能使得用户每次观察到的索引 Cardinality值都是一样的,那就是表足够小,表的叶子节点数小于或者等于8个。这时即使随机采样,也总是会采取到这些页,因此每次得到的 Cardinality 值是相同的。

在InnoDB 1.2 版本之前,可以通过参数 innodb_stats_sample_pages 用来设置统计Cardinality 时每次采样页的数量,默认值为8。 同时,参数innodb_stats_method 用来判断如何对待索引中出现的NULL 值记录。该参数默认值为nulls_equal, 表示将NULL 值记录视为相等的记录。其有效值还有nulls_unequal, nulls_ignored, 分别表示将NULL值记录视为不同的记录和忽略NULL 值记录。例如某页中索引记录为NULL 、NULL 、1 、2 、2 、3 、3 、3, 在参数innodb_stats_method 的默认设置下,该页的Cardinality 为4; 若参数innodb_stats_method 为 nulls_unequal, 则该页的Caridinality 为5; 若参数innodb_stats_ method 为nulls_ignored, 则 Cardinality 为3 。

**当执行SQL 语句ANALYZE TABLE 、SHOW TABLE STATUS 、SHOW INDEX以及访问INFORMATION_SCHEMA 架构下的表TABLES 和STATISTICS 时会导致lnnoDB 存储引擎去重新计算索引的Cardinality 值。**若表中的数据量非常大,并且表中存在多个辅助索引时,执行上述这些操作可能会非常慢。虽然用户可能并不希望去更新Cardinality 值。InnoDBl.2 版本提供了更多的参数对Cardinality 统计进行设置,如下表:

B+ 树索引的使用

数据库中存在两种类型的应用, OLTP 和OLAP应用。

OLTP: 在OLTP 应用中,查询操作只从数据库中取得一小部分数据,一般可能都在10 条记录以下,甚至在很多时候只取1 条记录,在这种情况下, B+ 树索引建立后,对该索引的使用应该只是通过该索引取得表中少部分的数据。这时建立B+ 树索引才是有意义的,否则即使建立了,优化器也可能选择不使用索引。

OLAP: 在OLAP 应用中,都需要访问表中大量的数据,根据这些数据来产生查询的结果,这些查询多是面向分析的查询,目的是为决策者提供支持。因此在OLAP 中索引的添加根据的应该是宏观的信息,而不是微观,因为最终要得到的结果是提供给决策者的。但是对于OLAP 中的复杂查询,要涉及多张表之间的联接操作,因此索引的添加依然是有意义的。

联合索引

联合索引是指对表上的多个列进行索引

以下代码创建了一张t 表,并且 索引idx_a_b 是联合索引,联合的列为(a, b) 。

CREATE TABLE t (
    a INT,
    b INT,
    PRIMARY KEY (a),
    KEY idx ab (a,b)
) ENGINE=INNODB

联合索引内部的结构:从本质上来说,联合索引也是一棵B+ 树,不同的是联合索引的键值的数量不是1, 而是大于等于2。比如下图为例,对于查询SELECT * FROM TABLE WHERE a=xxx and b=xxx, 显然是可以使用(a, b) 这个联合索引的。对于单个的a 列查询 SELECT * FROM TABLE WHERE a=xxx, 也可以使用这个(a, b) 索引 。但对于b 列的查询SELECT * FROM TABLE WHERE b=xxx, 则不可以使用这棵B+ 树索引,因为叶子节点的b值不是排序的。

联合索引的第二个好处是已经对第二个键值进行了排序处理。其实就是比如联合索引(a,b),b在a给定的情况下,b是有序的,对于这类的查询是直接可以通过联合索引得到的,SQL语句层面就是:联合索引(a, b) 等价于SELECT … FROM TABLE WHERE a=xxx ORDER BY b,对于联合索引Ca, b, c) 来说,下列语句同样可以直接通过联合索引得到结果:

SELECT ... FROM TABLE WHERE a=xxx ORDER BY b
SELECT ... FROM TABLE WHERE a=xxx AND b=xxx ORDER BY c

例子:创建如下表,并对userid字段设置一个单列索引,对userid和buy_date字段同时设置一个联合字段

create table buy_log(
    userid int unsigned not null,
    buy_date date
)engine=innodb;
alter table bug_log add key(userid);
alter table bug_log add key(userid,buy_date);
  • 此时我们查看一下只对userid执行查询时使用到的索引。可以看到可以使用的索引有两个,但是这个查询语句只用了userid索引(因为该索引的叶子节点包含单个键值,所以理论上一个页能存放的记录应该更多):

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HERJRygp-1671371228793)(null)]

  • 现在取出userid为1的最近3次购买记录,然后查看执行计划。可以看到可以使用的索引有两个,但是这个查询语句使用了联合索引(因为在这个查询中buy_date字段已经排序好了,使用联合索引取出数据,无序再对buy_date字段再进行一次排序操作)

  • 若查询时强制使用userid索引,那么执行计划如下,可以看到在“Extra”信息中显示“Using filesort”代表需要额外的一次排序操作才能完成查询,而这次显示需要对列buy_date进行排序,因为索引userid中buy_date是未排序的:

覆盖索引

InnoDB 存储引擎支持覆盖索引(covering index, 或称索引覆盖),即从辅助索引中就可以得到查询的记录, 而不需要查询聚集索引中的记录

好处:辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO 操作

对于InnoDB 存储引擎的辅助索引而言,由于其包含了主键信息,因此其叶子节点存放的数据为(primary key 1 , primary key2, …, key1, key2, …)。例如,下列语句都可仅使用一次辅助联合索引来完成查询:

SELECT key2 FROM table WHERE keyl=xxx;
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 table WHERE key1=xxx;

覆盖索引的另一个用处是对某些统计问题而言的,通过辅助索引就能完成数据统计,减少了IO操作

执行 SELECT COUNT(*) FROM buy_log ; InnoDB 存储引擎并不会选择通过查询聚集索引来进行统计。由于buy_log 表上还有辅助索引,而辅助索引远小于聚集索引,选择辅助索引可以减少IO 操作。在实际执行时,优化器进行了覆盖索引操作。possible_keys为NULL,但是Extra却显示了优化器进行了覆盖索引操作。

此外,在通常情况下,诸如(a,b)的联合索引,一般是不可以选择列b中所谓的查询条件。但是如果是统计操作,并且是覆盖索引的,则优化器会进行选择。从图中可以看到possible_keys依然为NULL,但是key却显示userid_2,即表示(userid,buy_date)的联合索引。Extra同样可以发现Using index提示,表示为覆盖索引。

优化器选择不使用索引的情况

在某些情况下,当执行EXPLAIN 命令进行SQL 语句的分析时,会发现优化器并没有选择索引去查找数据(当然不考虑索引本身没用),而是通过扫描聚集索引,也就是直接进行全表的扫描来得到数据。这种情况多发生于范围查找、JOIN 链接操作等情况下。

SELECT * FROM orderdetails WHERE orderid>1OOOO and orderid<102000;

表中有(orderid,productid)的联合主键,还有对于列orderid的单个索引,然后通过explain命令:

SQL语句显示的结果,possible_keys上可以看到查询使用primary、orderid、ordersorder_details 3个索引,优化器没有按照orderid上的索引来查找数据,而是选择了primary聚集索引(也就是表扫描,而非orderid辅助索引扫描)。

为什么直接进行全表(聚集索引)的扫描呢?

原因在于如果用户要选取的数据是整行信息,而辅助索引不能覆盖到我们要查询的所有(行)信息,因此在对辅助索引查询到指定数据后,还需要一次书签访问来查找整行数据的信息。虽然辅助索引中数据是顺序存放的,但是再一次进行书签查找的数据则是无序的,因此变为了磁盘上的离散读操作如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20% 左右),优化器会选择通过聚集索引来查找数据。因为之前巳经提到过,顺序读要远远快于离散读。因此对于不能进行索引覆盖的情况,优化器选择辅助索引的情况是,通过辅助索引查找的数据是少量的。(如果进行了索引覆盖,那么很多情况我们都可以选择辅助索引代替聚集索引)。

这是由当前传统机械硬盘的特性所决定的,即利用顺序读来替换随机读的查找。若用户使用的磁盘是固态硬盘,随机读操作非常快,同时有足够的自信来确认使用辅助索引可以带来更好的性能。(可以使用关键字FORCE INDEX来强制使用某个索引)

索引提示

MySQL数据库支持索引提示(index hint),显式地告诉优化器使用哪个索引。以下两种情况可能需要用到索引提示:

  • MySQL数据库的优化器错误地选择了某个索引,导致SQL语句运行的很慢。这种情况在最新的MySQL数据库版本中很少见。优化器在绝大部分情况下工作得都非常有效和正确。这时有经验的DBA或开发人员可以强制优化器使用某个索引,以此提高SQL运行的速度;
  • 某SQL语句可以选择的索引非常多,这时优化器选择执行计划时间的开销可能会大于SQL语句本身。例如,优化器分析Range查询本身就是比较耗时的操作。这时DBA或开发人员分析最优的索引选择,通过索引提示来强制使优化器不进行各个执行路径的成本分析,直接选择指定的索引来完成查询。

语法格式:

tbl_name [ [AS] alias] [index_hint_list]
index hint list:
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_name [, index_ name] ...
  • USE INDEX:提示操作,提示优化器可以使用某个索引,但是实际上使用的索引还是根据优化器自己选择;
  • FORCE INDEX:强制操作,让优化器强制根据自己提供的索引来进行查询工作。
Multi-Range Read 优化(MRR)

MySQL5.6 版本开始支持 Multi-Range Read (MRR) 优化。目的是为了减少磁盘的随机访问,并且将随机访问转化为较为顺序的数据访问,这对于IO-bound 类型的SQL 查询语句可带来性能极大的提升;还可以将某些范围查询,拆分为键值对,以此来进行批量的数据查询。Multi-Range Read 优化可适用于 range, ref, eq_ref 类型的查询。

MRR 优化的好处:

  • MRR 使数据访问变得较为顺序。在查询辅助索引时,首先根据得到的查询结果,按照主键进行 排序,并按照主键排序的顺序进行书签查找。
  • 减少缓冲池中页被替换的次数。若InnoDB 存储引擎或者MylSAM 存储引擎的缓冲池不是足够大,即不能存放下一张表中的所有数据,此时频繁的离散读操作还会导致缓存中的页被替换出缓冲池,然后又不断地被读入缓冲池。若是按照主键顺序进行访问,则可以将此重复行为降为最低。(性能差距非常大)(进行对比时,需要确保缓冲池中没有被预热,以及需要查询的数据并不包含在缓冲池中。)
  • 批量处理对键值的查询操作

对于 InnoDB 和MyISAM 存储引擎的范围查询和JOIN 查询操作, MRR 的工作方式:

  1. 优化器将二级索引查询到的记录放到一块缓冲区中(read_end_buffer_size);
  2. 如果二级索引扫描到文件的末尾或者缓冲区已满,则使用快速排序对缓冲区中的内容按照主键(rowid)进行排序;
  3. 根据缓冲区中的主键来访问实际的数据文件,如果取完数据,则继续调用过程 2) 、3),直至扫描结束。

通过上述过程,优化器将二级索引随机的 IO 进行排序,转化为主键的有序排列,从而实现了随机 IO 到顺序 IO 的转化,提升性能。

**当不启用MRR特性时,**看到的执行如下图所示:

若启动MRR特性,则除了会在列Extra看到Using index condition外,还会看见Using MRR选项,如下图所示:

Multi-Range Read 还可以将某些范围查询,拆分为键值对,以此来进行批量的数据查询。这样做的好处是可以在拆分过程中,直接过滤一些不符合查询条件的数据。

select * from t where key_part1>=1000 and key_part1<2000 and let_part2=10000;

表t中有(key_part1,key_part2)的联合索引,因此索引根据key_part1,key_part2的位置关系进行排序。

  • 若没有启动MRR,此时查询类型为范围查询,SQL优化器会先将key_part1大于1000且小于2000的数据都取出,即使key_part2不等于1000。待取出行数据后再根据key_part2的条件进行过滤。这会导致无用数据被取出。如果有大量的数据且其key_part2不等于1000,则启用MRR优化会使性能有巨大的提升;
  • 如果启用了MRR,优化器会先将查询条件进行拆分,然后再进行数据查询。就上述查询而言,优化器会将查询条件拆分为(1000,10000),(1001,10000),(1002,10000),…,(1999,10000),最后再根据这些拆分出的条件进行数据的查询。

现在来看一个实际的例子。例如:

select * from salaeies
where (from_date between '1986-01-01' and '1995-01-01')
and (salary between 38000 and 40000);

若启用MRR优化,则执行计划如下图所示:

是否启用Multi-Range Read 优化可以通过参数 optimizer_switch 中的标记( flag) 来控制。当mrr 为on 时,表示启用Multi-Range Read 优化。mrr_cost_based 标记表示是否通过cost based 的方式来选择是否启用mrr 。若将mrr 设为on, mrr_cost_based 设为off,则总是启用Multi-Range Read 优化。例如,下述语句可以将Multi-Range Read 优化总是设为开启状态。

set @@optimizer_switch='mrr=on,mrr_cost_based=off';

参数read_md_buffer_size 用来控制键值的缓冲区大小, 当大于该值时,则执行器对已经缓存的数据根据 RowID 进行排序,并通过RowID 来取得行数据。该值默认为256K。

Index Condition Pushdown优化(ICP)

和Multi-Range Read 一样, Index Condition Pushdown 同样是MySQLS.6 开始支持的一种 根据索引进行查询的优化方式。之前的MySQL 数据库版本不支持Index Condition Pushdown,当进行索引查询时, 首先根据索引来查找记录,然后再根据WHERE 条件来过滤记录。

在支持Index Condition Pushdown 后, MySQL 数据库会在取出索引的同时,判断是否可以进行WHERE 条件的过滤,也就是将WHERE 的部分过滤操作放在了存储引擎层。在某些查询下,可以大大减少上层SQL 层对记录的索取(fetch), 从而提高数据库的整体性能

当优化器选择Index Condition Pushdown 优化时,可在执行计划的列Extra 看到Using index condition 提示。

所有SQL的where条件,均可归纳为3大类:Index Key (First Key & Last Key),Index Filter,Table Filter:

  • Index First Key

    只是用来定位索引的起始范围,因此只在索引第一次Search Path(沿着索引B+树的根节点一直遍历,到索引正确的叶节点位置)时使用,一次判断即可;

  • Index Last Key

    用来定位索引的终止范围,因此对于起始范围之后读到的每一条索引记录,均需要判断是否已经超过了Index Last Key的范围,若超过,则当前查询结束;

  • Index Filter

    用于过滤索引查询范围中不满足查询条件的记录,因此对于索引范围中的每一条记录,均需要与Index Filter进行对比,若不满足Index Filter则直接丢弃,继续读取索引下一条记录;

  • Table Filter

    则是最后一道where条件的防线,用于过滤通过前面索引的层层考验的记录,此时的记录已经满足了Index First Key与Index Last Key构成的范围,并且满足Index Filter的条件,回表读取了完整的记录,判断完整记录是否满足Table Filter中的查询条件,同样的,若不满足,跳过当前记录,继续读取索引的下一条记录,若满足,则返回记录,此记录满足了where的所有条件,可以返回给前端用户。

我对Using index condition的理解是,首先mysql server和storage engine是两个组件,server负责sql的parse,执行; storage engine去真正的做数据/index的读取/写入。以前是这样:server命令storage engine按index key把相应的数据从数据表读出,传给server,然后server来按where条件(index filter和table filter)做选择。而在MySQL 5.6加入ICP后,Index Filter与Table Filter分离,Index Filter下降到InnoDB的索引层面进行过滤,如果不符合条件则无须读数据表,减少了回表与返回MySQL Server层的记录交互开销,节省了disk IO,提高了SQL的执行效率

ICP的使用限制:

  • 当sql需要全表访问时,ICP的优化策略可用于range, ref, eq_ref, ref_or_null类型的访问数据方法 。
  • 支持InnoDB和MyISAM表。
  • ICP只能用于二级索引,不能用于主索引。
  • 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
  • ICP的加速效果取决于在存储引擎内通过ICP筛选掉的数据的比例。

假设下面的表有联合索引(zip_code,last_name,firset_name),查询语句如下:

select * from people where zipcode='95054'
and lastname like '%etrunia%'
and address like '%Main Street%';

95054是Index key,lastname like ‘%etrunia%’ and address like ‘%Main Street%’ 是Index Filter,MySQL可以通过索引来定位zipcode等于95054的记录

  • 若不支持ICP优化,则数据库需要先通过索引取出所有zipcode等于95054的记录,然后再过滤WHERE之后的两个条件;
  • 若支持ICP优化,则在索引取出时,就会进行WHERE条件的过滤,然后再去获取记录。当然,WHERE可以过滤的条件是要该索引可以覆盖到的范围。

参考:

哈希索引

InnoDB 存储引擎使用哈希算法来对字典进行查找,其冲突机制采用链表方式,哈希函数采用除法散列方式(h(k)=k%m)。对于缓冲池页的哈希表来说,在缓冲池中的Page 页都有一个chain 指针,它指向相同哈希函数值的页(相当于碰撞后的链表)。

而对于除法散列, m 的取值为略大于2倍的缓冲池页数最的质数。eg:当前参数innodb_buffer_pool_size 的大小为 l0M, 则共有640 个16KB 的页。对于缓冲池页内存的哈希表来说,需要分配640X2=1280 (因640*16=10240)个槽,但是由于1280 不是质数,需要取比1280 略大的一个质数,应该是1399, 所以在启动时会分配1399 个槽的哈希表,用来哈希查询所在缓冲池中的页。

InnoDB 存储引擎的缓冲池对于其中的页是怎么进行查找的呢?上面只是给出了一般的算法,怎么将要查找的页转换成自然数呢?

InnoDB 存储引擎的表空间都有一个space_id, 用户所要查询的应该是某个表空间的某个连续16KB 的页,即偏移量offset 。InnoDB 存储引擎将space_id 左移20 位,然后加上这个space_id 和offset,即为space_id<<20+space_id+offset,然后通过除法散列到各个槽中去。

自适应哈希索引采用哈希表的方式实现。不同的是,这仅是数据库自身创建并使用的,DBA本身并不能对其进行千预。自适应哈希索引经哈希函数映射到一个哈希表中,因此对于字典类型的查找非常快速,如SELECT * FROM TABLE WHERE index_col=‘xxx’。但是对于范围查找就无能为力了。通过命令SHOW ENGINE
INNODB STATUS可以看到当前自适应哈希索引的使用状况。

全文索引

前面介绍过了,B+树索引的特点是可以通过索引字段的前缀进行查找。例如对于下面的查询B+树是支持的:

select * from blog where content like 'xxx%';

但是有时候我们要查询的是博客的内容中含有“xxx”,而不是以“xxx”开头,所以应该是下面的SQL语句:

select * from blog where content like '%xxx%';

即便添加了B+ 树索引也是需要进行索引的扫描来得到结果。而这种全文进行查找,这些都不是B+ 树索引所能很好地完成的工作。

全文检索(Full-Text Search) 是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。

在之前的MySQL 数据库中, InnoDB 存储引擎并不支持全文检索技术。大多数的用户转向MyISAM 存储引擎,这可能需要进行表的拆分,并将需要进行全文检索的数据存储为MyISAM 表。这样的确能够解决逻辑业务的需求,但是却丧失了InnoDB 存储引擎的事务性,而这在生产环境应用中同样是非常关键的。从lnnoDB 1.2.x 版本开始, InnoDB 存储引擎开始支持全文检索,其支持MylSAM存储引擎的全部功能,并且还支持其他的一些特性。

倒排索引

全文检索通常使用 倒排索引(inverted index) 来现。倒排索引同B+ 树索引一样,也是一种索引结构。

  • 正排索引:由文档指向关键词(记录–> 单词)
  • 倒排索引: 由关键词指向文档(单词—> 记录)

它在辅助表(auxiliary table) 中存储了单词与单词自身在一个或多个文档中所在位置之间的映射。这通常利用关联数组实现,其拥有两种表现形式:

  • inverted file index, 其表现形式为 {单词,单词所在文档的ID}
  • full inverted index, 其表现形式为{单词,(单词所在文档的ID, 在具体文档中的位置)}

例如下面是一张表t。其中有DocumentId、Text两个字段:

如果采用inverted file index的关联数组实现,则其存储的内容如下表所示:

可以看到:单词code存在于文档1和4中,单词days存在于文档3和6中…

如果采用full inverted index的关联数组实现,则其存储的内容如下表所示:

可以看到:code单词为(1:6),(4,8),表示code这个单词出现在文档1的第6个单词处,和文档4的第8个单词处。

full inverted index还存储了单词所在的位置信息,所以其占用更多的空间,但是能更好地定位数据,并扩充一些其他的搜索特性

InnoDB全文检索的实现

InnoDB 存储引擎从1.2.x 版本开始支持全文检索的技术,其采用 full inverted index 的方式。在InnoDB 存储引擎中,将(Documentld, Position) 视为一个"ilist" 。因此在全文检索的表中,有两个列,一个是word 字段,另一个是ilist 字段,并且在word 字段上设有索引。此外,由于InnoDB 存储引擎在ilist 字段中存放了Position 信息,故可以进行Proximity Search, 而MyISAM 存储引擎不支持该特性。

FTS Document ID 是另外一个重要的概念。在InnoDB 存储引擎中,为了支持全文检索,必须有一个列与word 进行映射,在InnoDB 中这个列被命名为 FTS_DOC_ID, 其类型必须是BIGINT UNSIGNED NOT NULL, 并且InnoDB 存储引擎自动会在该列上加入一个名为FTS_DOC_ ID_ INDEX 的Unique Index 。上述这些操作都由InnoDB 存储引擎自己完成,用户也可以在建表时自动添加FTS_DOC_ID, 以及相应的Unique Index 。由于列名为FTS_DOC_ID 的列具有特殊意义,因此创建时必须注意相应的类型,否则MySQL 数据库会抛出错误。

倒排索引需要将 word 存放到一张表中,这个表称为 Auxiliary Table (辅助表)。在InnoDB 存储引擎中,为了提高全文检索的并行性能,共有 6 张Auxiliary Table, 目前每张表根据 word 的 Latin 编码进行分区

Auxiliary Table 是持久的表,存放于磁盘上。 然而在InnoDB 存储引擎的全文索引中,还有另外一个重要的概念FTS Index Cache(全文检索索引缓存),其用来提高全文检索的性能。FTS Index Cache 是一个红黑树结构,其根据(word, ilist) 进行排序。这意味着插入的数据已经更新了对应的表,但是对全文索引的更新可能在分词操作后还在FTS Index Cache 中, Auxiliary Table 可能还没有更新。InnoDB 存储引擎会批量对Auxiliary Table进行更新,而不是每次插入后更新一次Auxiliary Table 。当对全文检索进行查询时,Auxiliary Table 首先会将在FTS Index Cache 中对应的word 字段合并到Auxiliary Table中,然后再进行查询。 这种merge 操作非常类似之前介绍的Insert Buffer 的功能,不同的是Insert Buffer 是一个持久的对象,并且其是B+ 树的结构。然而FTS Index Cache 的作用又和Insert Buffer 是类似的,它提高了InnoDB 存储引擎的性能,并且由于其根据红黑树排序后进行批量插入,其产生的Auxiliary Table 相对较小。

InnoDB 存储引擎允许用户查看指定倒排索引的 Auxiliary Table 中分词的信息,可以通过设置参数 innodb_ft_aux_table 来观察倒排索引的Auxiliary Table 。例如下面的SQL语句设置查看test数据库下表fts_a的Auxiliary Table:

set global innodb_ft_aux_table='test/fts_a';

设置之后,就可以通过查看information_schema数据库下的innodb_ft_index_table表来得到表fts_a中的分词信息。

设置参数innodb_ft_aux_table,然后在表中查看分词对应的信息:

  • 每个word都对应一个DOC_ID和POSITION
  • 此外,还记录了FITST_DOC_ID、LAST_DOC_ID以及DOC_COUNT。分别代表了该word第一次出现的文档ID、最后一次出现的文档ID、以及该word在多少个文档中存在

对于其他数据库,如Oracle Ilg, 用户可以选择手工在事务提交时,或者固定间隔时间时将倒排索引的更新刷新到磁盘。对于InnoDB 存储引擎而言,其总是在事务提交时将分词写入到 FTS Index Cache, 然后再通过批量更新写入到磁盘。虽然InnoDB 存储引擎通过一种延时的、批量的写入方式来提高数据库的性能,但是上述操作仅在事务提交时发生。

当数据库关闭时,在FTS Index Cache 中的数据库会同步到磁盘上的Auxiliary Table中。然而,如果当数据库发生宕机时,一些FTS Index Cache 中的数据库可能未被同步到磁盘上。那么下次重启数据库时,当用户对表进行全文检索(查询或者插入操作)时,InnoDB 存储引擎会自动读取未完成的文档,然后进行分词操作,再将分词的结果放入到 FTS Index Cache 中。

参数 innodb_ft_cache_size 用来控制 FTS Index Cache 的大小,默认值为32M 。当该缓存满时,会将其中的(word, ilist) 分词信息同步到磁盘的 Auxiliary Table 中。增大该参数可以提高全文检索的性能,但是在宕机时,未同步到磁盘中的索引信息可能需要更长的时间进行恢复。

**文档中分词的插入操作是在事务提交时完成,然而对于删除操作,其在事务提交时,不删除磁盘Auxiliary Table 中的记录,而只是删除FTS Cache Index 中的记录。对于Auxiliary Table 中被删除的记录, InnoDB 存储引擎会记录其FTS Document ID, 并将其保存在 DELETED auxiliary table 中。**在设置参数innodb_ft_aux_table 后,用户同样可以访问information_schema 架构下的表 INNODB_FT_DELETED 来观察删除的FTS Document ID 。

由于文档的DML 操作实际并不删除索引中的数据,相反还会在对应的DELETED表中插入记录,因此随着应用程序的允许,索引会变得非常大,即使索引中的有些数据已经被删除,查询也不会选择这类记录。为此, InnoDB 存储引擎提供了一种方式,允许用户手工地将已经删除的记录从索引中彻底删除,该命令就是OPTIMIZE TABLE 。因为OPTIMIZE TABLE 还会进行一些其他的操作,如Cardinality 的重新统计,若用户希望仅对倒排索引进行操作,那么可以通过参数innodb_optimize_full_text_ only 进行设置(若被删除的文档非常多,那么OPTIMIZE TABLE 操作可能需要占用非常多的时间,这会影响应用程序的并发性,并极大地降低用户的响应时间。用户可以通过参数 innodb_ft_num_word_optimize 来限制每次实际删除的分词数量。该参数的默认值为2000)。

stopword 列表( stopword list)表示该列表中的 word 不需要对其进行索引分词操作。例如,对于the 这个单词,由于其不具有具体的意义,因此将其视为stopword 。InnoDB 存储引擎有一张默认的stopword 列表,其在 information_schema 架构下,表名为INNODB_FT_DEFAULT_STOPWORD, 默认共有36 个stopword 。此外用户也可以通过参数 innodb_ft_server_stopword_table 来自定义stopword 列表。

create table user_stopword(
    value varchar(30)
)ENGINE=InnoDB;
set global innodb_ft_server_stopword_table="test/user_stopword";

当前InnoDB 存储引擎的全文检索还存在以下的限制:

  • 每张表只能有一个全文检索的索引;
  • 由多列组合而成的全文检索的索引列必须使用相同的字符集与排序规则;
  • 不支持没有单词界定符(delimiter) 的语言,如中文、日语、韩语等。
全文检索

MySQL 数据库支持全文检索(Full-Text Search) 的查询,其语法为:

MATCH (coll, col2, . ..) AGAINST (expr [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 指定了使用何种方法去进行查询。下面将对各种查询模式进行详细的介绍。

创建一个表:

create table fts_a(
    FTS_DOC_ID BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
    body TEXT,
    primary key(FTS_DOC_ID),
    fulltext key(body)
);

在真正创建的时候,并不需要显示的创建FTS_DOC_ID。

(1)Natural Language

全文检索通过MATCH 函数进行查询,默认采用Natural Language 模式,其表示查询带有指定word 的文档

例如:查询body 字段中带有Pease的文档,若不使用全文索引技术,则允许使用下述SQL 语句:

mysql> SELECT* FROM fts_a WHERE body LIKE'%Pease%';

显然上述SQL 语句不能使用B+ 树索引。若采用全文检索技术,可以用下面的SQL语句进行查询:

SELECT * FROM fts_a WHERE MATCH(body) AGAINST ('Porridge' IN NATURAL LANGUAGE MODE);

由于NATURAL LANGUAGE MODE 是默认的全文检索查询模式,因此用户可以省略查询修饰符,即上述SQL 语句可以写为:

SELECT* FROM fts_a WHERE MATCH(body) AGAINST ('Porridge');

观察上述SQL语句的查询计划:

在type列显示了fulltext,即表示全文检索的倒排索引,key列显示了body,表示索引的名字。

在where条件中使用MATCH函数,查询返回的结果是根据相关性进行降序排序的,即相关性最高的结果放在第一位。相关性的值是一个非负的浮点数字,0表示没有任何的相关性。根据MySQL官方文档可知,其相关性的计算依据以下4个条件:

  • word是否在文档中出现
  • word在文档中出现的次数
  • word在索引列中的数量
  • 多少个文档包含该word

在上述查询中,由于Porridge在文档2种出现了两次,因而具有更高的相关性,故第一个显示为了统计MATCH函数得到的结果数量,可以执行下面的SQL语句:

select count(*) from fts_a where match(body) against ('Porridge' in natural language mode);

上述SQL语句也可以更改为下面的形式:

select count(if(match(body) against ('Porridge' in natural language mode),1,NULL)) as count from fts_a;

上述两个SQL语句虽然得到的逻辑结果是相同的,但是从内部运行来看,第二句SQL的执行速度更快些。因为第一句SQL语句还需要进行相关性的排序统计,而在第二句SQL中是不需要的。

此外,用户可以通过SQL语句查看相关性:

select fts_doc_id,body,match(body) against ('p') as Relevance from fts_a;

对于InnoDB的全文检索,还需要考虑以下的因素:

  • 查询的word在stopword列中,忽略该字符串的查询
  • 查询的word的字符长度是否在区间[innodb_ft_min_tokennnnnnn_size,innodb_ft_max_token_size]内,不在就忽略

(2)Boolean查询模式

当使用该修饰符时,查询字符串的前后字符会有特殊的含义。Boolean全文检索支持以下几种操作符:

  • +:表示该word必须存在;
  • -:表示该word必须被排除;
  • (no operator):表示该word是可选的,但是如果出现,其相关性会更高;
  • @distance:表示查询的多个单词之间的距离是否在distance之间,distance的单位是字节。这种全文检索的查询也称为Proximity Search。如MATCH(body) AGAINST (‘“Pease pot”@30’ IN BOOLEAN MODE)表示字符串Pease和pot之间的距离需在30字节内;
  • >:表示出现该单词时增加相关性;
  • <:表示出现该单词时降低相关性;
  • ~:表示允许出现该单词,但是出现时相关性为负(全文检索查询允许负相关性);
  • *:表示以该单词开头的单词,如lik*,表示可以是lik、like,或者likes;
  • ‘’:表示短语。

例子:

  • 以上面的fts_a表为例。下面的SQL语句返回有pease又有hot的文档:

    select * from fts_a where match(body) against ('+Pease +hot' in boolean mode);
    
  • 下面的SQL语句返回有pease但没有hot的文档:

    select * from fts_a where match(body) against ('+Pease -hot' in boolean mode);
    
  • 下面的语句返回有pease或有hot的文档:

    select * from fts_a where match(body) against ('Pease hot' in boolean mode);
    
  • 下面的语句进行Proximity Search:

    select fts_doc_id,body from fts_a where match(body) against ('"Pease pot" @30' in boolean mode);
    select fts_doc_id,body from fts_a where match(body) against ('"Pease pot" @10' in boolean mode);
    

    若Pease与pot距离为22字节,第一个语句可以显示出来,第二个语句显示为空。

  • 下面的语句根据是否有单词like或pot进行相关性统计,并且出现单词pot后相关性需要增加。文档4虽然出现两个like单词,但是没有pot,因此相关性没有文档1和文档8高:

    select fts_doc_id,body,match(body) against ('like >pot' in boolean mode) as Relevance from fts_a;
    

  • 下面的查询增加了“<some”的条件,可以看到文档4变为了负,因为其中存在like单词,也存在some单词:

    select fts_doc_id,body,match(body) against ('like >pot <some' in boolean mode) as Relevance from fts_a;
    

  • 接着运行下面的语句,查询文档中以“po”开头的单词:

    select * from fts_a where match(body) against ('po*' in boolean mode);
    
  • 下面的SQL语句查询关于短语,例如:

    • 第一条SQL语句没有使用" "将like和it视为一个短语,而只是将其视为两个单词,因此结果共返回2个文档
    • 第二条SQL语句使用“like it”,因此查询的是短语,故仅文档4符合查询条件

(3)Query Expansion查询模式

这种查询通常在查询的关键词太短,用户需要implied knowledge(隐含知识)时进行

例如,对于单词“database”的查询,用户可能希望查询的不仅仅是包含database的文档,可能还指哪些包含MySQL、Oracle、DB2、RDBMS的单词。而这时可以使用Query Expansion模式来开启全文检索的implied knowledge。

通过在查询短语中添加WITH QUERY EXOANSIONIN NATURAL LANGUAGE MODE WITH QUERY EXPANSION可以开启blind query expansion(又称为automatic relevance feedback)。

该查询分为两个阶段:

  • 第一阶段:根据搜索的单词进行全文索引查询
  • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询

创建一个测试表articles,在这个表中没有显示创建FTS_DOC_ID列,因此InnoDB会自动创建该列,并添加唯一索引,此外,表的全文检索索引是根据列title和body的联合索引

接着根据database关键字进行的全文检索查询。可以看到,查询返回了3条记录,body字段包含database关键字:

接着开启Query Expansion,可以看到最后得到8条结果,除了之前包含database的记录,也有包含title或body字段中包含MySQL、DB2的文档。这就是Query Expansion。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值