首页 / PostgreSQL 入门教程 / 索引基础(B-tree/唯一/多列)

PostgreSQL 入门教程

索引基础(B-tree/唯一/多列)

本教程共 50 篇 · 第 39 篇 · 更新于 2026-07-31 · 约 7 分钟阅读

PostgreSQLPostgreSQL 入门教程索引B-tree唯一索引多列索引

39. 索引基础(B-tree/唯一/多列)

本节目标:理解索引为什么能加速查询,掌握 CREATE INDEX、唯一索引、部分索引、多列索引的写法与查看方式。

为什么用索引

索引(INDEX)是一份”排好序的指针清单”。打个比方:查字典时翻拼音目录比一页页翻快得多,索引就是数据库的”目录”。

没有索引时,PostgreSQL 只能做”全表扫描”(Sequential Scan),一行行看过去。表越大越慢。有了索引,它能快速定位到目标行,再做一次”索引扫描”(Index Scan)。

Note

索引不是越多越好。它让 SELECT 变快,却让 INSERT / UPDATE / DELETE 变慢,因为每次改数据都要顺手维护索引。小表上用索引反而可能更慢,优化器会直接放弃索引去全表扫描。

默认索引类型:B-tree

PostgreSQL 默认用 B-tree 索引,适合等值和范围查询(=><BETWEENORDER BY)。如果不写 USING,就是 B-tree。

B-tree 是一种”平衡树”结构,数据按顺序排好,既能快速”点查”某个值,也能顺着叶子节点”扫一段范围”。所以它能同时服务等值条件和排序。

先建示例表 orders

postgres=# CREATE TABLE orders (
postgres=#   id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
postgres=#   user_id    INT,
postgres=#   amount     NUMERIC(10,2),
postgres=#   status     TEXT,
postgres=#   created_at TIMESTAMP DEFAULT now()
postgres=# );

amount 上建一个普通索引:

postgres=# CREATE INDEX idx_orders_amount ON orders (amount);

先准备好全书统一的示例表 users(本章的 orders 见上文):

postgres=# CREATE TABLE users (
postgres=#   id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
postgres=#   username   TEXT NOT NULL,
postgres=#   email      TEXT,
postgres=#   age        INT,
postgres=#   created_at TIMESTAMP DEFAULT now()
postgres=# );

唯一索引

唯一索引(Unique Index)既加速查询,又强制列值不重复,和 UNIQUE 约束效果一致。常用于邮箱、用户名这类不能重复的字段。

postgres=# CREATE UNIQUE INDEX idx_users_email ON users (email);

如果插入重复邮箱,会直接报错。

Tip

给已有表加唯一索引前,先确认列里没有重复值,否则 CREATE 会失败。可以先跑 SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1; 排查。

部分索引(Partial Index)

部分索引只对表里”一部分行”建索引,用 WHERE 子句限定。它体积更小、维护更轻,适合只频繁查某类数据的场景。

比如订单表大多已完结,你只常查”未支付”(status = ‘unpaid’)的订单:

postgres=# CREATE INDEX idx_orders_unpaid ON orders (created_at)
postgres=# WHERE status = 'unpaid';

这个索引只包含未支付订单,查询时同样能命中:

postgres=# SELECT * FROM orders
postgres=# WHERE status = 'unpaid'
postgres=# ORDER BY created_at;  -- 走 idx_orders_unpaid
Note

部分索引的 WHERE 条件必须出现在查询里,优化器才会用它。查询条件和索引 WHERE 对不上,索引就用不上。

外键列也该建索引

当你用 user_id 这种外键去 JOIN 或过滤时,给它建个索引能明显提速:

postgres=# CREATE INDEX idx_orders_user_id ON orders (user_id);
Warning

PostgreSQL 在创建外键时,只会在”被引用”的那一侧(父表的主键)自动建索引,不会在”引用方”(子表的 user_id)自动建。子表这边的索引得自己加,否则按外键查子表会全表扫描。

多列索引

多列索引(Multicolumn Index)把多列合在一个索引里,适合经常一起出现在 WHERE 里的组合条件。

postgres=# CREATE INDEX idx_orders_user_status ON orders (user_id, status);

这种索引遵循”最左前缀”原则:它能加速 (user_id)(user_id, status) 的查询,但单独用 status 做条件时,通常派不上用场。

postgres=# -- 能用上索引:最左列 user_id 在条件里
postgres=# SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
postgres=# -- 用不上索引:缺少最左列 user_id
postgres=# SELECT * FROM orders WHERE status = 'paid';
Warning

列的顺序很重要。把选择性高、常单独查询的列放前面。如果你既会单独按 user_id 查、又会按 user_id + status 查,把 user_id 放前面一个索引就够;反过来则不行。

查看与删除索引

用 psql 元命令查看表上的索引:

postgres=# \d orders

列出数据库中全部索引:

postgres=# \di

删除索引:

postgres=# DROP INDEX IF EXISTS idx_orders_amount;

主键和唯一约束会自动建隐式索引,不需要你手动再建。删掉约束时,对应的索引也会跟着删。

用 EXPLAIN 验证索引是否生效

别凭感觉建索引,用 EXPLAIN 看执行计划。有索引时是 Index Scan 或 Bitmap Heap Scan,没索引时是 Seq Scan:

postgres=# EXPLAIN SELECT * FROM orders WHERE amount = 99.00;

想看真实耗时,加 ANALYZE(它会真的执行一次查询):

postgres=# EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1;
Tip

先看 EXPLAIN 里是不是 Seq Scan。如果是,再判断是不是该建索引;如果表很小,优化器选全表扫描反而是对的,别硬加。

什么时候不该建索引

  1. 表很小(几百行以内),全表扫描本来就快。
  2. 写多读少,每次写入都要维护索引,得不偿失。
  3. 列的选择性很低,比如”性别”只有两个值,建索引意义不大。

不阻塞建索引:CONCURRENTLY

默认 CREATE INDEX 会锁表,建索引期间不能写入。生产环境的大表不能直接这么干。加 CONCURRENTLY 可以在建索引的同时继续读写:

postgres=# CREATE INDEX CONCURRENTLY idx_orders_amount ON orders (amount);
Warning

CONCURRENTLY 要扫两遍表,建得慢;而且它不能放在 BEGIN ... COMMIT 事务块里执行,必须单独跑。中途失败会留下一个 INVALID 状态的索引,用 DROP INDEX 删掉重来即可。

索引也会膨胀,要 REINDEX

索引不是一成不变的。大量 UPDATE / DELETE 后,索引里会留下”空洞”,体积变大、查询变慢。这时候用 REINDEX 重建:

postgres=# REINDEX INDEX idx_orders_user_id;
postgres=# REINDEX TABLE orders;  -- 重建整张表的所有索引

找出用不上的索引

建了一堆索引,到底哪些从没被用过?查系统视图 pg_stat_user_indexes

postgres=# SELECT indexrelname, idx_scan
postgres=# FROM pg_stat_user_indexes
postgres=# WHERE idx_scan = 0;  -- 扫描次数为 0,可能没用
Tip

idx_scan 为 0 的索引多半是浪费。但别急着删:先确认业务低峰真的没用到,有些索引只在月报、年审时才派上用场。

索引选择的经验法则

给你的索引决策列几条我常用的经验:第一,常用于 WHERE、JOIN、ORDER BY 的列优先建;第二,选择性越高越值得建;第三,写多读少的表克制建;第四,小表别建。实在拿不准,就 EXPLAIN 看慢查询,让优化器告诉你它有没有走索引,比拍脑袋强。

索引和约束的关系

顺带厘清:主键和唯一约束会自动建索引,所以你不用再手动建一遍。外键不会自动在子表建索引,得自己加。普通索引只加速查询,不保证唯一。把这几种关系理清楚,就不会重复建索引或漏建索引。

常见误区

  1. 索引越多查询越快。错,索引拖慢写入,还可能让优化器选错计划。
  2. 给外键忘了建索引,导致 JOIN 慢。
  3. 多列索引顺序乱放,结果后面的列永远用不上。
  4. 小表上也疯狂建索引,纯属负担。