MySQL之创建高性能的索引(三)

创建高性能的索引

哈希索引

哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有的列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code).哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时哈希表中保存指向每个数据行的指针。在MySQL中,只有Memory引擎显式支持哈希索引。这也是Memory引擎表的默认索引类型,Memory引擎同时也支持B-Tree索引。值得一提的是,Memory引擎是支持非唯一哈希索引的,这在数据库世界里面是比较与众不同的。如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中.下面来看一个例子

sql 复制代码
mysql> CREATE TABLE testhash(
    -> fname VARCHAR(50) NOT NULL,
    -> lname VARCHAR(50) NOT NULL,
    -> KEY USING HASH(fname)
    -> ) ENGINE=MEMORY;

表种包含如下数据:

sql 复制代码
mysql> SELECT * FROM testhash;
+-------+-----------+
| fname | lname     |
+-------+-----------+
| Arjen | Lentz     |
| Baron | Schwartz  |
| Peter | Zaitsev   |
| Vadim | Tkachenko |
+-------+-----------+

假设索引使用假想的哈希函数f(),它返回下面的值(都是示例数据,非真实数据):

c 复制代码
f('Arjen')=2323
f('Baron')=7437
f('Peter')=8784
f('Vadim')=2458

则哈希索引的数据结构如下:

c 复制代码
槽(Slot)      值(Value)
2323          指向第1行的指针
2458          指向第4行的指针
7437          指向第2行的指针
8784          指向第3行的指针

注意每隔槽的编号是顺序的,但是数据行不是。先在来看如下查询

sql 复制代码
mysql> SELECT lname FROM testhash WHERE fname ='Peter';

MySQL先计算'Peter'的哈希值,并使用该值寻找对应的记录指针。因为f('Peter')=8784,所以MySQL在索引中查找8784,可以找到指向第3行的指针,最后异步是比较第三行的值是否为'Peter',以确保就是要查找的行。因为索引自身只需要存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。然而,哈希索引也有它的限制:

  • 1.哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显
  • 2.哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序
  • 3.哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列(A,B)上建立哈希索引,如果查询只有数据列A,则无法使用该索引
  • 4.哈希索引只支持等值比较查询,包括=、IN()、<=>(注意<>和<=>是不同的操作)。也不支持任何范围查询,例如WHERE price >100
  • 5.访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,主键进行比较,直到找出所有符合条件的行
  • 6.如果哈希冲突很多的化,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。

因为这些限制,哈希索引只适用于某些特定的场合。而一旦适合哈希索引,则它带来的的性能提升将非常显著。举个例子,在数据仓库应用中有一种经典的"星型"schema。需要关联很多表,哈希索引就非常适合查找表的需求。除了Memory引擎外,NDB集群引擎也支持唯一哈希索引,且在NDB集群引擎中作用非常特殊。

InnoDB引擎有一个特殊的功能叫作"自适应哈希索引(Adaptive Hash Index)"。当InnoDB注意到某些索引值被使用得非常频繁时,它会在内存中基于B-Tree之上再创建一个哈希索引,这样就让B-Tree索引也具有哈希索引的一些优点,比如快速的哈希查找。这是一个完全自动的、内部的行为,用户无法控制或者配置,不过有必要,完全可以关闭该功能。

创建自定义哈希索引

如果存储引擎不支持哈希索引,则可以模拟像InnoDB一样创建哈希索引,这可以享受一些哈希索引的便利,例如只需要很小的索引就可以为超长的键创建索引。思路很简单:在B-Tree基础上创建一个伪哈希索引。这和真正的哈希索引不是一回事,因为还是适用B-Tree进行查找,但是它适用哈希值而不是键本身进行索引查找。你需要做的事就是在查询的WHERE子句中手动指定适用哈希函数。下面是一个示例,例如需要存储大量的URL,并需要根据URL进行搜索查找。如果适用B-Tree来存储URL,存储的内容就会很大,因为URL本身都很长。正常情况下会有如下查询:

sql 复制代码
mysql> SELECT id FROM url WHERE url = "http://www.mysql.com";

若删除原来URL列上的索引,而新增一个被索引的url_crc列,适用CRC32做哈希,就可以适用下面的方式查询:

sql 复制代码
mysql>SELECT  id FROM url WHERE url="http://www.mysql.com" AND url_crc=CRC32("http://www.mysql.com");

这样做的性能会非常高,因为MySQL优化器会适用这个选择性很高而提及很小的基于url_crc列的索引来完成查找(在上面的案例中,索引值伪1560514994)即使有多个记录相同的索引值,查找仍然很快,只需要根据哈希值做快速的整数比较就能找到索引条目,然后一一比较返回对应的行。另外一种方式就是对完整的URL字符串做索引,那样会非常慢。

sql 复制代码
mysql> SELECT CRC32("http://www.mysql.com");
+-------------------------------+
| CRC32("http://www.mysql.com") |
+-------------------------------+
|                    1560514994 |
+-------------------------------+
1 row in set (0.10 sec)

这样实现的缺陷事需要维护哈希值。可以手动维护,也可以适用触发器实现。下面的案例演示了触发器如何在插入和更新时维护url_crc列,首先创建如下表:

sql 复制代码
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 ));

然后创建触发器。先临时修改一下语句分隔符,这样就可以在触发器定义中适用分毫:

sql 复制代码
DELIMITER //
CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;
//

CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
END;
//

DELIMITER;

剩下工作就是验证一下触发器如何维护哈希索引:

sql 复制代码
mysql>INSERT INTO pseudohash (url) VALUES ('http://www.mysql.com');
mysql> SELECT * FROM pseudohash;
+----+----------------------+------------+
| id | url                  | url_crc    |
+----+----------------------+------------+
|  1 | http://www.mysql.com | 1560514994 |
+----+----------------------+------------+
1 row in set (0.08 sec)
mysql> UPDATE pseudohash SET url = 'http://www.mysql.com/' WHERE id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM pseudohash;
+----+-----------------------+------------+
| id | url                   | url_crc    |
+----+-----------------------+------------+
|  1 | http://www.mysql.com/ | 1558250469 |
+----+-----------------------+------------+
1 row in set (0.07 sec)

SHA1()和MD5()

如果采用这种方式,记住不要适用SHA1()和MD5(0作为哈希函数。因为这两个函数计算出来的哈希值是非常长的字符串,会浪费大量空间,比较时也会更慢。SHA1()和MD5()是强加密函数,设计目标是最大限度消除冲突,但这里并需要这样高的要求。简单哈希函数的冲突在一个可以接受的范围,同时又能够提供好的性能。如果数据表非常大,CRC32()会出现大量的哈希冲突,则可以考虑自己实现一个简单的64位哈希函数。这个自定义函数要返回整数,而不是字符串。一个简单的办法可以适用MD5()函数返回值的一部分来作为自定义哈希函数。这可能比自己写一个哈希算法的性能要差,不过这样实现最简单:

sql 复制代码
mysql> SELECT CONV(RIGHT(MD5('http://www.mysql.com/'), 16), 16, 10) AS HASH64;
+---------------------+
| HASH64              |
+---------------------+
| 9761173720318281581 |
+---------------------+

处理哈希冲突,当适用哈希索引进行查询的时候,必须在WHERE子句中包含常量值:

sql 复制代码
mysql> SELECT id FROM url WHERE url_crc=CRC32('http://mysql.com') AND url='http://www.mysql.com';

一旦出现哈希冲突,另一个字符串的哈希值也恰是1560514994,则下面的查询时无法正确工作的。

sql 复制代码
mysql> SELECT id FROM url WHERE url_crc=CRC32('http://www.mysql.com');

因为所谓的"生日悖论",出现哈希冲突的概率的增长速度可能比想象的要快得多。CRC32()返回的是32位的整数,当索引有93 000条时出现的冲突的概率是1%。例如将/usr/share/dict/words中的词导入数据表并进行CRC32()计算,最后会有98 569行,这就已经出现一次哈希冲突了,冲突让下面的插叙年返回了多条记录

sql 复制代码
mysql>SELECT word,crc FROM words WHERE crc=CRC32('gnu');

正确的刑法应该如下:

sql 复制代码
mysql>SELECT word, crc FROM words WHERE crc=CRC32('gnu') AND word = 'gnu';

要避免冲突问题,必须在WHERE条件中带入哈希值和对应列值。如果不是想查询具体值,例如只是统计记录数(不精确的),则可以不带如列值,直接适用CRC32()的哈希值查询即可。还可以适用如FNV64()函数作为哈希函数,这是移植自Percona Server的函数,可以以插件的方式在任何MySQL版本中适用,哈希值64位,速度快,且冲突比CRC32()要少很多

相关推荐
用户6279947182624 分钟前
南大通用GBase 8c分布式版本gha_ctl 命令-HI参数详解
数据库
斯汤雷13 分钟前
Matlab绘图案例,设置图片大小,坐标轴比例为黄金比
数据库·人工智能·算法·matlab·信息可视化
腥臭腐朽的日子熠熠生辉18 分钟前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
ejinxian19 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
SQLplusDB20 分钟前
Oracle 23ai Vector Search 系列之3 集成嵌入生成模型(Embedding Model)到数据库示例,以及常见错误
数据库·oracle·embedding
杉之25 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
喝醉酒的小白40 分钟前
SQL Server 可用性组自动种子设定失败问题
数据库
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
chem41111 小时前
Conmon lisp Demo
服务器·数据库·lisp
俏布斯1 小时前
算法日常记录
java·算法·leetcode