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

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

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

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

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

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

PostgreSQL 有两个类型:json 和 jsonb。名字像,内核完全不同:

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

除非你要「一字不差地存回原始 JSON 文本」(比如存第三方回调的原文留证),否则一律用 jsonb。它能索引、能高效查询,是把 JSON 当「数据」用的正确选择;json 更像是把 JSON 当「一段文本」存。

取值:-> 与 ->> 的一字之差

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

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

-- #>> 按路径深入嵌套,取文本
SELECT doc#>>'{address,city}' FROM people;
-- -> ['Shanghai', 'Beijing', 'Shanghai']
  • -> / ->>:按键或数组下标取一层。
  • #> / #>>:按路径数组 {address,city} 一步取到深层。
  • 记法:带第二个 > 的都「出文本」,不带的「还在 JSON 世界里」,可以继续往下取。

查询:包含 @> 与键存在 ?

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

-- @> 「包含」: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)可选,这是个值得懂的取舍:

-- 默认 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);

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

SQL/JSON path:标准的 JSONPath

PG 12 起支持 SQL 标准的 JSON path 语言,用 @@ 匹配、jsonb_path_query 抽取:

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

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

更新:jsonb_set 与 || 合并

JSONB 是不可变值,「改」其实是算出一个新值再写回:

-- 给 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 JSONBMySQL JSONOracleSQL Server
存储解析后二进制二进制(类似)21c 起原生 JSON文本 + JSON 类型
包含查询 @>有,GIN 加速JSON_CONTAINS 函数JSON_EXISTSJSON_QUERY/VALUE
专用索引GIN(两种 opclass)多值索引 / 函数索引函数索引 / search index计算列 + 索引
path 查询SQL/JSON path(标准)JSONPathSQL/JSON pathJSON path
混合建模体验最顺:文档字段与关系列平权尚可逐版本追赶逐版本追赶

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

应用场景

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

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

小结与下一章

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

类型系统的最后一站,我们看两个「让列和约束更聪明」的特性:范围类型让数据库能强制「时段不重叠」,生成列让派生字段由数据库自动维护。👉 范围类型与生成列:让约束替你把关。

参考文献

本页目录