热门标签 | HotTags
当前位置:  开发笔记 > 数据库 > 正文

PostgreSQL源码分析:动态Hash

1.为什么需要动态hash平常的hash,大多是下面这样一副面孔:图1一个静态hash结构这种Hash维护着一些桶,就是图上左边的部分,每一个桶中装着hash值相同的数据。这些具有相同hash值的数据形成一个链表。这种hash的一个最主要缺点就是桶的数目是一定的,不

1. 为什么需要动态hash 平常的hash,大多是下面这样一副面孔: 图1 一个静态hash结构 这种Hash维护着一些桶,就是图上左边的部分,每一个桶中装着hash值相同的数据。 这些具有相同hash值的数据形成一个链表。这种hash的一个最主要缺点就是桶的数目是一定的,不

1. 为什么需要动态hash

平常的hash,大多是下面这样一副面孔:



图1 一个静态hash结构

这种Hash维护着一些桶,就是图上左边的部分,每一个桶中装着hash值相同的数据。

这些具有相同hash值的数据形成一个链表。这种hash的一个最主要缺点就是桶的数目是一定的,不易扩展,随着插入数据增多,查找效率会急剧下降。

动态hash就是用来解决这个问题的,postgresql实现的动态hash保证填充因子不超过一个预定值的情况下动态地增长hash表的容量。同时每一次扩容所作的改动不大,空间利用率也比较地高。

2. 动态hash的结构

Postgresql与动态hash相关的代码分布在dynahash.c和hashfn.c这两个文件之中hashfn.c

主要是一些Hash Function,而dynahash.c才是动态hash的主要实现。

与普通hash表相比,动态hash多了一个新的行政单位: 目录。 如下图:



图2 postgresql 动态hash结构

dir是一个大小可变的数组,初始长度可以在创建时指定,以后每一次扩展其长度都会X2。dir中的每一项都指向一个长度固定的Segment, 这些Segment的长度都相同且必须是2的整数次幂,Segment数组中的元素是Bucket(桶) ,每一个桶中存放着一个链表,动态hash将所有具有相同hash值的元素都放在同一个桶中。

现在来看一下pg中 这些基本概念的定义:

typedef struct HASHELEMENT
{
struct HASHELEMENT *link; /* link to next entry in same bucket */
uint32 hashvalue; /* hash function result for this entry */
} HASHELEMENT;


/* A hash bucket is a linked list of HASHELEMENTs */
typedef HASHELEMENT *HASHBUCKET;


/* A hash segment is an array of bucket headers */
typedef HASHBUCKET *HASHSEGMENT;
这些定义都可以和上图相对应,不再多说。

3. 给定hash value,如何找到与其对应的Bucket

先看一下实现吧:

/* Convert a hash value to a bucket number */
static inline uint32
calc_bucket(HASHHDR *hctl, uint32 hash_val)
{
uint32 bucket;

bucket = hash_val & hctl->high_mask;
if (bucket > hctl->max_bucket)
bucket = bucket & hctl->low_mask;

return bucket;
}
hctl->max_bucket 指的是bucket总数减1,对于图2来说,这个值为15

hctl->low_mask 是<= (hctl->max_bucket + 1)的最大的2^K减1, 对于图2来说,这个值是16 - 1 = 15 (0000 1111)

hctl->high_mask 是2^(K + 1)减1, 对于图2来说,这个值是32 - 1 = 31 (0x0001 1111)

这几个变量要注意的是,hctl->max_bucket在hash表创建好以后,会变化,一般情况下每次增加一个,如果hctl->max_bucket变成了2的整数次幂,就需要更新hctl->low_mask和hctl->high_mask。更新代码如下:

/*
* If we crossed a power of 2, readjust masks.
*/
if ((uint32) new_bucket > hctl->high_mask)
{
hctl->low_mask = hctl->high_mask;
hctl->high_mask = (uint32) new_bucket | hctl->low_mask;
}

我们回头继续看那个calc_bucket函数, 因为理解这个函数是理解动态扩展的关键。从上面关于max_bucket,low_mask和high_mask的介绍,可以得出下面的结论:

hctl->high_mask >= hctl->max_bucket >= hctl->low_mask

反应在它们的二进制表示上,如果hctl->low_mask占m位(2^m - 1),则hctl->high_mask占m + 1位。而hctl->max_bucket是在闭区间[low_mask,high_mask]之间的。所以要取得hash_val的bucket值应优先&上high_mask,如果发现得到的bucket的序号比max_bucket还大,则再&上low_mask。这一步为后面的扩展埋下了伏笔。在扩展的时候,也就是max_bucket增加的时候,可能会造成这种情况,扩展前后同一hash_val通过calc_bucket算出的值不一样。下面在叙述扩展的时候再详细解说其解决办法 。

4. 动态扩展

在弄清楚动态hash的结构以及如何从hash值得到其所在的bucket后,hash表的查找,删除以及通常情况下的插入操作就非常地容易啦。比如删除,就先是调用 cal_bucket找到所在的桶,然后在这个桶中一个个地找,找到具有相同键值的元素,就从桶所对应的链表中删除。 下面来说一下动态扩展的问题,毕竟这才是pg动态hash的核心。

说到动态扩展,就得说扩展时机,什么时候我们的hash表需要增大容量呢? 我们增大容量是为了保证其查找的效率。而hash表的查找效率是与一个叫做填充因子东西有很大关系的。pg的填充因子定义为:

填充因子 = hctl->nentries / (hctl->max_bucket + 1)

从这个公式可以看出填充因子会随着插入元素的增多而增加, 当增大到比hctl->ffactor还要大的时候就需要扩展了,这里的扩展的意思是指hctl->max_bucket要增加1个。其步骤如下:

1) 计算出 max_bucket + 1所在的segment索引值segndx

<<扩展dir和segment>>

2) 如果segndx没有超出已分配的segment的容量,转入5),否则继续;

3) 检查segndx是否超出了现有的dir大小,如是,将dir的大小扩展为以前的2倍;

4) 给dir[segndx]分配一个新的segment;

<<计算新旧bucket>>

5) 计算max_bucket + 1在没有扩展前的bucket号old_bucket;

6) max_bucket++后检查max_bucket是否成了2的整数次幂,若是,修改low_mask和high_mask;

7) 计算新的bucket号new_bucket;

<<将old_bucket中满足条件的元素移动new_bucket中>>

8) 遍历old_bucket链表,重新计算他们所应该在的bucket,将需要移动的移到到new_bucket中。

这个过程中只有步骤8需要说明一下。为什么只是本次扩展前的old_bucket,而没有检查,前一次,前两次扩展的old_bucket呢?这是由calc_bucket中计算bucket的方法所决定的,其实你想的没有错,是应该去遍历前一次,两次,扩展的那些old_bucket,但是没有必要,即使你这样做了,你也从这些bucket中找不到一个可以入到new_bucket中的元素。我们只需要证明经过一次扩展,留在old_bucket中的元素的hash值对应的bucket从些以后不会再改变就行了。

扩展前后bucket计算路径可以用下面矩阵来表示:

扩展后 &high_mask 扩展后 &low_mask

扩展前 &high_mask (1) (2)

扩展前 &low_mask (3) (4)

这其中(2)是不可能出现的,因为扩展前的high_mask与扩展后low_mask相等,如果(2)成立就出出现max_bucket > (max_bucket + 1)的矛盾。而对于 情况(1),由于high_mask和max_bucket只会增加,把以bucket号从些不会改变, 而(3)和(4)实际上就是本次扩展需要移动的项。所以已经证明那些留在 old_bucket中的元素不会变。

5. 总结

pg实现的这个动态hash保证填充因子不超过设定值,其检索效率不会因插入元素的增多而降低,同时其扩展的代价也不是十分地大,每次只需要移动一个bucket中的某些项到新的bucket中。
推荐阅读
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了adg架构设置在企业数据治理中的应用。随着信息技术的发展,企业IT系统的快速发展使得数据成为企业业务增长的新动力,但同时也带来了数据冗余、数据难发现、效率低下、资源消耗等问题。本文讨论了企业面临的几类尖锐问题,并提出了解决方案,包括确保库表结构与系统测试版本一致、避免数据冗余、快速定位问题等。此外,本文还探讨了adg架构在大版本升级、上云服务和微服务治理方面的应用。通过本文的介绍,读者可以了解到adg架构设置的重要性及其在企业数据治理中的应用。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • 本文详细介绍了MysqlDump和mysqldump进行全库备份的相关知识,包括备份命令的使用方法、my.cnf配置文件的设置、binlog日志的位置指定、增量恢复的方式以及适用于innodb引擎和myisam引擎的备份方法。对于需要进行数据库备份的用户来说,本文提供了一些有价值的参考内容。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文由编程笔记小编整理,介绍了PHP中的MySQL函数库及其常用函数,包括mysql_connect、mysql_error、mysql_select_db、mysql_query、mysql_affected_row、mysql_close等。希望对读者有一定的参考价值。 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • MyBatis错题分析解析及注意事项
    本文对MyBatis的错题进行了分析和解析,同时介绍了使用MyBatis时需要注意的一些事项,如resultMap的使用、SqlSession和SqlSessionFactory的获取方式、动态SQL中的else元素和when元素的使用、resource属性和url属性的配置方式、typeAliases的使用方法等。同时还指出了在属性名与查询字段名不一致时需要使用resultMap进行结果映射,而不能使用resultType。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
author-avatar
徐曼曼_
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有