PostgreSQL 默认的 B-tree 索引支持多种常见需求,包括等值、范围、有序访问和唯一性约束;哈希索引提供的则是更窄的等值访问路径。两者的选择应先从需要支持的运算符与数据约束开始,再进入性能测量。说哈希“只要是等值就一定更快”,或者规划器“几乎从来不会选它”,都是用未经负载验证的概括代替具体问题。先确认能力,再比较成本,顺序不能颠倒。

先检查能力,再比较耗时

B-tree 按运算符类和排序规则保留键的顺序,因此既能定位单个等值键,也可能支持有界范围与 ORDER BY,省掉额外排序。它还可以为 IS NULL 和 IS NOT NULL 提供索引访问。不过,存在索引不等于强制使用索引;查询需要读取很大一部分表时,规划器仍可能选择其他路径。这并不改变索引在语义上支持哪些操作。

文本模式另有条件。像 LIKE 'abc%' 这样由常量构成、锚定开头的前缀,可以利用合适的 B-tree 路径,但排序规则和运算符类会影响适用性。非 C 区域设置下,可能需要专门支持模式匹配的运算符类。开头带通配符的匹配,也不会因为创建了普通 B-tree 就自动变成高选择性的有序范围。不能把“支持部分前缀匹配”写成“所有 LIKE 都能一趟索引解决”。

PostgreSQL 哈希索引保存四字节哈希码,而不是完整列值。它支持等值,不保留键顺序,只支持单个索引列,也不能检查唯一性。对于较大值,较小的索引条目可能有利于某些等值负载,但读取堆记录进行复核、访问桶和溢出页仍有成本。数据结构的名字本身不能证明某次真实查询谁会胜出。

坏掉的样子

把 B-tree 换成哈希索引,可能失去原有的范围或有序访问路径,也会失去 IS NULL 的索引访问能力:PostgreSQL 哈希索引不支持这个条件。更严重的是,把承担唯一性要求的索引替换成哈希索引,因为后者根本无法提供对应约束。即使点查偶尔更快,也不能用性能收益替代数据完整性契约。

空值语义需要单独核对。默认唯一 B-tree 允许多个空值,因为唯一性检查把它们视为彼此不同。如果应用要求最多只能出现一个空值,应在唯一约束或索引上明确使用 NULLS NOT DISTINCT,而不能从“唯一”两个字直接推导。索引支持怎样访问,与它强制哪些数据规则,是关联但不同的两项决定。验证时必须分别看查询结果和约束行为。

快速验证

在 PostgreSQL 18 的 psql 中运行下面实验。临时表含两万个整数键和两条键为空的记录,先建立哈希索引,再把它替换为唯一 B-tree。整个实验在事务内运行,最后回滚清理。

\set ON_ERROR_STOP on
BEGIN;
CREATE TEMP TABLE points (key integer,value text);
INSERT INTO points SELECT g,'value-'||g FROM generate_series(1,20000) AS g;
INSERT INTO points VALUES (NULL,'no-key-a'),(NULL,'no-key-b');
CREATE INDEX points_hash ON points USING hash(key);
ANALYZE points;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT value FROM points WHERE key=12345;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT key,value FROM points WHERE key>=12340 AND key<12350 ORDER BY key;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT value FROM points WHERE key IS NULL;
DO $$ BEGIN
  CREATE UNIQUE INDEX points_hash_unique ON points USING hash(key);
  RAISE EXCEPTION 'hash uniqueness unexpectedly supported';
EXCEPTION WHEN feature_not_supported THEN RAISE NOTICE 'hash cannot enforce uniqueness';
END $$;
DROP INDEX points_hash;
CREATE UNIQUE INDEX points_btree ON points(key);
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT value FROM points WHERE key=12345;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT key,value FROM points WHERE key>=12340 AND key<12350 ORDER BY key;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT value FROM points WHERE key IS NULL;
DO $$ BEGIN
  IF (SELECT value FROM points WHERE key=12345) IS DISTINCT FROM 'value-12345' OR
     (SELECT count(*) FROM points WHERE key>=12340 AND key<12350) <> 10 OR
     (SELECT count(*) FROM points WHERE key IS NULL) <> 2 THEN RAISE EXCEPTION 'index changed answers'; END IF;
END $$;
ROLLBACK;

PostgreSQL 18.6 实测中,哈希等值查询使用 points_hash,返回 value-12345。十个键的范围查询采用顺序扫描加排序,空值查询也采用顺序扫描,返回两行。尝试建立唯一哈希索引时,数据库抛出预期的不支持特性异常,代码块有意捕获这一种失败;如果发生别的错误,脚本仍然停止,不能把任意失败都解释成验证成功。

替换后,本夹具中的 B-tree 支持了等值、有界有序范围和空值查找。范围结果仍是十行,空值结果仍是两行。尽管有两个空值,唯一索引仍然建立成功,恰好说明默认的空值彼此不同语义。断言检查访问路径变化没有改变答案,避免把计划用了索引与结果正确混为一谈。

计划选择与页面计数会随表大小、缓存、统计和配置变化。本实验建立的是能力边界及一次实际选择,不是证明所有机器上某种方法都更快的基准测试。相反,哈希不能实现唯一性这一点,不会因为数据量或成本参数改变而突然消失。可优化的成本与不可省略的契约应分别处理。

测量完整负载

真实候选应同时比较有代表性的等值查询、范围查询、排序要求、写入活动、存储和维护开销。在受控环境比较替代方案,避免随手在生产上建立多个很大的竞争索引。哈希索引可能凭某个经过测量的等值负载证明价值,而 B-tree 仍因唯一性或其他查询必须保留。每个留下的索引都应对应实际访问或完整性要求,并把写入成本与读取收益一起评估。

来源