# JSONB · 关系库里长出一个文档库

> JSONB 把 JSON 以解析后的二进制格式存储，配上一整套运算符(-> ->> @> ？ ||)、GIN 索引和 SQL/JSON path 查询。它让你在同一张表里既有严格 schema 的列，又有灵活的文档字段——PostgreSQL 因此成了「关系 + 文档」混合库。

- 作者：David（道雾轩）
- 专栏：PostgreSQL 特性详解（https://daiw.org/manual/postgresql.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/postgresql/jsonb
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# JSONB · 关系库里长出一个文档库

十年前，「要灵活 schema 就上 MongoDB，要事务和 JOIN 就上关系库」是条铁律。JSONB 把这条铁律打破了：你可以在**同一张 PostgreSQL 表**里，一半是严格 schema 的关系列（有类型、有约束、能 JOIN），一半是自由生长的 JSON 文档字段——两边共享同一套事务、同一套备份、同一套查询语言。

## JSON 与 JSONB：差在「存之前解不解析」

PostgreSQL 有两个类型：`json` 和 `jsonb`。名字像，内核完全不同：

- `json`：**原样存文本**。保留空格、键顺序、重复键。每次查询要**重新解析**。
- `jsonb`：存**解析后的二进制**(decomposed binary)。丢掉无意义的空格、键去重、键有序。存的时候多花一点点解析成本，**换来读取时不必再解析**，而且能建索引。

<Callout type="info">
  除非你要「一字不差地存回原始 JSON 文本」（比如存第三方回调的原文留证），否则**一律用 `jsonb`**。它能索引、能高效查询，是把 JSON 当「数据」用的正确选择；`json` 更像是把 JSON 当「一段文本」存。
</Callout>

## 取值：-> 与 ->> 的一字之差

JSONB 的运算符第一眼有点密，但抓住一个规律就清楚了：**单箭头 `->` 返回 JSON，双箭头 `->>` 返回文本。**

```sql
-- -> 取到的还是 jsonb;->> 取到的是 text
SELECT doc->>'name' FROM people;
-- -> ['Alice', 'Bob', 'Carol']

-- #>> 按路径深入嵌套,取文本
SELECT doc#>>'{address,city}' FROM people;
-- -> ['Shanghai', 'Beijing', 'Shanghai']
```

- `->` / `->>`：按键或数组下标取一层。
- `#>` / `#>>`：按**路径数组** `{address,city}` 一步取到深层。
- 记法：**带第二个 `>` 的都「出文本」**，不带的「还在 JSON 世界里」，可以继续往下取。

## 查询：包含 @> 与键存在 ？

真正让 JSONB 好用的，是这几个能**走索引**的查询运算符：

```sql
-- @> 「包含」:tags 数组里含 "engineer" 的人
SELECT name FROM people WHERE doc->'tags' @> '"engineer"';
-- -> ['Alice', 'Carol']

-- ? 「键存在」:含 address 键的文档有几个
SELECT count(*) FROM people WHERE doc ? 'address';
-- -> 3
```

`@>`（包含）、`?`（键存在）、`?|`（存在任一键）、`?&`（存在所有键）——这些是 JSONB 查询的主力，而且都能被 GIN 索引加速。

## GIN 索引：两种 opclass 的取舍

给 JSONB 建 GIN 索引时，有两种运算符类(operator class)可选，这是个值得懂的取舍：

```sql
-- 默认 jsonb_ops:键和值都进索引,支持 @> ? ?| ?& 全套
CREATE INDEX idx_doc ON people USING gin (doc);

-- jsonb_path_ops:只索引「路径→值」,更省空间、@> 更快,但不支持 ? 系列
CREATE INDEX idx_doc_path ON people USING gin (doc jsonb_path_ops);
```

<Callout type="info">
  怎么选？**只用 `@>` 包含查询，就上 `jsonb_path_ops`**——索引更小、查询更快。**还要用 `?` 键存在查询，就用默认的 `jsonb_ops`**。底层原理：`jsonb_ops` 为每个键和每个值都建词条，覆盖面广但索引大；`jsonb_path_ops` 只为「完整路径 + 值」的哈希建词条，只能回答「包含」，但因此更紧凑。
</Callout>

## SQL/JSON path：标准的 JSONPath

PG 12 起支持 SQL 标准的 **JSON path** 语言，用 `@@` 匹配、`jsonb_path_query` 抽取：

```sql
-- 找 address.city = 'Shanghai' 的人
SELECT jsonb_path_query(doc, '$ ? (@.address.city == "Shanghai")')
FROM people;
-- -> ['"Alice"', '"Carol"']
```

`$` 是根，`?` 引出过滤条件，`@` 指当前节点。复杂的嵌套条件用 path 表达比堆一串 `->` 清爽得多。

## 更新：jsonb_set 与 || 合并

JSONB 是不可变值，「改」其实是**算出一个新值再写回**：

```sql
-- 给 Alice 的文档加一个 manager 字段
UPDATE people SET doc = jsonb_set(doc, '{manager}', '"Carol"')
WHERE doc->>'name' = 'Alice';
-- Alice.manager = Carol ✓

-- || 合并:给所有行并入 verified=true
UPDATE people SET doc = doc || '{"verified": true}'::jsonb;
-- 所有行都加上 verified=true ✓
```

`jsonb_set`（定点改路径）、`jsonb_insert`（插入）、`||`（浅合并）、`-`（删键）、`#-`（删路径）凑成一套不可变更新工具。

## 横向对比

| 能力 | PostgreSQL JSONB | MySQL JSON | Oracle | SQL Server |
| --- | --- | --- | --- | --- |
| 存储 | 解析后二进制 | 二进制（类似） | 21c 起原生 `JSON` | 文本 + `JSON` 类型 |
| 包含查询 `@>` | 有，GIN 加速 | `JSON_CONTAINS` 函数 | `JSON_EXISTS` | `JSON_QUERY/VALUE` |
| 专用索引 | **GIN（两种 opclass）** | 多值索引 / 函数索引 | 函数索引 / search index | 计算列 + 索引 |
| path 查询 | SQL/JSON path（标准） | JSONPath | SQL/JSON path | JSON path |
| 混合建模体验 | **最顺**：文档字段与关系列平权 | 尚可 | 逐版本追赶 | 逐版本追赶 |

各家都有 JSON 了，差距在**成熟度与索引**：PostgreSQL 的 `@>` + GIN 组合出现最早、生态最全，把「文档查询」做到了和关系查询同一个数量级的性能。

## 应用场景

- **半结构化 + 强 schema 混合**：订单主表用规范列（金额、状态、时间，有约束、能 JOIN），把「各渠道五花八门的元数据」丢进一个 `meta jsonb`，免得为每个渠道加一堆稀疏列。
- **配置 / 特性开关**：每行一个 `settings jsonb`，新增配置项不用改表结构。
- **事件 / 日志载荷**：结构多变的 payload 存 JSONB，`@>` + GIN 按内容检索。
- **API 响应留档**：第三方返回的原始 JSON 存下来，既能整取也能按字段查。

<Callout type="warn">
  别把 JSONB 当成「不用设计 schema 的借口」。凡是**查询频繁、需要约束、要和别的表 JOIN** 的字段，就该是**正式的列**。JSONB 是给那些「真的多变、真的稀疏」的部分用的。一张全是 JSONB 的表，等于把关系库当 KV 存，白白丢掉它最值钱的部分。
</Callout>

## 小结与下一章

JSONB 让 PostgreSQL 一脚跨进文档数据库的地盘，却不必牺牲事务、JOIN 和约束。它的价值不在「能存 JSON」——那谁都能——而在**能像查关系数据一样高效地查 JSON**：`@>` 包含、`?` 键存在、path 抽取，全都能 GIN 索引。

类型系统的最后一站，我们看两个「让列和约束更聪明」的特性：**范围类型**让数据库能强制「时段不重叠」，**生成列**让派生字段由数据库自动维护。👉 [范围类型与生成列：让约束替你把关](https://daiw.org/manual/postgresql/range-generated)。

## 参考文献

- [1] PostgreSQL 官方文档.「8.14. JSON Types.」https://www.postgresql.org/docs/current/datatype-json.html —— `json` 与 `jsonb` 的区别、索引建议。
- [2] PostgreSQL 官方文档.「9.16. JSON Functions and Operators.」https://www.postgresql.org/docs/current/functions-json.html —— 全部运算符、`jsonb_set`、SQL/JSON path。
- [3] PostgreSQL 官方文档.「8.14.4. jsonb Indexing.」https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING —— `jsonb_ops` 与 `jsonb_path_ops` 的取舍。
