第五讲 哈希表

我们在前面讲了存储层,以及从次磁盘中将页面加载到缓冲池【Buffer Pool】中,现在我们继续往上,来讨论如何支持 DBMS 的执行引擎从页面中读取/写入数据。这部分是访问方法层的功能,它负责通过索引或者表本身,设置是其他机制来实现功能,
这里会涉及两种类型的数据结构:

  • 哈希表(无序)
  • 树(有序)

1 数据结构

DBMS 内部很多地方都在使用数据结构:

  •  内部元数据:比如页目录【Page Dictionary】,或者页表【Page Table】,使用哈希结构,它们负责将页ID【Page ID】映射到磁盘中页的位置或者在缓冲区中的位置。

  • 核心数据存储:比如索引组织的表,其中实际的元组本身在 b+ 树的叶节点中,所以你可以让你的表直接在数据结构中表示,而不是无序的热文件

  • 临时数据结构:我们还可以使用这些数据结构来执行查询【Query】,生成临时或临时的数据集合,这样可以更有效地执行查询【Query】,这这基本上就是我们现在要非常快速地实现哈希联接的方式,以使用哈希联接非常快速地实现联接,所以我们会动态地建立一个哈希表,并用表中的数据填充或者扫描进行连接,然后扔掉哈希表,所以你知道我们在构建哈希表并不意味着它会持续很长时间

  • 表索引

2.设计考虑

数据组织【Data Organization】:我们如何在内存/页面中布局数据结构以及存储哪些信息以支持高效访问。

并发性【Concurrency】:如何让多个线程同时访问数据结构而不引起问题。

3. hash table

哈希表实现了将键映射到值的无序关联数组【unordered associative array】。


它使用哈希函数来计算给定键在该数组中的偏移量,从中可以找到所需的值。

空间复杂度:O(n)
时间复杂度:

  • 平均:O(1)
  • 最差:O(n)

3.1 静态哈希表【Static Hash Table】

分配一个巨大的数组,该数组为您需要存储的每个元素分配一个槽。
要查找某个键对应的条目,请将元素键的哈希值与数组大小 N 取余,就可以得到他在数组中的偏移量

当然,在实际中,我们并不会在数组中存储 key,本质上存储的其实是一个指向其他地址的指针,该指针所在的结构中存储着 key 和其对应的值。

至于为什么我们要存储key,这是因为哈希碰撞。

不切实际的假设
  • 假设1:元素数量提前已知并固定:在某些场景下是可以做到的,比如Buffer Pool,他的总大小以及页大小是已知的,因此我们可以计算出来元素总量,但是像索引,它随着数据插入会越来越多,因此它的元素数量是无法预知的
  • 假设2:每个键都是唯一的。
  • 假设3:完美的哈希函数保证不会发生冲突:如果 key1≠key2,则 hash(key1)≠hash(key2) 

当我们在构建哈希表时,我们会考虑以下两点:

  1. 哈希函数【Hash Function】
    1. 如何将大的 Key 空间,映射到更小的有限的域【Domain】里
    2. 在速度与冲突率/碰撞率之间权衡
  2. 哈希方案【Hash Schema】
    1. 哈希模式是我们在完成哈希【hash】之后用来处理碰撞的机制
    2. 在分配一个大的哈希表与花费额外的计算来实现对 Key 的 GET/PUT 之间权衡

3.2 哈希函数

对于任意的 Key ,哈希函数可以返回一个数字(通常都是64位的)来代表该 Key 。

我们不想对 DBMS 哈希表使用加密哈希函数(例如 SHA-256)。并且,我们希望哈希函数可以在保证速度的情况下,提供更低的碰撞率。

下面是系统所使用的哈希函数的一个快速概述:

  • CRC-64 (1975):用于网络中的错误检测。
  • MurmurHash (2008):设计为快速、通用的哈希函数,redis也是用的它
  • Google CityHash (2011):设计针对于短密钥(<64字节)更快
  • Facebook XXHash (2012):来自 zstd 压缩的创建者。
  • Google FarmHash (2014):CityHash 的新版本具有更好的碰撞率。

3.3 静态哈希方案【static hash schema】

方法1:线性探针哈希【Linear Probe Hashing】
方法2:布谷鸟哈希【Cuckoo Hashing】
我们将在高级数据库课程中介绍其他几种方案:

  • 罗宾汉哈希【Robin Hood Hashing】
  • 跳房子哈希【Hopscotch Hashing】
  • 瑞士表【Swiss Tables】
3.3.1 线性探针哈希【Linear Probe Hashing】

它是一个巨大的表【table】(可以将它看作是一个巨大的环形缓冲【circular buffer】),他有很多插槽【slots】,通过线性搜索表中的下一个空闲槽来解决冲突。

要想插入元素,我们首先要对 Key 做哈希,如果对应的槽是自由的/空的【free】,我们就可以在这里插入元素,而如果对应的槽不是自由/空的【Not Free】,那就查找下一个自由/空的【free】槽(我们会一直查找,直到找到为止,但是如果遍历一圈回来的话,那说明该表已经满了,那就需要崩溃或者扩容了),并在那里插入元素。

  • 要确定某个元素是否存在,请散列到索引中的某个位置并自此开始扫描【scan】。
  • 必须将键【key】存储在索引中才能知道何时停止扫描。
  • 插入和删除是查询的一般化

示例:谷歌的 absl::flat_hash_map

线性探针哈希【Linear Probe Hashing】也被称为开放寻址【Open Addressing】

栗子:

1️⃣ 插入元素A时,我们通过哈希函数计算出槽,并将key和value均存储进去,存储key的目的是为了查询时的扫描

2️⃣ 插入元素C时,哈希函数算出它与元素A的插槽一致,这时候我们需要向后扫描,找到最近的空槽

问题点 1 删除问题

1️⃣ 当A~F 元素全部插入后,我们删除元素C,我们通过扫描找到了C,

然后我们删除元素C

2️⃣ 此时,我们来查询元素D,但是元素D 本来是哈希到元素C所在的位置,而该位置现在变成空的,我们可以认为D元素不会在哈希表里了

解决办法一  移动【Movement】:重新散列后面的键【Key】,直到碰到第一个空槽为止。但是这可能会导致全表的重新组织,代价太大了,没有任何系统采用这种办法

解决方法二 墓碑【Tombstone】,即设置一个标记来指示槽中的条目已被逻辑删除。

  • 插槽可以重用给新的 Key。
  • 可能需要定期进行垃圾收集,否则会浪费资源。
     

问题点 2 不唯一键

这里有两种方法:

方法 1 单独的链表【Separate Linked List】

  • 将每个键的值存储在单独的存储区域中,比如一个链表中。
  • 但是,如果重复项数量较多,值的列表可能会溢出到多个页【page】上。

方法 2 冗余键【Redundant Keys】

  • 将重复的键【keys】的条目【entries】一起存储在哈希表中。但是删除时,需要key+value u一起来定位删除哪一个元素
  • 这是大多数系统所做的。

优化点
  • 基于键的类型和大小的专用哈希表实现,比如当哈希的键【key】是字符串时,字符串可以是几个字节,也可能是几兆的,此时我们可能只是想在数组中存储到该字符串的指针,而不是将字符串存储在插槽中
  • 将元数据(比如墓碑,比如null,比如空等)信息存储到一个单独的数组中,比如通过压缩位图,来记录某个插槽时墓碑还是null等信息。
  • 使用表版本【table】+槽版本【slot versioning】控制元数据快速使哈希表中的所有条目失效。因为一次性分配内存的代价是比较大的,我们希望可以重复利用创建的数组,为此我们为表和插槽增加了版本元数据,当插槽的版本低于表版本时,我们可以认为任何东西已经被删除了,我们可以忽略在这里看到任何东西。
3.3.2 布谷鸟哈希【CUCKOO HASHING】

布谷鸟哈希使用多个哈希函数,以在哈希表中查找多个位置来插入记录。

  • 插入时,检查多个位置,并选择空的位置。
  • 如果没有可用的位置,则从其中之一驱逐该元素,然后重新散列它以找到新的位置。

查找和删除始终为 O(1),因为无论我们有多少个哈希函数,我们只知道,在每次计算后,我们就知道要去哈希表中的某个位置。
 

栗子:

1️⃣ 当我们插入元素A时,根据hash函数计算,我们可以得到两个候选槽位

2️⃣ 这时候我们可以任意选择一个位置,假设我们选择第一个位置:

3️⃣ 然后我们插入元素B,经过哈希计算后,其中有一个位置与元素A重合了

 

4️⃣ 这时候我们选择空槽进行插入:

 5️⃣ 当插入元素C时,假设哈希函数计算的槽位都被占用了

6️⃣ 假设我们投硬币,选择了元素B被驱逐,然后C插入了进去,然后B经过哈希计算后,将A元素驱逐,B插入了进去,然后A元素经过哈希计算后,插入到了一个新位置上

 7️⃣ 当我们陷入无限循环时,即绕了一圈又回来了(你只需要跟踪我放进去的键和我一开始放进去的键是否时一样的),那这时候就需要将哈希数组扩容并进行rehash

8️⃣ 当我们查询元素B时,只需要重新计算哈希函数,然后进行key检查,就可以找打到元素




3.4 动态哈希方案【dynamic hash schema】

前面讲到的的哈希表要求 DBMS 知道它想要存储的元素的数量,否则,如果需要增加/缩小表的大小,则必须重建表。 

动态哈希表根据需要逐渐调整自身大小。

  • 链式哈希【Chained Hashing】
  • 可扩展散列【Extendible Hashing】
  • 线性哈希【Linear Hashing】
3.4.1 链式哈希【Chained Hashing】

为哈希表中的每个槽维护一个存储桶的链表。

通过将具有相同哈希键的所有元素放入同一个桶中来解决冲突。

  • 要确定某个元素是否存在,请散列到其存储桶并扫描它
  • 插入和删除是查找的一般化

栗子:

1️⃣ 简单的插入元素A,B

2️⃣ 当出现冲突时,通过链来解决

 3️⃣ 甚至可以在 bucket 中放入一个 bloom 过滤器,来协助我们过滤某个 key 是否在对应的链表里

Bloom Filter 

回答集合成员关系查询【Query】的概率数据结(位图)。它与索引不同,索引的目的是,给定一个 key ,它告诉你它在哪里,而 bloom 过滤器只是告诉你它会否存在。

  •  假阴性永远不会发生

  • 假阳性有时会发生

Insert(x) :使用  k 列函数将过滤器中的位设置为1

Lookup(x)  :检查每个哈希函数结果的位是否为1

栗子:

1️⃣ 我们插入一元素时,经过哈希函数运算后,我们将其对应的位设置为 1:

2️⃣ 当我们进行查询时,就是曲检查每个哈希函数算出来的位是否全为1

 3️⃣ 但是偶尔我们也会出现错误

 3.4.2  可扩展散列【Extendible Hashing】

链式哈希方法逐步分割存储桶,而不是让链表永远增长。
多个槽位置可以指向同一个桶链。
在拆分时重新洗牌存储桶条目并增加要检查的位数。
→ 数据移动仅限于分割链。

栗子:

0️⃣ 首先,我们全局ID = 2,它不仅控制全局 Bucket 数组的大小,也控制哈希结果的有效位,左边的是槽点数组【Slot Point Array】,右边的是桶列表【Buckect List】,顶部两个槽位指向最顶上的桶数组,而下面的两个槽则分别指向不同的桶数组。

1️⃣ 数据查询A时,我们根据哈希结果的高位,哦判断出它们位于本地 1

2️⃣  当数据插入B时,根据哈希结果高位,我们将其置于本地 2 中

3️⃣ 当我们插入元素C时,它与元素B碰撞到相同的本地桶列表中,而这时候,该桶列表已经满了,

这时候我们需要将全局 ID 从 2 增加到 3

并将原来已经满了的本地桶列表的本地 ID 从 2 增加到 3 ,并拆为两个本地 ID = 3 的桶列表,而其他没有满的本地桶列表继续保持。

然后我们就可以将C插入到对应的桶列表中了

 3.4.3 线性哈希【Linear Hashing】

哈希表维护一个指针,用于跟踪下一个要拆分的桶。

  • 当任何桶溢出时,在指针位置分裂桶。

使用多个哈希来查找给定键所在的正确存储桶。
可以使用不同的溢出标准:

  • 空间利用【Space Utilization】
  • 溢出链的平均长度【Average Length of Overflow Chains】

栗子:

1️⃣ 假设我们的桶列表现在长这样

2️⃣ 当我们查询元素时,只需要哈希计算后找打到对应的 桶列表,在内部做扫描查询即可

3️⃣ 当我们插入元素时,而该元素哈希结果所在的桶列表满了,

 

对于此,我们要做一个溢出:

4️⃣ 现在,因为我们碰到溢出了,因此我们需要拆分,此时Split Pointer指向插槽 0 ,我们需要新增一个新的槽位 4,并需要将元素8和元素20进行rehash,且取模的因子也从 N 变成了 2N

5️⃣ 接下来,拆分指针继续向下移动,但是此时不拆分

6️⃣ 我们查询元素20,因为其对应的原始插槽被拆分过,所以他需要计算两次哈希,兵最终找打到插槽4

 7️⃣ 我们继续查询元素9,通过哈希函数计算的到它在插槽1 ,而且当前拆分指针停留在插槽1处,因此它还没有被拆分,我们可以在插槽1对应的桶列表中查到元素

8️⃣ 最终,拆分指针会到最底部,这也预示着我们的插槽也变成了8个

根据拆分指针对桶进行拆分,最终会到达所有溢出的桶,当指针到达最后一个槽时,删除第一个哈希函数并将指针移回到开头。

如果拆分指针下方的“最大”的桶为空,则哈希表可以将其删除,并沿相反方向移动拆分指针。

栗子:

1️⃣ 当我们打算删除元素20时,此时拆分指针位于插槽1处

2️⃣ 通过计算我们的到它位于插槽4,然后我们删除该元素,这时候桶数组为空,我们需要删除最大的插槽及其桶数组,并将拆分枝上向上移动:

 

3.4 结论

哈希表是支持 O(1) 查找的快速数据结构,它在整个 DBMS 内部都有使用

  • 速度和灵活性之间的权衡

但是哈希表通常不是会用于表的索引!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/767699.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

生物信息学文章中常见的图应该怎么看?

目录 火山图 热图 箱线图 森林图 LASSO回归可视化图&#xff08;套索图&#xff09; 交叉验证图 PCA图 ROC曲线图 这篇文章只介绍这些图应该怎么解读&#xff0c;具体怎么绘制&#xff0c;需要什么参数&#xff0c;怎么处理数据&#xff0c;会在下一篇文章里面给出 火山…

python之jsonpath的使用

文章目录 介绍安装语法语法规则举例说明 在 python 中使用获取所有结构所有子节点的作者获取所有子孙节点获取所有价格取出第三本书的所有信息取出价格大于70块的所有书本从mongodb 中取数据的示例 介绍 JSONPath能在复杂的JSON数据中 查找和提取所需的信息&#xff0c;它是一…

Java设计模式之单例模式(多种实现方式)

虽然写了很多年代码&#xff0c;但是说真的对设计模式不是很熟练&#xff0c;虽然平时也会用到一些&#xff0c;但是都没有深入研究过&#xff0c;所以趁现在有空练下手 这章主要讲单例模式&#xff0c;也是最简单的一种模式&#xff0c;但是因为spring中bean的广泛应用&#…

YoloV8改进策略:BackBone改进|PKINet

摘要 PKINet是面向遥感旋转框的主干,网络包含了CAA、PKI等模块,给我们改进卷积结构的模型带来了很多启发。本文,使用PKINet替代YoloV8的主干网络,实现涨点。PKINet是我在作者的模型基础上,重新修改了底层的模块,方便大家轻松移植到YoloV8上。 论文:《Poly Kernel Ince…

计算机三级网络技术 选择+大题234笔记

上周停去准备计算机三级的考试啦&#xff0c;在考场上看到题目就知道这次稳了&#xff01;只有一周的时间&#xff0c;背熟笔记&#xff0c;也能稳稳考过计算机三级网络技术&#xff01;

鸿蒙开发学习:【华为支付服务客户端案例】

简介 华为应用内支付服务&#xff08;HUAWEI In-App Purchases&#xff09;支持3种商品&#xff0c;包括消耗型商品、非消耗型商品和订阅型商品。 消耗商品&#xff1a;仅能使用一次&#xff0c;消耗使用后即刻失效&#xff0c;需再次购买。非消耗商品&#xff1a;一次性购买…

计算机常见的知识点(3)

计算机系统 系统的构成 一个完整的计算机系统是由硬件和软件组成 硬件是由运算器、控制器、存储器、输入设备、输出设备五部分组成 其中&#xff1a;中央处理器(简称CPU)运算器控制器 主机中央处理器主存储器 计算机软件包括计算机本身运行所需要的系统软件和用户完成任务…

Mybatis中显示插入数据成功,但在数据库中却没有显示插入的数据

1、在mybatis-config.xml中查看是否添加了JDBC&#xff0c;并引入了映射文件 2、在测试文件中&#xff0c;结尾是否添加提交事务&#xff1a;sqlSession.commit() 添加了这一步就能够将数据提交到数据库中&#xff0c;最后再关闭事务&#xff1a;sqlSession.close() * 如果运…

JWT原理分析

为什么会有JWT的出现&#xff1f; 首先不得不提到一个知识叫做跨域身份验证&#xff0c;JWT的出现就是为了更好的解决这个问题&#xff0c;但是在没有JWT的时候&#xff0c;我们一般怎么做呢&#xff1f;一般使用Cookie和Session&#xff0c;流程大体如下所示&#xff1a; 用…

手撕算法-买卖股票的最佳时机 II(买卖多次)

描述 分析 使用动态规划。dp[i][0] 代表 第i天没有股票的最大利润dp[i][1] 代表 第i天持有股票的最大利润 状态转移方程为&#xff1a;dp[i][0] max(dp[i-1][0], dp[i-1][1] prices[i]); // 前一天没有股票&#xff0c;和前一天有股票今天卖掉的最大值dp[i][1] max(dp[i-1…

Linux查看磁盘空间

查看磁盘空间 df -h 查看目录所占空间 du -sh [目录] 查看当前目录下, 所有目录所占空间 (一级目录) find . -maxdepth 1 -type d -exec du -sh {} \;-maxdepth 1 查看的目录深度是1级, 2则是2级

FOCUS-AND-DETECT: A SMALL OBJECTDETECTION FRAMEWORK FOR AERIAL IMAGES

摘要 为了解决小对象检测问题&#xff0c;提出了一个叫做 Focus-and Detect 的检测框架&#xff0c;它是一个两阶段的框架。 第 一阶段包括由高斯混合模型监督的对象检测器网络&#xff0c;生成构成聚焦区域的对象簇 。 第二阶段 也是一个物体探测器网络&#xff0c;预测聚焦…

【云开发笔记No.6】腾讯CODING平台

腾讯云很酷的一个应用&#xff0c;现在对于研发一体化&#xff0c;全流程管理&#xff0c;各种工具层出不穷。 云时代用云原生&#xff0c;再加上AI&#xff0c;编码方式真是发生了质的变化。 从前&#xff0c;一个人可以写一个很酷的软件&#xff0c;后来&#xff0c;这变得…

<商务世界>《第16课 餐桌礼仪之座次》

1 简要 我国自古以来就很重视座位礼仪&#xff0c;非讲究&#xff0c;分君臣、分宾主、分方位等等而今座位礼仪已经简化为&#xff1a; 以“中”为尊&#xff1a; 中心为尊&#xff0c;突出主位。 以“右”为尊&#xff1a; 从历史上到国际上都是以右为尊。 以“内”为尊&…

故障诊断模型 | 基于图卷积网络的轴承故障诊断

文章目录 文章概述模型描述模型描述参考资料文章概述 故障诊断模型 | 基于图卷积网络的轴承故障诊断 模型描述 针对基于图卷积网络(GCN)的故障诊断方法大多默认节点间的权重相同、导致诊断精度较低与鲁棒性较差的问题,提出了一种基于欧式距离和余弦距离的 GCN 故障诊断方法…

力扣热门算法题 62. 不同路径,66. 加一,67. 二进制求和

62. 不同路径&#xff0c;66. 加一&#xff0c;67. 二进制求和&#xff0c;每题做详细思路梳理&#xff0c;配套Python&Java双语代码&#xff0c; 2024.03.21 可通过leetcode所有测试用例。 目录 62. 不同路径 解题思路 完整代码 Python Java 66. 加一 解题思路 …

29-goto语句

29-1 goto语句介绍 C语言中提供了可以随意滥用的goto语句和标记跳转的标号。 从理论上goto语句是没有必要的&#xff0c;实践中没有goto语句也可以很容易的写出代码。 但是某些场合下goto语句还是用得着的&#xff0c;最常见的用法就是终止程序在某些深度嵌套的结构的处理过程…

第十一届蓝桥杯大赛第二场省赛试题 CC++ 研究生组-回文日期

solution1&#xff08;通过50%&#xff09; #include<stdio.h> void f(int a){int t a;while(a){printf("%d", a % 10);a / 10;}if(t < 10) printf("0"); } int isLeap(int n){if(n % 400 0 || (n % 4 0 && n % 100 ! 0)) return 1;r…

抖音IP属地怎么更改

抖音是一个非常受欢迎的短视频平台&#xff0c;吸引了无数用户在上面分享自己的生活和才艺。然而&#xff0c;随着快手的火爆&#xff0c;一些用户开始担心自己的IP地址会被他人获取&#xff0c;引起个人隐私风险。那么&#xff0c;抖音用户又该如何更改到别的地方呢&#xff1…

父类子类构造方法调用示例

父类写无参构造&#xff0c;子类不写构造&#xff0c;实例化子类&#xff0c;会同时调用父类构造方法 public class Father {private String name;private int age;public Father() {System.out.println("父类无参构造");}} public class Son extends Father {priva…