第二节 GIN 索引与 JSONB 查询优化

目标:理解 GIN 索引的工作原理,并掌握如何为 JSONB 字段创建和使用 GIN 索引,以实现对 JSON 文档的高效查询。

JSONB 的查询性能优势,很大程度上来源于GIN(Generalized Inverted Index,通用倒排索引) 的支持。没有索引,对 JSONB 字段的查询(例如,查找某个键是否存在,或某个值是否匹配)将导致全表扫描,在数据量大时是灾难性的。


什么是倒排索引?

在理解 GIN 之前,我们先回顾一下“倒排索引”的概念。这与搜索引擎中的原理非常相似。

  • 正向索引:像书的目录,从“页码”(或行ID)指向“内容”。
  • 倒排索引:像书末尾的“关键词索引”,从“关键词”(或 JSONB 中的键/值)指向它出现的“页码”(行ID)。

示例: 假设我们有以下 JSONB 数据:

  • 行1: {"tags": ["a", "b"]}
  • 行2: {"tags": ["b", "c"]}

一个针对 tags 数组的 GIN 索引会创建类似这样的结构:

  • "a" -> 行1
  • "b" -> 行1, 行2
  • "c" -> 行2

当你查询 tags 包含 "a" 的文档时,PostgreSQL 可以直接通过索引找到“行1”,而无需扫描整张表。


JSONB 创建 GIN 索引

JSONB 字段创建 GIN 索引的语法非常简单。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- 准备一个用于存储设备信息的表
CREATE TABLE devices (
    id SERIAL PRIMARY KEY,
    attributes JSONB
);

INSERT INTO devices (attributes) VALUES
('{"type": "sensor", "location": "room1", "tags": ["temp", "humidity"]}'),
('{"type": "switch", "location": "room2", "tags": ["power", "smart"]}'),
('{"type": "sensor", "location": "room2", "tags": ["temp", "pressure"]}');

-- 在整个 JSONB 字段上创建 GIN 索引
CREATE INDEX idx_devices_attributes_gin ON devices USING GIN (attributes);

这个索引会索引 attributes 字段中所有的键和值


GIN 索引支持的操作符

创建了 GIN 索引后,并不是所有的 JSONB 操作都能利用它。只有特定的操作符才能触发索引扫描。

操作符描述示例是否使用索引
@>A 包含 B (A contains B)attributes @> '{"type": "sensor"}'
<@A 被 B 包含 (A is contained by B)'{"type": "sensor"}' <@ attributes
?顶层键是否存在attributes ? 'location'
`?`是否存在任意一个顶层键`attributes ?
?&是否存在所有这些顶层键attributes ?& ARRAY['type', 'location']
->按键获取 JSON 对象字段attributes->'type' = '"sensor"'
->>按键获取文本attributes->>'type' = 'sensor'

最重要的结论: 要让 JSONB 查询走 GIN 索引,必须优先使用 @> (包含) 操作符


查询优化实战

让我们对比一下使用和不使用索引优化操作符的区别。

场景:查找所有类型为 “sensor” 的设备

错误的方式(无法使用索引):

1
2
EXPLAIN (ANALYZE)
SELECT * FROM devices WHERE attributes->>'type' = 'sensor';

执行计划会显示 Seq Scan (全表扫描)。

正确的方式(高效使用索引):

1
2
EXPLAIN (ANALYZE)
SELECT * FROM devices WHERE attributes @> '{"type": "sensor"}';

执行计划会显示 Bitmap Heap ScanBitmap Index Scan,证明成功使用了 GIN 索引。

@> 操作符的强大之处在于它可以匹配任意深度的、任意复杂的 JSON 结构。

示例:查找所有位于 “room2” 并且标签包含 “temp” 的传感器

1
2
3
4
5
SELECT * FROM devices
WHERE attributes @> '{
    "location": "room2",
    "tags": ["temp"]
}';

这个查询会高效地利用 GIN 索引,一次性完成所有条件的匹配。


特殊 GIN 索引:jsonb_path_ops

默认的 GIN 索引会索引所有的键和值,这可能导致索引体积较大。如果你只关心对特定路径的查询,可以使用 jsonb_path_ops 操作符类来创建更小、更专注的索引。

1
2
3
4
5
6
-- 删除旧索引
DROP INDEX idx_devices_attributes_gin;

-- 创建一个只支持 @> 操作符的索引
CREATE INDEX idx_devices_attributes_gin_path_ops ON devices
USING GIN (attributes jsonb_path_ops);

jsonb_path_ops 创建的索引只支持 @><@ 操作符,但它的体积更小,对于只使用包含查询的场景,性能可能更高。


📌 小结

  • GIN 索引JSONB 高性能查询的基石,它通过倒排列表快速定位包含特定键或值的文档。
  • 要利用 GIN 索引,必须使用特定的操作符,其中最重要、最常用的是 @> (包含) 操作符
  • 应避免在 WHERE 子句中使用 ->->> 进行过滤,因为它们无法利用 GIN 索引。请将这类查询改写为 @> 的形式
  • jsonb_path_ops 可以创建更小、更专注的 GIN 索引,适用于只进行包含查询的场景。

掌握了 GIN 索引,你就掌握了 JSONB 查询优化的精髓,也就真正开启了在 PostgreSQL 中高效使用 NoSQL 功能的大门。