PostgreSQLNotes

第 08 章:B-Tree 与 HASH

zjc 于 2026-01-08 发布

这是《PostgreSQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 B-Tree 是 PostgreSQL 默认索引,适合等值、范围和排序。HASH 索引只适合等值,适用面较窄。

8.1 B-Tree

CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);

B-Tree 适合:

  1. =
  2. >>=<<=
  3. BETWEEN
  4. IS NULL
  5. ORDER BY
  6. 前缀 LIKE 'abc%'

不适合:

  1. LIKE '%abc'
  2. %abc%
  3. 对列套函数后查询;
  4. 类型不匹配导致隐式转换;
  5. 低选择性大范围扫描。

8.2 复合索引

CREATE INDEX idx_orders_status_created
ON orders(status, created_at);

有效:

WHERE status = 'PAID'

WHERE status = 'PAID'
  AND created_at > now() - interval '1 day'

ORDER BY created_at

较弱:

WHERE created_at > now() - interval '1 day'

复合索引遵循左前缀原则。设计顺序通常是等值列在前、范围列在后。

8.3 覆盖索引

CREATE INDEX idx_orders_cover
ON orders(user_id, created_at)
INCLUDE (amount, status);

INCLUDE 列不参与排序,可用于 Index Only Scan。是否能只扫索引还取决于可见性映射和 vacuum 状态。

8.4 部分索引

只索引活跃订单:

CREATE INDEX idx_orders_open
ON orders(created_at)
WHERE status IN ('CREATED', 'PAID');

适合状态少部分占比、查询条件固定的场景。

8.5 表达式索引

CREATE INDEX idx_users_lower_email
ON users(lower(email));

查询必须使用相同表达式:

SELECT *
FROM users
WHERE lower(email) = 'alice@example.com';

适合函数值高频查询,但增加写入成本,并要求函数语义稳定。

8.6 唯一索引

CREATE UNIQUE INDEX uq_orders_order_no
ON orders(order_no);

部分唯一索引常配合软删除:

CREATE UNIQUE INDEX uq_users_email_active
ON users(email)
WHERE deleted_at IS NULL;

8.7 HASH 索引

CREATE INDEX idx_users_email_hash
ON users USING HASH (email);
维度 HASH B-Tree
等值 支持 支持
范围排序 不支持 支持
索引大小 可能更小 通常更大
适用面 较窄 通用

只有纯等值、基数高且 B-Tree 空间压力大时才值得评估 HASH。

8.8 索引维护

查看索引使用:

SELECT
    schemaname,
    relname,
    indexrelname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan;

重建:

REINDEX INDEX idx_orders_user_created;
REINDEX TABLE CONCURRENTLY orders;

删除索引前观察完整业务周期,避免只看低峰一天。

8.9 索引代价

  1. 写入放大;
  2. 存储成本;
  3. 更新热点;
  4. VACUUM 负担;
  5. 优化器选择成本。

索引不是越多越好。每个索引都应能对应明确查询。

本章小结

B-Tree 覆盖大多数等值、范围和排序需求。复合索引注意左前缀,表达式和部分索引能解决特殊模式,HASH 只适合纯等值。索引设计要同时考虑查询收益和写入代价。

思考题

  1. 为什么复合索引左前缀重要?
  2. INCLUDE 列和键列有什么区别?
  3. 部分索引适合什么场景?
  4. 表达式索引有什么限制?
  5. 如何发现无用索引?