JSONB · 关系库里长出一个文档库
JSONB 把 JSON 以解析后的二进制格式存储,配上一整套运算符(-> ->> @> ? ||)、GIN 索引和 SQL/JSON path 查询。它让你在同一张表里既有严格 schema 的列,又有灵活的文档字段——PostgreSQL 因此成了「关系 + 文档」混合库。
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 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 存下来,既能整取也能按字段查。
别把 JSONB 当成「不用设计 schema 的借口」。凡是查询频繁、需要约束、要和别的表 JOIN 的字段,就该是正式的列。JSONB 是给那些「真的多变、真的稀疏」的部分用的。一张全是 JSONB 的表,等于把关系库当 KV 存,白白丢掉它最值钱的部分。
小结与下一章
JSONB 让 PostgreSQL 一脚跨进文档数据库的地盘,却不必牺牲事务、JOIN 和约束。它的价值不在「能存 JSON」——那谁都能——而在能像查关系数据一样高效地查 JSON:@> 包含、? 键存在、path 抽取,全都能 GIN 索引。
类型系统的最后一站,我们看两个「让列和约束更聪明」的特性:范围类型让数据库能强制「时段不重叠」,生成列让派生字段由数据库自动维护。👉 范围类型与生成列:让约束替你把关。
参考文献
- [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的取舍。