排查慢查询时曾遇到一个典型案例:订单表在 user_id 和 status 两列上建了哈希索引,查询条件是 WHERE user_id = 42 AND status = 'paid' AND created_at > now() - interval '30 days',执行计划却仍然走了顺序扫描。原因很简单,多了一个 created_at 的范围过滤,哈希索引无法承担任何范围判断,整条索引就被优化器放弃了。哈希索引在 PostgreSQL 中并不是新技术,但它的能力边界比多数人预想的更窄,理解这些限制可以避免建了索引却用不上的尴尬。

哈希索引只认等值:能力边界要清楚
先看哈希索引的基本结构。PostgreSQL 为索引列的值计算一个哈希码,哈希码决定数据落入哪个桶,索引项只保存哈希码和对应的元组指针,并不保存原始字段值。等值查询时,数据库先算出条件的哈希码,定位桶,再比较桶内每个元组指针指向的堆元组,确认是否真正匹配。这个设计带来两个直接后果:第一,查询必须是精确的等值比较,即 = 或 IN 列表;第二,即使哈希码相同,也还要回到堆中核对原始值,因为不同值可能产生相同的哈希码。
正因为这个机制,<、>、BETWEEN、LIKE、正则匹配等条件都无法使用哈希索引。范围查询依赖值的顺序,而哈希码本身没有顺序,优化器不可能利用哈希索引做有序扫描。ORDER BY 排序、MIN/MAX 聚合同样如此。一个常见的错误是建了哈希索引后,查询里混入一个范围条件,结果整个索引失效,执行计划退化回顺序扫描或位图扫描。
可以用一个简单的 SQL 验证等值查询与范围查询的差异。先建表和哈希索引:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders (user_id, status, created_at)
SELECT (random() * 1000)::bigint, 'paid', now() - (random() * interval '90 days')
FROM generate_series(1, 100000);
CREATE INDEX idx_orders_user_hash ON orders USING hash (user_id);
下面两条查询,第一条会使用哈希索引,第二条会因为范围条件放弃索引:
EXPLAIN SELECT * FROM orders WHERE user_id = 42; EXPLAIN SELECT * FROM orders WHERE user_id = 42 AND created_at < now() - interval '30 days';
容易被忽略的硬性限制
多列哈希索引有一条很严格的要求:所有索引列都必须出现在等值条件中,缺一不可。这跟 B-tree 的最左前缀规则完全不同。比如一个 (user_id, status) 的哈希索引,查询如果只写 WHERE user_id = 42,虽然 user_id 是索引的第一列,但哈希索引仍然无法使用,因为哈希码是基于完整键计算的,缺少 status 就无法定位桶。只有 WHERE user_id = 42 AND status = 'paid' 这样的完整等值组合才能命中。
这意味着多列哈希索引的适用面非常窄,它不像 B-tree 复合索引那样可以顺带优化前缀列查询。如果业务里经常按单列过滤,又偶尔按多列组合过滤,通常需要为单列和多列分别建索引,哈希多列索引不能替代单列索引。
另一个硬性限制是哈希索引无法支撑唯一约束和主键。PostgreSQL 的唯一约束、主键、外键依赖唯一索引来保证不重复,而哈希索引只存哈希码和指针,无法在索引层判断唯一性,并且哈希碰撞会让唯一性检查变得不可靠。尝试创建唯一哈希索引会直接报错,提示访问方法不支持唯一索引:
CREATE UNIQUE INDEX idx_orders_user_unique_hash ON orders USING hash (user_id);
同时,哈希索引也不能提供仅索引扫描。B-tree 在查询只涉及索引列时,可以直接从索引返回结果,避免回表;哈希索引因为不保存原始列值,即便查询只需要 user_id,也必须回表读取堆元组才能获得真实值。对于高频的轻量查询,比如 SELECT user_id FROM orders WHERE user_id = 42,B-tree 的 Index Only Scan 往往比哈希索引更高效。
此外,早期 PostgreSQL 版本的哈希索引不写 WAL,崩溃后索引可能损坏,需要重建。这个问题在现代版本中已经解决,哈希索引在流复制和崩溃恢复方面与 B-tree 一样安全。不过这个历史包袱仍然让一些老用户对哈希索引持保留态度。
什么时候可以考虑哈希索引
哈希索引的最大优势在于索引体积和等值查找速度。由于只存哈希码和指针,不需要像 B-tree 那样维护多层树结构和排序键,对于宽度很大、重复较多的等值键列,哈希索引通常比 B-tree 更小。索引体积小意味着缓存命中率更高,写入时维护成本也可能更低。在只做单列等值查询、不需要排序和范围扫描的场景里,哈希索引值得测试。
但“值得测试”不等于“直接替换”。判断是否使用哈希索引,应该先用真实数据量和查询负载做对比。可以分别在同一个列上建立 B-tree 和哈希索引,比较索引大小与查询计划:
CREATE INDEX idx_orders_user_btree ON orders USING btree (user_id);
CREATE INDEX idx_orders_user_hash ON orders USING hash (user_id);
SELECT pg_size_pretty(pg_relation_size('idx_orders_user_btree')) AS btree_size,
pg_size_pretty(pg_relation_size('idx_orders_user_hash')) AS hash_size;
在这个例子中,大数据量下哈希索引通常会小一些。接着用 EXPLAIN ANALYZE 分别跑高频查询,对比执行时间与缓冲区命中。需要注意的是,如果查询只需要索引列,B-tree 的仅索引扫描会占明显优势;如果查询返回大量行,哈希索引也未必更快,因为大量回表访问同样耗时。
还有一个现实因素:PG 的优化器对哈希索引的支持已经比较成熟,但等值查询中 IN 条件会被展开成多个等值匹配,哈希索引可以处理包含 IN 的查询。不过如果 IN 列表很长,数据库可能选择位图索引扫描,此时哈希索引的收益需要具体观察。
使用哈希索引的监控与维护
哈希索引同样会产生膨胀,尤其是在更新和删除频繁的表中。PostgreSQL 的 VACUUM 可以回收死元组占用的空间,但哈希索引不像 B-tree 那样能高效地复用页内空间,长期频繁更新后索引可能会出现较多空洞。建议定期通过 REINDEX 重建索引,或者监控索引膨胀程度。
监控索引使用情况可以查询系统视图:
SELECT relname, indexrelname, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE relname = 'orders';
如果 idx_scan 长期为 0,说明这个哈希索引从未被查询计划使用,需要检查查询条件是否违背了等值限制,或者统计信息是否过期。批量导入数据后,应及时运行 ANALYZE 更新统计信息,帮助优化器正确估算成本。创建哈希索引时也可以指定填充因子:
CREATE INDEX idx_orders_user_hash_fill ON orders USING hash (user_id) WITH (fillfactor = 70);
最后记住一个原则:没有一种索引能覆盖所有查询模式。哈希索引只适合非常窄的等值查询场景,如果你的业务查询里有任何排序、范围、前缀匹配,或者需要唯一性约束,B-tree 仍应是默认选择。创建索引前多问一句“这个查询真的只有等值条件吗”,往往能避免一次无效索引带来的维护成本。
PostgreSQL哈希索引索引限制修改时间:2026-09-26 22:15:04