索引类型大全 · 六种索引怎么选

PostgreSQL 内置 6 种索引——B-tree、Hash、GIN、GiST、SP-GiST、BRIN,各擅长不同的访问模式。再加上部分索引、表达式索引、覆盖索引这些「修饰」。选对索引类型,是让大表查得快的第一步;BRIN 在时序大表上能小到只有 B-tree 的几分之一。

作者 David更新于 第 9 篇(共 17 篇)

索引类型大全 · 六种索引怎么选

大多数数据库,「索引」基本等于「B-tree」。PostgreSQL 不一样:它内置 6 种索引,每一种针对不同的数据形状与访问模式。选对类型,同样的查询可能快几十倍、索引还小几倍。这一章把六种索引 + 三种「索引修饰」讲清楚,并给出选择的判断标准。

六种索引,各管一摊

索引擅长一句话原理
B-tree= < > BETWEEN ORDER BY默认;有序平衡树,范围与排序天然友好
Hash只做 = 等值哈希桶;PG 10 起写 WAL 后才生产可用
GIN数组 / JSONB / 全文检索等多值倒排索引:值 → 含它的行
GiST几何 / 范围 / 最近邻 / 自定义类型可扩展的平衡树,存的是「谓词」
SP-GiST非均匀分布(前缀树 / 四叉树)空间分区、非平衡结构
BRIN物理顺序≈值顺序的超大表每个块区间只记 min/max,索引极小

B-tree 是默认,90% 的场景够用。真正拉开差距的,是知道另外五种什么时候该上。

B-tree:默认,但别小看

有序、平衡、支持范围与排序——= < > BETWEEN ORDER BY IS NULL 全走它。它是 Lehman & Yao 那套高并发 B-tree 的工程实现,几乎所有主键、外键、常规查询列都靠它。你不指定索引类型时,建的就是 B-tree。

GIN:多值字段的倒排索引

前面数组、JSONB、全文检索三章的加速,靠的都是 GIN。它是倒排索引:不为「整行」建一条目,而是为字段里的每个元素 / 键 / 词元建「元素 → 行列表」的映射。所以 tags @> ARRAY['eng']、doc @> '...'、tsv @@ '...' 这类「含某值」的查询,直接查元素对应的行列表,不必全表扫。凡是一列里装了多个可检索单元(数组、JSONB、tsvector),就是 GIN 的地盘。

GiST / SP-GiST:空间、范围、最近邻

GiST 是一棵可扩展的平衡树,节点里存的是「谓词」(比如「这棵子树的所有几何图形都落在这个框里」)。它是 PostGIS 地理索引、范围类型 EXCLUDE 约束(见 范围与生成列)、以及「最近邻」查询(ORDER BY point <-> target LIMIT 5,找最近的 5 个)的底座。

SP-GiST(Space-Partitioned GiST)是它的非平衡版,适合天然分区、分布不均的数据:前缀树(文本 / IP)、四叉树(点)、k-d 树。数据分布越不均匀,它相对 GiST 越有优势。

BRIN:时序大表的「省钱利器」

BRIN(Block Range Index)是最该认识的一个「非主流」索引。它不为每一行建条目,而是把表按物理块分成一段段,每段只记这段里的 min/max。查 WHERE ts BETWEEN ... 时,它快速跳过「min/max 完全不沾边」的块区间。

它的杀手锏是小。看实测里同一张日志表上各索引的物理大小:

600 kB  logs_payload_gin           (JSONB GIN)
216 kB  logs_level_time_covering   (覆盖索引)
128 kB  logs_occurred_btree        (时间列 B-tree)
 40 kB  logs_errors_only           (部分索引)
 24 kB  logs_occurred_brin         (时间列 BRIN)   ← 同一列,只有 B-tree 的 1/5
616 kB  logs                       (表本体)

同样是给 occurred_at 列建索引,BRIN 只有 B-tree 的约 1/5,数据量越大差距越夸张(几十 GB 的表可能是几百倍)。

BRIN 生效有个前提:物理存储顺序要和列值顺序大致相关。时间序列天然满足——日志按时间追加,磁盘上早的在前、晚的在后。这时 BRIN 用极小的体积换来「跳过大片无关块」的能力。但如果数据是乱序插入的(比如按随机 UUID),BRIN 就几乎没用——每个块的 min/max 都横跨全域,跳不掉任何块。时序、单调追加的大表用 BRIN;乱序数据别用。

三种「索引修饰」:同样的类型,更省更快

索引类型之外,还有几种正交的「修饰」,常常比换类型更立竿见影:

① 部分索引(Partial) —— 只索引满足条件的行:

-- 只给 error 日志建索引:99% 的 info 日志根本不进索引
CREATE INDEX logs_errors_only ON logs (occurred_at) WHERE level = 'error';

实测里它只有 40 kB(全量 B-tree 是 128 kB)。当你总是只查一小部分行(未处理的任务、错误日志、未删除的记录),部分索引又小又快,是性价比极高的一招。

② 表达式索引(Expression) —— 给「函数的结果」建索引:

CREATE INDEX logs_msg_lower ON logs (lower(message));
-- 于是 WHERE lower(message) = '...' 能走索引(大小写不敏感查询)

③ 覆盖索引(Covering,INCLUDE) —— 把查询要读的列一起塞进索引,实现索引-only 扫描(不用再回表):

CREATE INDEX logs_covering ON logs (level, occurred_at) INCLUDE (user_id);
-- 查 level+时间排序、只要 user_id 时,全程读索引,不碰表

「回表」是性能的隐形杀手:普通索引扫描找到匹配行的位置后,还要去堆表(heap) 把整行捞出来读需要的列。覆盖索引把那几列直接放进索引页,于是「索引-only 扫描」全程不碰堆表,I/O 少一大截。代价是索引更大、写入更慢——用在读多、列少的热点查询上最划算。

实测最后验证了部分索引真的被选中:查「error 日志按时间倒序取 10 条」,planner 走的正是那个 40 kB 的 logs_errors_only——Index Scan Backward using logs_errors_only。

横向对比

能力PostgreSQLMySQL(InnoDB)OracleSQL Server
B-tree有有(主力)有有
倒排(多值)GIN有限(多值索引 8.0+)域索引全文专用
空间GiST / SP-GiSTR-tree(空间)R-tree空间索引
块范围BRIN❌ 无有(存储索引,商业版特性)Columnstore(不同思路)
部分索引有❌ 无有(基于函数变通)有(筛选索引)
表达式索引有8.0.13+有计算列 + 索引
覆盖索引 INCLUDE有靠联合索引有有

PostgreSQL 的索引「工具箱」是这几家里最全的:BRIN、部分索引、GIN 的多值倒排是 MySQL 明显缺的三块,分别对应时序大表、稀疏查询、数组/JSON/全文——都是实打实能省一个量级资源的场景。

应用场景

  • 时序 / 日志大表:主查询列用 BRIN(省几十倍空间),高频精确过滤列再叠一个部分索引。
  • 状态机 / 任务队列:WHERE status='pending' 的部分索引,只索引待处理的少数行。
  • 大小写 / 格式不敏感查询:lower(email)、(data->>'city') 用表达式索引。
  • 热点只读查询:高频的 SELECT a,b WHERE c=? 用覆盖索引做索引-only 扫描。
  • 数组 / JSONB / 全文:一律 GIN。

小结与下一章

「加索引」不该只是无脑 CREATE INDEX。选对类型(B-tree / GIN / BRIN / GiST)匹配数据形状,再叠上修饰(部分 / 表达式 / 覆盖)贴合查询模式,才是让大表查得快又不浪费空间的正解。PostgreSQL 给的这套工具箱,比大多数数据库都全。

索引让「查得快」,但当单表大到几亿行,光靠索引也扛不住——这时要把大表物理拆小。下一章看声明式分区。👉 表分区:把一张大表拆成很多小表。

参考文献

本页目录