哈希索引

    xiaoxiao2022-07-05  153

    哈希索引(hash index)基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。

    哈希索引可细分为静态哈希和动态哈希这两大类,先介绍静态哈希,然后再介绍动态哈希。

    静态哈希

    基于散列技术的文件组织使我们能够避免访问索引结构,同时也提供了一种构造索引的方法。在对散列的描述中,使用桶(bucket)来表示能存储一条或多条记录的一个存储单位。通常一个桶就是一个磁盘块,但也可能大于或者小于一个磁盘块。

    散列索引将散列函数作用于搜索码以确定对应的桶, 然后将此搜索码以及对应的指针存入此桶(或溢出桶)中。

    【散列函数(hash function)】:令 K 表示所有的搜索码值的集合, 令 B 表示所有桶地址的集合,,散列函数 h 是一个从 K 到 B 的函数.。

    插入操作:计算 h(Ki),从而获得存放该记录的桶的地址,并将记录存储到该桶中;查找:计算 h(Ki),然后搜索具有该地址的桶;删除:计算 h(Ki),然后在相应的桶中查找此记录并从中删除它。

    【散列函数的特点】:

    理想的散列函数把存储的码均匀地分布到所有的桶中,使每个桶含有相同数目的记录。分布是均匀的:即散列函数从所有可能的搜索码值集合中为每个桶分配同样数量的搜索码值。分布是随机的:即在一般情况下, 不管搜索码值实际怎样分布, 每个桶应分配到的搜索码值数目几乎相同. 更确切地说, 散列值不应与搜索码的任何外部可见的排序相关, 例如按字母的顺序或按搜索码长度的顺序. 散列函数应该表现为随机的.

    桶溢出

    到目前为止,我们一直假设插入记录时,记录映射到的桶有存储记录的空间。若此时桶已经没有足够的空间,就会发生桶溢出现象。

    【产生原因】:

    桶不足:通数目(用 Nb 表示)的选择必须使 Nb > Nr/fr,其中 Nr 表示将要存储的记录总数,fr 表示一个桶能存放的记录数目。需要注意的是,这种表示是以记录总数已知为前提选择散列函数。偏斜:某些桶分配到的记录比其他桶多,所以即使其他桶仍有空间,某个桶也仍可能溢出,这种情况称为桶偏斜。偏斜发生的原因有两个: 多条记录可能有相同的搜索码;所选的散列函数可能会造成搜索码的分布不均。

    【处理方案】:溢出链。

    桶的数目选为 (Nr / fr) * (1 + d),其中 d 为避让因子,取值一般约为 0.2。相当于增加了桶的数目(若桶的数目不为整数,则向上取整),虽然有一些空间会浪费,但在一定程度上减少了溢出的可能性。

    需要明确的是:尽管分配的桶比所需的桶多一些,但是桶溢出还是可能发生。我们用溢出桶(多出来的那些桶)来处理桶溢出问题。具体过程如下:如果一条记录必须插入桶 b,而桶 b 此时已满,系统会提供另一个溢出桶来存储该记录。如果溢出桶也满了,再提供另一个溢出桶,如此循环。

    一个给定桶的所有溢出桶用一个链表链接在一起,使用这种链表的溢出处理称为溢出链。

    为了处理溢出链, 我们需要将查找算法做轻微的改动。和前面一样,系统使用搜索码上的散列函数来确定一个桶 b,系统必须检查桶 b 中所有的记录,看是否有匹配的搜索码。此外,如果桶 b 有溢出桶,则系统还要检查桶 b 的所有溢出桶中的记录。

    静态哈希的缺点

    静态散列最大的缺点在于必须在实现系统时选择确定的散列函数。此后若被索引的文件变大或缩小,要想再改变散列函数就不容易了。因为散列函数 h 将搜索码值映射到桶地址的固定集合 B 上:

    根据当前文件大小选择散列函数,这样的选择会使得性能随着数据库的增大而下降。换言之,初始时集合 B 太小,一个桶就会包含许多不同的搜索码值的记录,从而可能发生桶溢出。当文件变大时,性能就会受到影响。根据将来某个时刻文件的预计大小选择散列函数。 尽管这样可以避免性能下降,但是初始时会造成相当大的空间浪费。

    虽然我们可以随着文件增大,周期性地对散列结构进行重组。这种重组涉及一系列问题:

    包括新散列函数的选择;在文件中每条记录上重新计算散列函数;分配新的桶。

    重组是一个规模大、耗时的操作,而且重组期间必须禁止对文件的访问。所以我们需要解决的问题就是如何动态地改变桶的数目和散列函数。

    动态哈希

    针对静态散列技术出现的问题,动态散列(dynamic hashing)技术允许散列函数动态改变,以适应数据库增大或缩小的需要,下面介绍可扩充散列(extendable hashing)。

    可扩充散列

    当数据库增大或缩小时,可扩充散列可以通过桶的分裂或合并来适应数据库大小的变化,这样可以保持空间的使用效率。此外,由于重组每次仅作用于一个桶,因此所带来的性能开销较低。

    【做法】:

    选择一个具有均匀性和随机性特性的散列函数 h。此散列函数产生的值范围相对较大,是 b 位二进制整数,一个典型的 b 值是 32。把记录插入文件时按需建桶,用小于等于 b 的 i 个位用作附加桶地址表中的偏移量,i 的值会随着数据库大小的变化而增大或者减少。

    查询操作

    【过程】:查找包含搜索关键字 k 的存储桶。

    计算 h(k) = X;使用 X 的前 i 个高位作为存储区地址表的偏移量,并按指针指向相应的存储区。

    插入操作

    【过程】:插入具有搜索关键字 k 的记录。

    按照查询操作找到存储桶,假设定位到桶编号为 j。如果该存储桶仍然有空间,则在该存储桶中插入记录,否则必须拆分存储桶,并将该桶中现有记录和新记录一起进行重新分配。为了拆分该存储桶,系统必须先根据散列值确定是否需要增加所使用的位数。然后根据 i(global depth)和 ij(local depth)的大小关系进行相应的操作。 如果 i = ij,那么在桶地址表中只有一个指向桶 j 的指针,所以系统需要增加桶地址表的大小。以容纳由于桶 j 分裂而产生的两个桶指针。为了达到这个目的,系统可以将 i 的值加 1,从而使桶地址表的大小加倍。这样,原表中每个表项都被两个表项替代,两个表项都包含和原表项一样的指针。现在桶地址表中有两个表项指向桶 j。系统重新分配一个桶(桶z),并让第二个表项指向此新桶。桶 j 中的各条记录重新散列, 根据前 i 位(i 已经加1)来确定该记录是放在桶 j 中还是放在新创建的桶中。如果 i > ij,那么就有多个表项(指针) 指向桶 j。因此,不需要扩大桶地址表就可以分裂桶 j。指向桶 j 的所有表项的索引前缀的最左 ij 位相同。系统分配一个新桶(桶 z),将 ij 和 iz 置为原 ij 加 1 后得到的值。然后系统需要调整桶地址表中原来指向桶 j 的表项。系统让这些表项的前一半保持原样(指向桶 j),而后一半指向新创建的桶 (桶 z)。然后桶 j 中的各条记录重新被散列,分配到桶 j 或者桶 z 中。

    【示例】:假设现在有如下三条搜索码值,h(k1) = 100100,h(k2) = 000110,h(k3) = 110110。先假设桶的大小为 1,并插入前两项 k1 和 k2,可以通过最左边的位值进行区分,得到如下图所示的结果。

    接着,插入第三项,此时左边第一位已经无法区分了,所以将 i 增加 1,此时刚好可以将三个搜索码进行区分,见下图。

    k1 和 k3 可通过 10 和 11 进行区分,以 0 作为第一位的不需要进行比较,所以 00 和 01 都是指向 k2。

    现在需要插入一条新的搜索码值 h(k4) = 011110。前两位为 01 指向了桶 A,但桶 A 已满,所以需要进行分裂。因为没有更多的搜索码指向桶 A,所以 i 不需要增加,属于 i > ij 的情况,仅需要将桶 A 进行分裂,令 ij + 1,并重新进行分配。

    如果,h(k2) = 010110,也就是说 k2 和 k4 都指向桶 D,并且桶 D 已满,更多的搜索码指向桶 D,属于 i = ij 的情况,所以需要将 i 加 1,用左边的三位来作为桶地址。现在将桶 D 分裂,变成 D’ 和 E,并重新进行散列操作,将 k2 放到 D’ 桶中,k4 放到 E 桶中。

    删除操作

    【过程】:删除一条搜索码值为 k 的记录。

    系统先查找到对应的桶,设为桶 j。系统不仅要把搜索码从桶中删除,还要把记录从文件中删除。如果这时桶为空了,那么也要将桶删除掉。

    需要注意的是,此时某些桶可能合并(就是与具有相同 ij 值和相同 ij -1 前缀的桶合并),桶地址表的大小也可以减半。


    通过上述分析可总结出动态散列的优缺点。

    【优点】:

    随着记录的增加, 动态散列的性能并不会下降;动态散列有着最小的空间开销。

    【缺点】:

    会增加一次额外的查询定位, 因为在查询桶本身之前还需要查找目录来定位桶;存储区地址表本身可能变得很大;更改存储区地址表的大小是一项代价昂贵的操作。

    比较分析

    顺序索引和散列索引的比较分析

    【散列索引】:

    在数据库中查找一个值的效率更高;没有办法利用索引完成排序;在有大量重复值的时候,散列索引的效率也是极低的,因为存在哈希碰撞的情况。

    【顺序索引】:在指出了一个值范围的查询中比散列索引更可取。

    如果存储的数据重复度很低(也就是说基数很大),对该列数据以等值查询为主,没有范围查询、没有排序的时候,特别适合采用散列索引;大多数情况下,会有范围查询,排序、分组等查询特征,就用顺序索引。

    B+ 树索引和哈希索引的比较分析

    【B+ 树】:

    所有的值都是按照顺序存储的,并且每一个叶子页到根的距离相同;适用于全键值、键值范围或者键前缀查找。顺序索引技术在指出了一个值范围的查询中比散列索引更可取。

    【哈希索引】:基于哈希表实现,只有精确匹配索引所有列的查询才有效。

    索引自身只需要存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。缺点: Hash索引能使用范围查询;数据库无法利用Hash索引的数据来避免任何排序运算;Hash索引任何时候都不能避免表扫描。

    Q&A

    问:顺序索引与散列索引的适用场景?

    这两种索引结构都有其优缺点,如果是 select * from a where b=c 这样的定值查询,散列比顺序索引跟适合,顺序索引会随着记录数的增加而性能降低,散列则相对稳定。而对于 where b > c and b < d 这样的范围搜索,则顺序索引更适合,散列的随机特性使得无法定位搜索的桶。所以散列只适合根据搜索码搜索且不是范围查询的场合。

    问:可扩充散列为什么需要局部深度和全局深度?

    可扩展散列允许目录的大小增加或减少,具体取决于插入和删除的数量和种类。

    目录大小更改后,应用于搜索键值的哈希函数也应更改。 因此索引中应该有一些关于要应用哪个哈希函数的信息。 此信息由全局深度提供。

    目录大小的增加不会导致为每个新目录条目创建新存储桶。 每当要拆分由两个或多个目录条目共享的存储桶时,目录大小不需要加倍。这意味着对于每个存储桶,我们需要知道它是由两个或多个目录条目共享。 此信息由存储桶的局部深度提供。(目录:桶地址表)

    总结

    参考

    数据库中的索引技术——哈希索引:https://blog.csdn.net/olizxq/article/details/82313489MySQL BTree索引和hash索引的区别:https://blog.csdn.net/oChangWen/article/details/54024063hash索引和B+tree索引区别:https://www.cnblogs.com/zhidongjian/p/10414129.html
    最新回复(0)