数据库 CMU15-455 学习笔记四(Hash Tables)

338 阅读6分钟

Hash Tables

之前的课程学习了Disk Manager和Buffer Pool Manager,接下来开始学习Access Methods.

Access Methods的主要功能就是支持DBMS的执行引擎去从Page中读取或者写入文件。

主要有两种类型的数据结构:

  1. Hash Tables(无序)
  2. Trees(有序)
image.png

Hash Table实现了可以将任意key映射到具体value的无序关联数组。对于一个给定的key, 他会使用hash函数来计算key在该数组中的offset,然后根据offset就可以拿到想要的value。

对于Hash Table, 空间复杂度是O(n), 时间复杂度 平均是O(1), 最差是O(n)

image.png

Hash Table中的两个重要概念

  1. Hash Function: 如何把大范围的key映射到一个小区间,并同时权衡好Hash速率和冲突率。

  2. Hashing Scheme: 如何处理好hashing之后的冲突,并权衡好hash table的空间和额外的插入/获取操作

image.png

Hash Functions

哈希函数就是给定一个key,然后返回一个代表这个key的integer。我们理想的哈希函数就是返回结果很快且哈希冲突也很小。 以下是一些常见的哈希函数。

image.png

Static Hash Schemes

静态hash schemes的hash表大小是固定的,当hash表被用完,则会进行扩容和重建。

对于静态hash schemes,处理hash冲突的方法有两种:

  1. Linear Probe Hashing
  2. Cuckoo Hashing image.png

Linear Probe Hashing

Linear Probe Hashing(线性探测哈希)是一种解决哈希冲突的方法。它属于开放地址法(Open Addressing)的一种形式。在Linear Probe Hashing中,当发生哈希冲突时,即两个键映射到同一个桶(bucket)时,线性探测哈希会顺序地探测下一个可用的桶,直到找到一个空闲的桶或达到哈希表的末尾。具体步骤如下:

  1. 使用哈希函数将键映射到哈希表的初始位置。
  2. 如果该位置为空闲,则将键存储在该位置。
  3. 如果该位置已被占用,则继续顺序地检查下一个位置,直到找到一个空闲的位置。
  4. 如果达到哈希表的末尾仍未找到空闲位置,则可以绕回到哈希表的起始位置,继续线性探测。
  5. 重复步骤2-4,直到成功地插入键或确定键不存在。

线性探测哈希的主要优点是简单而高效,不需要额外的数据结构来处理冲突,且内存访问连续性较好。然而,它容易引起聚集现象(clustering),即相同的哈希值的键倾向于聚集在一起,导致查找性能下降。此外,删除操作也稍显复杂,因为删除一个键后,后续的线性探测链可能会被打断。

插入

如下图所示,插入E时,发现hash值的位置已经被A占用,然后一直探测下一个位置,发现都被C,D,E占用,最终放到了E的下一个bluck中.

image.png

删除

在删除元素时,如果直接把对应的bluck给清空,比如把C给清理掉,那么就会导致找不到D和E,因为到C 系统就以为结束了,不会继续往下探测,解决这个问题有两个方法

Movement

该方法会把keys都重新rehash直到找到第一个空的slot, 这个方法代价很高,可能会重新组织整个table, 没有DBMS会这么做 image.png

Tombstone

该方法会在被删除的slot位置设置一个标记,表示该slot的数据已经被逻辑上删除了。如果有新的key插入,可以放到这个slot中。用这个方法需要定期的垃圾清理。 image.png

Non-Unique Keys

对于特殊场景,比如一个key对应多个value的非唯一key场景, 主要有两个方法

  1. 每个key都对应单独的列表来存储value
  2. 存储冗余的元素,大数据系统都是选择这种方法 image.png

Cuckoo Hashing

Cuckoo Hashing的核心思想是使用多个hash算法来找到空闲的slot,如果有冲突,则把之前存放的值进行回退。

主要流程如下:

  1. 使用两个独立的哈希函数,将键分别映射到两个不同的哈希表位置(通常称为两个桶)。
  2. 当插入一个键时,首先检查两个哈希表位置是否有空闲空间。如果至少有一个位置为空,则将键存储在其中一个位置上。
  3. 如果两个位置都已被占用,选择其中一个位置,并将已存储的键替换到另一个位置上。这个过程可能会引起链式反应,即替换的键在另一个位置上也引发冲突,进而替换其他键,直到找到一个空闲位置。
  4. 如果替换过程中找不到空闲位置,需要进行重新哈希操作,即重新选择两个哈希函数,然后重新插入所有的键。
  5. 重复步骤2-4,直到成功插入键或达到最大重哈希次数
image.png

Dynamic Hash Tables

上面提到的静态Hash tables都需要DBMS知道存储的元素个数,如果需要扩容,则需要rebuild整个hash table。 而动态Hash tables则可以在不重建的情况下根据需要增量调整大小。

Chained Hashing

该方法会在每个slot中维护一个buckets链表。如果发生hash冲突,则把数据插入到对应slot的链表中。获取的时候也是通过hash函数找到对应的slot,然后遍历链表即可。

这样就可以基于链表做到几乎无限增长,但问题就是如果数据量太大,链表的查询性能会下降。

image.png

Extendible Hashing

相较于上一种方法会让链表无限增长,该方法会超过大小限制的bucket进行拆分。

首先有一个global counter的数值,表示取hash值的前多少位作为key。比如插入B时,B的hash值前两位是10,则把他让入10对应的bucket中。插入C,C的hash值前两位也是10,此时10对应的bucket已经满了,需要进行拆分。

image.png

此时会把global counter值加1,变为3,代表取hash值的前三位作为key。同时把之前10对应的bucket进行拆分, 100开头的放入一个bucket, 101的放入一个bucket, C是101开头的,因此放入到拆分后的bucket中。完成了一个插入操作。

image.png

Linear Hashing

Linear Hashing会对超过大小的bucket进行扩容,并且新增一个slot,还有一个split pointer来指向需要rehash的slot.

比如当前有4个slot,把17放进去时,发现对应的第二个slot中的bucket已经满了,此时就会对这个bucket进行扩容。 image.png

同时会把slot的个数加1,新增一个slot, 并对split pointer指向的slot中的数据利用新的hash函数来重新分配位置,比如之前在第一个slot里面的20,在新hash函数后被分到了第五个slot中。Split Pointer也会向下移动一位。 image.png