目录
一、B-Tree 索引
二、哈希索引
三、空间数据索引(R-Tree)
四、全文索引
五、其他索引类型
索引有很多种类型,为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现。不同存储引擎的索引其工作方式并不一样。也不是所有存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层实现也可能不同。
我们通过提到索引时,多半说的都是 B-Tree 索引,使用 B-Tree 数据结构来存储数据。大多数 MySQL 引擎都支持这种索引。之所以称之为“B-Tree” 是因为 MySQL 在创建表和其他语句中也使用该关键字。不过,底层的存储引擎也可能使用不同的存储结构,例如:InnoDB 则使用 B+Tree。存储引擎以不同的方式使用 B-Tree 索引,性能也各有不同,各有优劣。例如,MyISAM 使用前缀压缩技术使得索引更小,但 InnoDB 则按照原数据格式进行存储。再如 MyISAM 索引通过数据的物理位置引用被索引的行,而 InnoDB 则根据主键引用被索引的行。
【B-Tree(多路搜索树)】:通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。如下图:展示了 B-Tree 索引的抽象表示,大致反映了 InnoDB 索引是如何工作的。InnoDB 的叶子节点称为叶子页,大小为 16K。
B-Tree 索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针指向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。
叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页(不同引擎的“指针”类型不同)。如下图,绘制了一个节点和其对应的叶子节点,其实在跟节点和叶子节点之间可能有很多节点页,树的深度和表的大小直接相关。B-Tree 对索引列是顺序组织存储的,所以很适合查找范围数据。例如下图,基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的,所以像“找出所有以A到C开头的名字”这样的查询效率会非常高。假设数据表信息如下:
CREATE TABLE People(
last_name VARCHAR(50) NOT NULL,
first_name VARCHAR(50) NOT NULL,
birthday DATE NOT NULL,
gender ENUM('m','f') NOT NULL,
KEY(last_name,first_name,birthday)
);
对于表中的每一行数据,索引中包含 last_name,first_name 和 birthday列的值,如下图表示索引是如何组织数据的存储的。索引对多个值进行排序的依据是 CREATE TABLE 语句中定义索引时列的顺序,看一下最后两个条目,两个人的姓和名都相同时,则根据他们的出生日期来排列顺序。
可以使用 B-Tree 索引的查询类型。B-Tree 索引使用于全键值、范围键值或键前缀查找(值where条件)。其中键前缀查找只适用于根据最左前缀的查找。前面所述的索引对如下类型的查询有效:
【1】全值匹配:和索引中的所有列进行匹配,例如前面提到的索引可用于查找姓名为 Cuba Allen、出生于 1960-01-01 的人。
【2】匹配最左前缀:前面提到的索引可用于查找所有姓为 Allen 的人,即只使用索引的第一列。
【3】匹配列前缀:也可以只匹配某一列的值的开头部分。例如前面提到的索引可用于查找所有以 A 开头姓的人。这里也只使用了索引的第一列。模糊查询以常量开头,那么可以使用上索引。
【4】匹配范围值:例如前面提到的索引可用于查找姓在 Allen 和 Barrymore 之间的人。这里也只使用了索引的第一列。
【5】精准匹配某一列并范围匹配另外一列:前面提到的索引也可用于查找姓为 Allen,并且名字是字母 K 开头的人。即第一列 last_name 全匹配,第二列 first_name 范围匹配。
【6】只访问索引的查询:B_Tree 通常可以支持 “只访问索引的查询”,即查询只需要访问索引,而无需访问数据行。称之为“覆盖索引” 的优化。
因为索引树中的节点是有序的,所以除了按值查找之外,索引还可以用于查询中的 ORDER BY 操作(按顺序查找asc)。一般来说,如果 B-Tree 可以按照某种方式查找到值,那么也可以按照这种方式用于排序。所以,如果 ORDER BY 子句满足前面列出的集中查询类型,则这个索引也可以满足对应的排序需求。
【B-Tree 索引的限制】:【1】如果不是按照索引的最左前缀开始查找,则无法使用索引。例如上面例子中的索引无法用于查找名字为 Bill 的人,也无法根据生日查找,因为这两列都不是最左数据列。也无法查找姓以某个字母结尾的人。
【2】不能跳过索引中的列:也就是说,前面所述的索引无法用于查找姓为 Smith 并且在某个特定日期出生的人。如果不指定名(first_name),则 MySQL 只能使用索引的第一列。
【3】如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查询。例如有查询 WHERE last_name LIKE 'A%' AND first_name = 'Cuba',这个查询只能使用索引的第一列,因为 LIKE 是个范围查询。
所以,索引列的顺序是很重要的,上面的限制都和索引列的顺序有关。在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。也有些限制并不是 B-Tree 本身导致的,而是 MySQL 优化器和存储引擎使用索引的方式导致的。这部分限制在未来的版本中可能就不再是限制了。
哈希索引(hash index)是基于哈希表实现的,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也是不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。
【MySQL 中】:只有 Memory 引擎显示支持哈希索引。这也是 Memory 引擎表的默认索引类型,Memory 引擎同时也支持 B-Tree 索引。值得一提的是,Memory 引擎是支持非唯一哈希索引的,这在数据库世界里面是比较与众不同的。如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。
CREATE TABLE People(
last_name VARCHAR(50) NOT NULL,
first_name VARCHAR(50) NOT NULL,
KEY USING HASH(last_name)
)ENGINE=MEMORY;
表中包含的数据如下:
假设索引使用假设的哈希函数f(),它返回下面的值(伪造数据):
f('Allen')= 1223
f('Peter')= 8493
f('Baron')= 3490
则哈希索引的数据结构如下:索引(hash值,指针),每个槽的编号是顺序的,但是数据行不是。
槽(Slot) | 值(Value) |
1223 | 指向第1行的指针 |
3490 | 指向第3行的指针 |
8493 | 指向第2行的指针 |
【举个栗子】:进行如下查询:
SELECT last_name FROM People WHERE last_name='Peter';
MySQL 先计算 'Peter' 的哈希值,并使用该值寻找对应的记录指针。因为 f('Peter')=8493,所以对 MySQL 在索引中查找 8493,可以找到指向第二行的指针,最后一步是比较第二行的值是否为'Peter',以确保就是要查找的行。因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有它的限制:
【1】哈希索引只包含哈希值和指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。
【2】哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。
【3】哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列(A,B)上建立索引,如果查询只使用A,则无法使用该索引。是不遵循最左前缀的思想。
【4】哈希索引只支持等值查询,也不支持任何范围查询。
【5】访问哈希索引的数据非常快,除非有很多哈希冲突。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。
【6】如果哈希冲突很多的话,一些索引维护操作的代价也会很高。
因为上述限制,哈希索引只适用于某些特定的场合。而一旦适合哈希索引,则它的性能会将非常显著。除了 Memory 引擎外,NDB 集群引擎也支持唯一哈希索引。InnoDB 引擎有一个特殊的功能叫做“自适应哈希索引(adaptive hash index)” 当InnoDB 注意到某些索引值被使用得非常频繁时,它会在内存中基于 B-Tree 索引之上再创建一个哈希索引,这样就使 B-Tree 索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动化的、内部的行为,用户无法控制或者配置,不过该功能可以关闭。
【创建自定义哈希索引】:如果存储引擎不支持哈希索引,则可以模拟像 InnoDB 一样创建哈希索引。思路很简单:在 B-Tree 基础上创建一个伪哈希索引,这和真正的哈希索引不是一回事,因为还是使用 B-Tree 进行查找,但是使用 Hash值进行查找而非键值本身。只需要在 WHERE 子句中手动指定使用哈希函数。
【举个栗子】:例如表中存储了大量的 URL,并需要根据URL 进行搜索查询。如果使用 B-Tree 来存储 URL,存储的内容就会很大,因为 URL本身就很长。若在原有的表中,新增一个被索引的 url_crc列(使用CRC32 对 URL 进行哈希)。使用 CRC32 做哈希就可以使用如下方式查询:性能会提升很多,因为 MySQL 优化器会使用选择性高而体积小的 url_crc 列的索引来查询。
SELECT id FROM url WHERE url="http://www.mysql.com" AND url_crc=CRC32("http://www.mysql.com");
上述的缺点是需要维护哈希值。可以手动维护,也可以使用触发器实现。如下:先临时修改一下语句分隔符,这样就可以在触发器定义中使用分号;
DELIMITER //
CREATE OR REPLACE TRIGGER People_insert_trigger BEFORE INSERT ON People FOR EACH ROW BEGIN
SET NEW.url_cc=CRC32(NEW.url);
END;
//
DELIMITER ;
记住不要使用 SHA1() 和 MD5() 作为哈希函数。因为这两个函数计算出来的哈希值是非常长的字符串,会浪费大量空间,比较时也会更慢。SHA1() 和 MD5() 是强加密函数,设计目的是最大限制消费冲突,但这里并不需要这么高的要求,简单哈希函数的冲突在一个可以接受的范围,同时又能够提供更好的性能。如果数据表非常大,CRC32() 会出现大量的哈希冲突,则可以考虑自己实现一个简单的 64位哈希函数。这个自定义函数要返回整数,而不是字符串。一个简单的办法可以使用 MD5() 函数返回值的一部分作为自定义哈希函数。这可能比自己写一个哈希算法的性能要差。
【处理哈希冲突】:当使用哈希索引进行查询的时候,必须在 WHERE 子句中包含常量值。CRC32() 返回的是32位的整数,当索引有93,000 条记录时出现冲突的概率是 1%。所以,避免哈希冲突的办法就是在 WHERE 条件中带入列值。或者使用 FNV64()函数,这是移植 Percona Server 的函数,可以以插件的方式在任何 MySQL版本中使用,哈希值为 64位,速度快,且冲突比CRC32() 要少很多。
SELECT id FROM url WHERE url="http://www.mysql.com" AND url_crc=CRC32("http://www.mysql.com");
MyISAM 表支持空间索引,可以用作地理数据存储。和B-Tree 索引不同,这类索引无需前缀查询。空间索引会从所有维度来索引数据。查询时,可以有效地使用任意维度来组合查询。必须使用 MySQL 的 GIS 相关函数如 MBRCONTAINS() 等来维护数据。MySQL 的 GIS 支持并不完善,所以大部分人都不会使用这个特性。开源关系数据库系统中对 GIS 的解决方案做得比较好的是 PostgreSQL 的 PostGIS。
全文索引时一种特殊类型的索引,他查找的是文本中的关键词,而不是直接比较索引中的值。全文搜索和其他几类索引的匹配方式完全不一样。他有许多需要注意的细节,如停用词、词干和复词、布尔搜索等。全文索引更类似 solr这种搜索引擎,而不是简单的 WHERE 条件匹配。同时在列上创建全文索引和基于值的 B-Tree 索引不会有冲突,全文索引适用于 MATCH AGAINST 操作,而不是普通的 WHERE 条件操作。
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body)
) ENGINE=InnoDB;
【全文索引的使用】: 通过在 title和body 两个字段中查找含有 ‘database’ 内容的行。
SELECT * FROM articles
WHERE MATCH (title,body)
AGAINST ('database' IN NATURAL LANGUAGE MODE);
IN NATURAL LANGUAGE MODE:默认的搜索形式(不加任何搜索修饰符或者修饰符为 IN NATURAL LANGUAGE MODE 的情况)。
特点:【1】对于搜索字符串中的字符都解析为正常的字符,没有特殊意义;
【2】对屏蔽字符列表中的字符串进行过滤;
【3】当记录的选择性超过50%的时候,通常被认为是不匹配;
【4】返回记录按照记录的相关性进行排序显示;IN BOOLEAN MODE:布尔模式搜索(搜索修饰符为IN BOOLEAN MODE的情况)。
特点:会按照一定的规则解析搜索字符串中的特殊字符的含义,进行一些逻辑意义的规则。如:某个单词必须出现,或者不能出现等。这种类型的搜索返回的记录是不按照相关性进行排序的。WITH QUERY EXPANSION:一种稍微复杂的搜索形式,实际上是进行了2次自然搜索,可以返回记录直接简介性关系的记录,修饰词IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION 或者WITH QUERY EXPANSION modifier。
特点:这种类型的搜素,实际上提供了一种间接的搜索功能,比如:我搜索某个词,而且返回的第一行中却不包含搜索词中的任意字符串。可以根据第一次搜索结果的记录词进行第二次匹配,从而可能找到一些间接关系的匹配记录。
还有第三方的存储引擎使用不同类型的数据结构来存储索引。例如 TokuDB 使用分型树索引(fractal tree index),这是一类较新开发的数据结构,既有 B-Tree 的很多优点,也避免了 B-Tree 的一些缺点。
----关注公众号,获取更多内容----