哈希索引(PostgreSQL 14 Internals翻译版)

概览

哈希索引提供了根据特定索引键快速查找tuple ID (TID)的功能。粗略地说,它只是一个存储在磁盘上的哈希表。哈希索引唯一支持的操作是根据相等条件进行搜索

当一个值插入到索引中时,将计算索引键的哈希函数。PostgreSQL哈希函数返回32位或64位整数;这些值中最低的几个位用作相应桶的编号。将TID和键的哈希码添加到所选的桶中。键本身不存储在索引中,因为这样处理固定长度的小值更方便。

索引的哈希表是动态扩展的。桶的最小数量是两个。随着索引元组数量的增长,其中一个存储桶被分成两个。该操作使用了多一位哈希码,因此元素仅在拆分产生的两个桶之间重新分布;哈希表的其他桶的组成保持不变。

索引搜索操作计算索引键和对应桶号的哈希函数。在所有桶内容中,搜索将只返回那些与键的哈希码对应的TID。由于bucket元素是按键的哈希码排序的,因此二进制搜索可以非常有效地返回匹配的TID。

由于键不存储在哈希表中,因此索引访问方法可能会由于哈希冲突而返回冗余的tid。因此,索引引擎必须重新检查访问方法获取的所有结果。出于同样的原因,不支持仅索引扫描

页面布局

与常规哈希表不同,哈希索引存储在磁盘上。因此,必须将所有数据安排到页中,最好是这样一种方式,即索引操作(搜索、插入、删除)需要访问尽可能少的页。

哈希索引使用四种类型的页面:

  • metapage—提供索引“目录”的页零
  • bucket pages—索引的主要页面,每个bucket一个
  • overflow pages—当主桶页不能容纳所有元素时使用的附加页
  • bitmap pages—包含位数组的页面,用于跟踪已被释放并可被重用的溢出页面

我们可以使用pageinspect扩展查看索引页。

让我们从一张空表开始:

哈希索引(PostgreSQL 14 Internals翻译版)_第1张图片

我已经分析了表,因此创建的索引将具有尽可能小的大小;否则,将根据表包含10个页面的假设选择桶的数量。

索引包含四个页面:元页面,两个桶页面和一个位图页面(一次创建以备将来使用):

哈希索引(PostgreSQL 14 Internals翻译版)_第2张图片

元页面包含有关索引的所有控制信息。我们现在只对几个值感兴趣:

哈希索引(PostgreSQL 14 Internals翻译版)_第3张图片

每个桶的估计行数显示在ffactor字段中。该值是根据块大小和fillfactor存储参数值计算得出的。通过绝对统一的数据分布和没有散列冲突,您可以使用更高的填充因子值,但在实际数据库中,它会增加页面溢出的风险。

对于散列索引来说,最糟糕的情况是当一个键被重复多次时,数据分布出现很大的倾斜。由于哈希函数将返回一个相同的值,因此所有数据将被放置到相同的桶中,并且增加桶的数量不会有帮助。

现在索引为空,如ntuples字段所示。让我们通过插入具有相同索引键值的多行来导致桶页溢出。索引中出现溢出页:

哈希索引(PostgreSQL 14 Internals翻译版)_第4张图片

综合所有页面的统计数据,桶0是空的,而所有的值都被放到了桶1中:其中一些位于主页面,不适合的可以在溢出页中找到。

哈希索引(PostgreSQL 14 Internals翻译版)_第5张图片

很明显,如果同一个bucket的元素分布在几个页面上,性能将受到影响。如果数据分布是均匀的,散列索引将显示最佳结果。

现在让我们来看看如何分割桶。当索引中的行数超过可用桶的估计因子值时,就会发生这种情况。这里我们有两个桶,因子是307,所以它将在第615行插入索引时发生:

哈希索引(PostgreSQL 14 Internals翻译版)_第6张图片
maxbucket值已经增加到2:现在我们有三个桶,编号从0到2。但是,尽管我们只添加了一个桶,页面的数量却翻了一番:

哈希索引(PostgreSQL 14 Internals翻译版)_第7张图片
其中一个新页面由bucket 2使用,而另一个页面保持空闲状态,一旦出现就会被bucket 3使用。

哈希索引(PostgreSQL 14 Internals翻译版)_第8张图片
哈希索引(PostgreSQL 14 Internals翻译版)_第9张图片
因此,从操作系统的角度来看,哈希索引是快速增长的,尽管从逻辑的角度来看哈希表是逐渐增长的。

为了在一定程度上平衡这种增长并避免一次分配过多的页面,从第10次增加开始,将页面分配为四个相等的批次,而不是一次分配所有的页面。

元页面的另外两个字段实际上是位掩码,提供了桶地址的详细信息:

哈希索引(PostgreSQL 14 Internals翻译版)_第10张图片
桶号由与高掩码对应的哈希码位定义。但是如果接收到的桶号不存在(超过maxbucket),则采用低掩码位。在这个特殊的例子中,我们取两个最低的位,得到0到3的值;但是如果我们得到3,我们只会取一个最低的位,也就是说,用桶1代替桶3。

每次大小增加一倍时,新的桶页被分配为单个连续块,而溢出页和位图页则根据需要插入这些片段之间。元页面保留插入到备件数组中每个块中的页面数,这使我们有机会使用简单的算术根据桶号计算其主页的数量。

在这个特殊的例子中,第一次增加之后插入了两个页面(一个位图页面和一个溢出页面),但是在第二次增加之后没有发生新的添加:

哈希索引(PostgreSQL 14 Internals翻译版)_第11张图片
当指向死元组的指针被删除时,索引页中的空间将被释放。它发生在页面修剪期间(在尝试将元素插入完全填充的页面时触发)或执行常规真空时。

但是,哈希索引不能缩小:一旦分配,索引页将不会返回给操作系统。主页被永久地分配到它们的bucket中,即使它们根本不包含任何元素;清除的溢出页在位图中被跟踪,并且可以被重用(可能由另一个桶)。减小索引物理大小的唯一方法是使用REINDEX或
VACUUM FULL命令。

查询计划没有索引类型的指示:

在这里插入图片描述
哈希索引(PostgreSQL 14 Internals翻译版)_第12张图片

你可能感兴趣的:(PostgreSQL,哈希算法,算法)