前言
在上篇文章中,我们从数据结构入手,学习了 MySQL 中 InnoDB 对于B-Tree 索引和相关知识。今天让我们来学习 MySQL 中常用的第二种索引 - 哈希索引 吧。
根据官网介绍,以 MySQL 5.7,目前 InnoDB 支持索引并没有哈希索引,仅仅支持 B-Tree 索引和全文索引。真正实现了哈希索引的有 Memory 数据引擎(非唯一索引)和 NDB 集群引擎(支持唯一哈希索引)。但是没关系,如果我们知道了哈希索引的原理,在其他的数据引擎也可以实现一个伪哈希索引。
Let's go。
❤️ 从数据模型(结构)与算法出发
哈希索引 是通过哈希表来实现的,同时也可以被称为散列表。散列表其实不是单单指一种数据结构,其实它是由散列表和散列函数组合实现的。
哈希表,也可以叫散列表,是一种可以根据关键码值可以直接进行访问的数据结构。换句话,就是可以直接根据关键码值映射到散列表中的一个位置来访问记录。一般来说,实现散列表的数据结构是数组。
哈希算法,也可以叫散列算法,它是一种非常常见的算法。它的计算过程是:把任意长度的输入(输入内容也被称为“关键字”),通过散列算法之后,返回固定长度的输出,得到的结果就是散列值。
假设有一个哈希函数 f(a),我们传参进行调用并拿到结果,如下表格
| 输入 a | 结果输出 |
|---|---|
| 12 | C20AD4D76FE97759AA27A0C99BFF6710 |
| 324 | F2FC990265C712C49D51A18A32B39F0C |
| 1256 | E3251075554389FE91D17A794861D47B |
整个过程并不难理解,我们只需要负责输入就可以拿到了输出。
这个哈希算法函数的实现,因为它是需要根据上面的因素来进行制定一个函数。一般来说编写一个散列函数需要考虑以下因素:
- 散列表的大小,或者说是散列函数计算出来的散列值处于的大小区间。散列表的大小是空间换时间的典型体现,用足够的空间来换取更快的查找速度。当散列表的空间很大的时候,散列算法可以减少哈希值的冲突,那么所有元素的查找操作理想状态下时间复杂度 O(1)。但是一般是无法做到非常巨大的,只能根据情况在时间和空间之间进行权衡。
- 哈希冲突。既然是在固定范围内,随着数据量越来越大,肯定会有不同的元素得到了相同的哈希值,所以哈希算法需要考虑如何解决这个冲突问题。哈希冲突的处理方式一般有
拉链法、线性探测法. - 结果均匀分布。需要尽量将元素均匀分布在这个范围之内,当空间充足的话且在理想状态下,每一个元素置于一个槽位,加快检索速度,同时避免过度冲突失去了散列表的优势。
- 关键字的长度。如果关键字太长的话,可能对散列计算所需时间造成影响,所以简短,具备意义的关键字是非常重要的。
所以我介绍几种 Hash 函数,看下设计思路:
- 直接寻址法。取关键字或关键字的某个线性函数值为散列地址。
- 数字分析法。根据关键字特征来分析,尽量截取数据中各关键字重复或相似频率较低的部分作为关键字。
- 平方取中法。取关键字平方后的中间几位作为散列地址。
- 随机数法。使用随机函数,取随机值作为其哈希值。
- 除留余数法。取关键字被一个不长于散列表长度的数 P 除之后取余数作为哈希值
为了降低哈希冲突的概率,一般来说采用不同的 Hash 算法进行多次 Hash 运算,所以上面的思路可以进行混合使用。
常见的现有散列函数有:
| 名称 | 说明 |
|---|---|
MD4 |
适用在32位字长的处理器上用高速软件实现——它是基于 32位操作数的位操作来实现的 |
MD5 |
MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以 512 位分组,其输出是 4 个 32 位字的级联,与 MD4 相同。MD5 比 MD4 来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好 |
SHA-1 及其他 |
SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于 264 的输入,产生长度为 160bit 的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。 |
无论如何构造哈希函数,冲突是无法完全避免的。
关于解决哈希冲突
下面处理哈希冲突的两种方法
拉链法
拉链法的思想是不同的关键字在哈希函数中计算出了相同的哈希值,意味它们会在哈希表上处于同一个槽位。发生这种冲突的话,同个槽位的元素以链表的形式联结。
当发生查询操作的时候,会先计算指定的关键字的哈希值,然后拿到链表,再使用查找算法寻找是否指定的关键字存在。
拉链法的优势在于,它在插入的时候,只需要修改一下指针即可(因为链表通过引用连接),不需要像开放定址法那样,寻找空闲地方。而缺点在于如果哈希函数没有很好地解决冲突问题,则会导致链表结点过长,空间浪费,性能降低。
开放定址法
开放地址法的思想和拉链法不同在于,当某关键字的哈希值的发生了冲突,它就会沿着数组往下一位进行查找,直到找到空闲的位置,然后就将关键字赋值。
开放地址法的优势在于,这种方法使槽位的盈满度更高,排列更加紧凑,不会因为冲突使同一个槽位聚集结点过多而导致性能下降。
❤️ Hash (哈希)索引
上面讲完了对哈希表的描述之后,或许大家都能猜得到所谓的哈希索引其实就是通过哈希表与索引的结合,通过哈希算法来达到一次计算即可定位到数据所在位置,效率还是蛮高的。
哈希索引的文件结构中仅仅包含哈希码和指针,不存储数据字段值。在哈希索引的过程中,MySQL 会先计算 where 条件值的哈希值,然后该值寻找对应的记录指针,根据指针找到数据行后,最后一步是比较是否是目标数据行。
创建一个 Hash 索引
通过建表进行创建哈希索引(数据引擎为 Memory)
create table user(
firstname varchar(100) not null default '',
lastname varchar(100) not null default '',
index using hash(firstname)
) engine=memory
通过 ALTER
ALTER TABLE user ADD COLUMN firstname int unsigned NOT NULL DEFAULT 0;
插入数据
insert into user (firstname,lastname ) values ('zhang','san'),('li','si'),('wang','wu');
查看表结构
desc user;
我们通过 desc 字段查看是否优化使用上了索引
DESC select * from user where firstname = 'zhang'
输出结果为
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | simple | user | ref | firstname | firstname | 102 | const | 2 | Using where |
而使用非索引列
DESC select * from user where lastname = 'san';
输出结果为
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | user | ALL | null | null | null | null | 3 | Using where |
创建自定义哈希索引
开头我们已经讲过了,并不是所有的数据引擎都支持哈希索引。但是我们可以通过上面对哈希表的了解来建立一个“伪”哈希索引。例如在 InnoDB 上面的 B-Tree 索引上创建哈希索引的话,最终是在 B-Tree 树上进行检索数据的,不同的是,在 B-Tree 上面检索的不是被设为索引的列值而是使用了哈希值。
假设你有个表,需要大量存储 URL,并且需要对 URL 进行查找。表 url 表结构如下
| 字段 | 说明 |
|---|---|
| id | 主键标识 |
| url | 路径 |
如果我们对上表进行查找,SQL 是这样的
SELECT id FROM url where url = "http://www.mysql.com";
一般来说,url 不会像上面例子这么短的,通常后面会附带许多信息例如 token/jessessionid等等。所以我们可以利用索引的特点,输入任意长度的关键字,哈希函数输出固定长度的结果。这样固定的长度结果可以较少数据对比,有利于加快查找速度。
我们在上表结构改造成这样
| 字段 | 说明 |
|---|---|
| id | 主键标识 |
| url | 路径 |
| url_crc | url 经过哈希函数处理返回哈希值 |
我们插入数据的时候,使用的是 CRC32 做哈希函数,使用下面方式进行查询
SELECT id FROM url WHERE url = "http://www.mysql.com" AND url_crc = CRC32("http://www.mysql.com")
这样查询性能非常高,因为 MySQl 优化器会使用这个选择很高而体积很小的基于 url_crc 列的索引来完成查找。(上述案例中,索引值为 1560514994)。
及时有多条相同的索引值,查找仍然很快,只需要根据哈希值做快速的整数比较就能找到索引条目,然后比较返回。另一种方式是直接通过 url 做索引,但是 url 不是定长而且一般是大部分内容都是字符串,这样会拖慢查询速度。
自定义制作索引的缺点是需要维护哈希值。维护的方式有:1. 手动维护 2. 触发器。下面演示触发器怎么在插入和更新是维护 url_crc 列。
首先创建表
CREATE TABLE pseudohash (
id int UNSIGNED NOT NULL auto_increment,
url VARCHAR(255) NOT NULL,
url_crc int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY(id)
)
表创建好后,创建一下触发器。
// 创建触发器,在插入的时候进行更新
DELIMITER
CREATE TRIGGER `pseudohash_crc_ins` BEFORE INSERT ON `pseudohash`
FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url_crc);
END;
// 创建触发器,在更新的时候进行更新
DELIMITER
CREATE TRIGGER `pseudohash_crc_ins` BEFORE UPDATE ON `pseudohash`
FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url_crc);
END;
然后验证是否触发器生效,分别进行插入和更新操作。
//插入数据
INSERT INTO pseudohash (url) VALUES ('http://www.mysql.com');
//查看结果
SELECT * from pseudohash;
//更新数据
UPDATE pseudohash SET url='http://www.mysql.com' WHERE id = 1;
//查看结果
SELECT * from pseudohash;
上面已经介绍了如何创建一个自定义列,但是别忘了,哈希函数不仅仅是将关键字变成哈希值,它还要处理哈希冲突的问题。crc32 函数在大数据量的表中,会出现大量的哈希冲突。
❤️ Hash (哈希)索引的限制及 B-Tree 索引和 Hash (哈希)索引的对比
虽然 Hash 索引效率高,但是 Hash 索引本身由于其特殊性也带来了很多限制和弊端。
下面表格是关于哈希索引的限制及其与 B-Tree 索引的一些对比。
| 方面 | 哈希索引 | B-Tree 索引 |
|---|---|---|
| 支持查询类型 | 仅仅能满足等值查询例如=/IN/<=>,不能使用范围查询 |
支持范围查询,支持等值查询 |
| 排序问题 | 无法利用索引的数据来避免任何排序运算 | 支持索引排序,例如 Order By、Group By 等 |
| 联合索引匹配问题 | 不支持多列联合索引的最左匹配规则 | 支持多列联合索引的最左匹配规则 |
| 表扫描 | 在任何时候都不能避免表扫描 | 由于数据结构缘由,支持局部扫描 |
| 扫描次数 | 最快仅需要一次 Hash 计算即可找到对应的 ROWID | 从根节点到子节点,可能需要多次磁盘 IO 操作才拿得到 ROWID,然后根据 ROWID 去找到挂载其上的数据块数据 |
| 数据引擎 | Memory 和 NDB 集群引擎 支持 | InnoDB 和 MyISAM |
| 适用场景 | 适用于等值类型的快速查找(精确匹配索引所有列的查询) | 适合范围查找,适合用于全键值、键值范围、或者键前缀查找 |