作者简介
Laurenz Albe
cybertec公司工程师
译者简介
王志斌
PostgreSQL爱好者
校对者简介
崔鹏
PostgreSQL爱好者
在PostgreSQL中有很多种索引类型,其中哈希索引是最广泛被忽视的。最近有人问我一个关于哈希索引性能的问题,这让我意识到了这一点。是时候探索一下这个鲜为人知的PostgreSQL角落并进行一些基准测试了!
哈希索引的历史
PostgreSQL历来拥有哈希索引(我查看了4.2版本的源代码),但在v10版本之前它们在崩溃时不够安全。因此在旧版本中基本不能使用它们。正是由于这个原因,哈希索引在PostgreSQL开发人员中受到了很少的关注。但即使在它们成为一等公民之后,它们也一直处于边缘地位。现在是时候让它们登台展示它们的能力了。
哈希索引实现细节
您可以在hash README文件中找到详细的描述。
哈希索引有两个或更多的桶页,用于存储系统定义哈希函数索引值的结果。每当索引中的条目平均数超过依赖于fillfactor的某个特定值时,PostgreSQL会通过拆分每个桶页来将桶页的数量翻倍。对于大型索引,PostgreSQL将这种翻倍操作分成四个批次执行,以便在多个DML操作中分摊工作量。因此,对具有哈希索引的表进行INSERT操作有时可能会变得非常缓慢,类似于在GIN索引中清理未决列表的效果。如果某个哈希值不适合其所属的桶页(但尚未到达翻倍索引的时间),PostgreSQL会将其放入特定目的的溢出页中。除非一些被索引的值非常频繁出现,否则这种情况不应经常发生。
哈希索引的潜在用途
哈希索引只能有单个列,它们只支持相等搜索,并且它们不能强制唯一性。因此,它们实际上不能做任何B-tree索引无法做到的事情,只有一个例外:在B-tree索引中,条目的长度限制是页面大小的三分之一,而在哈希索引中,您可以用于任意大小的值,因为索引中仅存储哈希值。不过,这应该是一个不常见的情况。
您有多么频繁的需要实现高效查询:
SELECT id, name FROM person WHERE mugshot = '\xffd8ffdb...' ;
复制
文档 暗示哈希索引在某些情况下可能比B-tree索引更好:
哈希索引最适用于在较大表上执行等值扫描的 SELECT 和 UPDATE-heavy 工作负载。在 B-tree索引中,搜索必须通过树下降直到找到叶子页。在包含数百万行的表中,这种下降可能会增加对数据的访问时间。哈希索引中等价于叶子页的部分被称为桶页。相反,哈希索引允许直接访问桶页,从而在较大表中可能减少索引访问时间。在索引/数据大于shared_buffers/RAM 时,这种“逻辑 I/O” 的减少会更加明显。
由于溢出情况,我们可以说哈希索引最适用于唯一的、几乎唯一的数据或每个哈希桶中行数较少的数据。
一个用于测试哈希索引性能的测试平台
我们将使用如下函数和表:
CREATE FUNCTION mkbig(integer) RETURNS text
LANGUAGE sql AS
'SELECT repeat(md5($1::text), 20)';
CREATE TABLE hashtest (
c1 bigint,
c2 bigint,
c3 text,
c4 text
);
复制
然后我们在其中一个列上创建一个哈希索引或B-tree索引。之后我们将加载1000万行并查看所需的时间:
\timing on
INSERT INTO hashtest
SELECT i, i 10000, mkbig(i), mkbig(i 10000)
FROM generate_series(1, 10000000) AS g(i);
复制
对于text列,初始的想法是插入一些既不小也不足以触发PostgreSQL的TOAST机制的值。
第二列和第四列包含重复10000次的值,以便我们看看哈希索引是如何处理这种情况的。如果有的话,我期望哈希索引在第三列上表现出色,因为该列包含大且唯一的值。
创建索引后,我将设置提示位并收集统计信息,希望能获得良好的执行计划和可重复的测量结果:
VACUUM (ANALYZE) hashtest;
复制
之后,我将在一个DO语句中执行100000次索引扫描:
\timing on
DO
$$DECLARE
i bigint;
r text;
BEGIN
FOR i IN 1..100000 LOOP
SELECT * INTO r
FROM hashtest
-- for c3 and c4, we will use mkbig(i 100)
WHERE c1 = i 100;
END LOOP;
END;$$;
复制
基准测试结果
这个基准测试是在我的笔记本电脑上进行的,它配备了8个Intel Core i7 CPU核心,一块运行Linux 6.6.6和PostgreSQL 16.1的NVMe硬盘,配置与标准配置相同。为了得到索引修改或索引扫描的净时间,我将没有索引运行时的INSERT时间减去,并把INSERT开销和索引扫描时间除以迭代次数。所有测量都进行了三次,并选择了中位数值。
索引平均修改时间 | 索引平均扫描时间 | 索引大小 | |
hash index on c1 (unique bigint) | 11.25 μs | 5.06 μs | 324 MB |
b-tree index on c1 (unique bigint) | 1.31 μs | 5.46 μs | 214 MB |
hash index on c2 (repeating bigint) | 3.85 μs | 564.95 μs | 444 MB |
b-tree index on c2 (repeating bigint) | 0.93 μs | 10.32 μs | 63 MB |
hash index on c3 (unique 640-character text) | 11.67 μs | 8.68 μs | 325 MB |
b-tree index on c3 (unique 640-character text) | 54.63 μs | 15.61 μs | 964 MB |
hash index on c4 (repeating 640-character text) | 4.69 μs | 572.44 μs | 444 MB |
b-tree index on c4 (repeating 640-character text) | 13.06 μs | 532.55 μs | 71 MB |
对基准测试结果的讨论
对于bigint列,使用哈希索引在插入数据时比b-tree索引慢得多。对于重复值(c2),当查询表时,哈希索引也比较慢。唯一bigint列的select性能能够竞争。
当涉及到长文本列时,哈希索引在插入过程中比b-tree索引要好得多。这很容易解释,因为对一个4字节的哈希值进行索引要比对一个644字节的字符串(包括varlena头)更容易。
对于唯一字符串来说,哈希索引的查询性能也要好两倍。其原因在于,对于大索引条目,每个b-tree页只能容纳少量的索引条目,索引树变得非常窄并且很深。PostgreSQL必须遍历许多中间的b-tree页才能到达叶节点。对每个条目重复10000次的情况,b-tree索引能够竞争的原因有两个:
•在范围扫描时,PostgreSQL可以保持在叶页级别,并且不必反复遍历深度b-tree
•对于重复条目,PostgreSQL可以从v13引入的index
key deduplication(索引关键键去重)功能中受益 - 这会显著减小索引的大小,从而加快扫描索引的速度
结论
如果你认为哈希索引比b-tree索引更好,请不要忘记我构造了一个特别设计的异类测试案例,这个案例特别适合哈希索引。在普通情况下,b-tree索引的性能优于哈希索引。这并不一定是因为哈希索引从根本上比b-tree索引差:如果它们得到与b-tree索引同样多的关注和优化,情况可能会有所不同。我认为没有理由认为去重或底部索引元组删除(这是两个最近的b-tree索引性能特性)不能同样适用于哈希索引。
在当前的情况下,你可以安全地忽略哈希索引。如果你有类似需要快速进行自由形式长用户评论的等值搜索等疯狂要求,你可能会重新考虑。或者你可以在 hashtext(comment) 上创建一个b-tree索引,然后对其进行搜索。
点击文章底部“阅读原文”查看原文内容!
评论


