小美是一位经验丰富的空间整理师,专门帮人把杂乱的老房子改造成收纳有序的温馨小家。以前她最常用的工具是“定制衣柜”——量好尺寸,钉死隔板,每个格子放什么、放多少,入住前就安排得明明白白。可后来客户的需求变了:今天多买几个包包,明天换季多出十几件大衣,固定格子根本装不下。于是小美换了一种思路——在房间里摆上一排“智能收纳箱”。箱子外面是统一尺寸,里面却能塞进任何形状的物品;每个物品都有标签,一台智能盘点器扫一眼目录,几秒就能找到想要的东西。这套收纳哲学,正是 PostgreSQL jsonb 的精髓。 在传统关系型数据库里,每张表的结构就像定制衣柜,列的类型和长度提前锁死,想加一个字段就要 ALTER TABLE,动辄锁表。而 jsonb 就像那只万能收纳箱:把一个 JSON 文档整体放进一个列,键值对想加就加、想删就删。更厉害的是,PostgreSQL 还为它配上了 GIN 索引和丰富的操作符,让“在箱子里找东西”这件事,比想象中快得多。这篇文章,就带你从这套“收纳系统”里,彻底读懂 jsonb 的存储、查询、索引与实战。

岗位关联

掌握 jsonb,意味着你在关系型数据库中获得了“灵活的 schema 且不失性能”的能力,是处理动态字段、替代 EAV 模型的关键武器,也是从“数据库使用者”迈向“数据架构师”的必备技能。

目录
  1. 可视化演示
  2. jsonb存储原理
  3. 查询操作符
  4. GIN索引机制
  5. 字段操作函数
  6. 半结构化场景
  7. EAV对比替代
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

jsonb存储原理

定制衣柜的格子尺寸是装修前定的,改一次要拆墙。小美后来换的智能收纳箱,外层看起来都一样,但内层是可以拉扯的软隔层,任何形状的物件都能塞进去,塞完摁下压缩扣,体积瞬间小了一半。PostgreSQL 里的 jsonb,就是数据库世界的这种“收纳箱”。

jsonb 不是一个文本列,而是一种原生的二进制存储格式。当你执行 INSERT INTO t (attrs) VALUES ('{"name":"Alice","age":30}'::jsonb) 时,PostgreSQL 会先对 JSON 做词法解析,再以二进制表示落盘。存储过程中它做了一整套“收纳压缩”:键名会被排序,重复键只保留最后一个,数字统一规范化,多余空白全部丢弃。

由于键被排序,语义相同的 JSON 在 jsonb 里会变成完全相同的字节序列,比较操作极快;键值对以类似哈希表的方式组织,取单个键的值无需全文档扫描。这套设计让 jsonb 在“灵活存储”和“高效访问”之间取得了平衡——这正是它取代 json 文本类型成为主流的原因。

正因为有了“压一压、排排序、去掉填充物”的收纳工序,小美的智能收纳箱才能装得更多、找得更快。json 类型就像一只没拆包装的原封箱——塞进去是什么样、拿出来还是什么样,省了整理的功夫,却换来了体积和检索的代价。

J={(k1,v1),(k2,v2),…,(kn,vn)}J = \{(k_1, v_1), (k_2, v_2), \ldots, (k_n, v_n)\}
  1. 键值对集合$J$ 表示一个 jsonb 文档;在 PostgreSQL 内部,JSON 对象被建模为键值对的无序集合 $k_i \to v_i$,存储时按键名排序,方便后续二分查找。
    J={(k1,v1),(k2,v2),…,(kn,vn)}J = \{(k_1, v_1), (k_2, v_2), \ldots, (k_n, v_n)\}
  2. 键唯一约束$\forall i \neq j, \ k_i \neq k_j$ 表示同一对象内键名不允许重复;若输入 JSON 中同一个键出现多次,jsonb 会在解析阶段自动合并,只保留最后一个值。
    ∀i≠j, ki≠kj\forall i \neq j, \ k_i \neq k_j
  3. 语义相等判定$J_1 = J_2 \iff \forall k, \ J_1[k] = J_2[k]$ 说明 jsonb 的比较只关心“每个键对应的值是否相等”,与书写顺序、空白、缩进完全无关。
    J1=J2  ⟺  ∀k, J1[k]=J2[k]J_1 = J_2 \iff \forall k, \ J_1[k] = J_2[k]

查询操作符

有了收纳箱,取东西靠“手势”。小美总结了几种手势:平拿、翻开看标签、按路径一路摸到角落。这些手势对应 jsonb 的操作符家族。

-> 按键取子值,结果仍是 jsonb;->> 将值转为文本。二者是入门最常用的兄弟操作符。若要深入嵌套结构,#> 和 #>> 接受一个文本数组路径,例如 data #> '{user,address,city}' 一次取到城市字段。此外,@> 判断左侧文档是否包含右侧的键值对(子集语义),? 判断某个键是否存在,?|、?& 处理多个键的存在判断,这些操作符也是后面 GIN 索引的“服务对象”。

手势虽然多,本质就两类:要“带包装的盒子”还是“拆开后的内容”。-> 和 #> 返回盒子的 jsonb 形态,可继续拆;->> 和 #>> 把内容直接摊在桌面上,变成普通字符串。

J->k返回 jsonb 子值,J->>k返回 text 值J \mathbin{\text{->}} k \quad \text{返回 jsonb 子值}, \qquad J \mathbin{\text{->>}} k \quad \text{返回 text 值}
  1. 键提取$J \mathbin{\text{->}} k$ 从文档 $J$ 中取出键 $k$ 对应的值,结果是 jsonb 类型,可继续链式调用 -> 深入子字段,像打开一个盒子再打开里层的小盒。
    J->kJ \mathbin{\text{->}} k
  2. 文本转换$J \mathbin{\text{->>}} k$ 与 -> 的路径相同,但结果被转换成 SQL 的 text 类型,方便与字符串函数和 = 比较配合,相当于拆开包装把内容直接摊在桌上。
    J->>kJ \mathbin{\text{->>}} k
  3. 路径访问$J \mathbin{\text{#>>}} p$ 中的 $p$ 是文本数组路径,如 '{user,age}',一次抵达嵌套位置,等价于多次 ->> 的组合,像按着楼栋、单元、房门一路摸到最终目标。
    J#>>pJ \mathbin{\text{\#>>}} p

GIN索引机制

收纳箱多了以后,翻找变成体力活。小美购置了一台“扫码盘点器”:每个箱子入柜时,机器扫描全部标签,生成一本总目录。找东西时先翻目录定位箱子,再精准打开。

PostgreSQL 为 jsonb 提供的 GIN(Generalized Inverted Index,广义倒排索引)就是这个“扫码盘点器”。它把每个文档中的键和值拆成词条,在索引中记录“哪个词条出现在哪些行”。执行 WHERE attrs @> '{"tags":"vip"}' 这样的查询时,优化器直接从索引目录定位候选行,而不是全表扫描。

默认的 GIN 索引支持 @>、?、?|、?& 等操作符。需要注意的是,->> 取文本这种操作默认无法直接使用 GIN,通常需要建表达式索引,例如 CREATE INDEX ON t (attrs -> 'name')。这就是“盘点器只能认标签,不能替你拆箱数物品”的道理。

扫码盘点器并不是万能的——它能瞬间告诉你“哪个箱子里贴了 VIP 标签”,但没法替你拆开箱子数物品。所以 GIN 索引只服务那几个“标签类”操作符,取值类的查询得靠别的招。

posting(k)={d∈D∣k∈keys(d)}\text{posting}(k) = \{ d \in D \mid k \in \text{keys}(d) \}
  1. 倒排记录$\text{posting}(k)$ 表示键 $k$ 的倒排列表,其中 $D$ 是全部文档集合,$\text{keys}(d)$ 是文档 $d$ 的键集合;GIN 索引为每个键维护这样一个列表,正如目录中每个标签后跟着一串箱子编号。
    posting(k)={d∈D∣k∈keys(d)}\text{posting}(k) = \{ d \in D \mid k \in \text{keys}(d) \}
  2. 候选交集$\text{candidates}(q) = \bigcap_{k \in \text{keys}(q)} \text{posting}(k)$ 表示查询条件 $q$ 含多个键时,取各键倒排列表的交集,快速缩小扫描范围,相当于“同时贴有 VIP 和 2024 标签的箱子”两个清单求重合部分。
    candidates(q)=⋂k∈keys(q)posting(k)\text{candidates}(q) = \bigcap_{k \in \text{keys}(q)} \text{posting}(k)
  3. 回表确认$\text{final}(q) = \{ d \in \text{candidates}(q) \mid d \supseteq_{\text{json}} q \}$ 表示索引只负责粗筛,最终还要回表用精确的 jsonb 包含语义校验,防止误报,避免目录记录的标签与箱内实际物品不一致。
    final(q)={d∈candidates(q)∣d⊇jsonq}\text{final}(q) = \{ d \in \text{candidates}(q) \mid d \supseteq_{\text{json}} q \}

字段操作函数

箱子用久了,里面的东西需要打理:换掉过期的标签、盘点头寸、清掉没用的物品。小美的工具箱里有一组“整理工具”,对应 PostgreSQL 的 jsonb 函数。

jsonb_set(j, path, new_value) 按路径替换或新增值,与直接改一个 JSON 对象不同,它会返回一个新文档,原文档保持不变,符合“不可变”的更新哲学;jsonb_each 把对象展开成键值两列的表,方便与关系查询组合;jsonb_array_length 统计数组元素个数;- 操作符按键删除。这些工具让“整箱维护”不再是一件只能整体推倒重来的笨重事。

小美的工具箱准则:能用的工具绝不多占地方。jsonb_set 负责“精准替换”,jsonb_each 负责“整体盘点”,jsonb_array_length 负责“快速清点”,删除键用 - 一把抽出。

jsonb_set(j,p,v)=j′满足j′[p]=v\text{jsonb\_set}(j, p, v) = j' \quad \text{满足} \quad j'[p] = v
  1. 路径定位$p$ 是一个文本数组形式的路径,例如 '{user,age}';函数沿路径逐层走进文档结构,像拿着楼层索引逐级下探到具体房间。
    jsonb_set(j,p,v)=j′\text{jsonb\_set}(j, p, v) = j'
  2. 值替换$j'[p] = v$ 表示在新文档 $j'$ 中,路径 $p$ 处的值被改为 $v$;若路径不存在,则按 create_missing 参数决定是否创建新键,相当于房间里没有这个柜子时现场打一个。
    j′[p]=vj'[p] = v
  3. 不可变性$j'$ 是全新的 jsonb 值,原 $j$ 不受影响;这类似函数式语言中“更新返回新值”的惯例,也让 PostgreSQL 的多版本并发控制机制天然适配。
    j′=jsonb_set(j,p,v)且原文档 j 不变j' = \text{jsonb\_set}(j, p, v) \quad \text{且原文档 } j \text{ 不变}

半结构化场景

很多业务数据天生“胖瘦不一”:同样是商品,衣服有尺码色号,数码产品有内存容量,食品有保质期。要是全建成普通列,表结构会膨胀成满是 NULL 的“千孔衣柜”。小美给客户的建议是:固定部分用定做格,变化部分放进收纳箱。

这就是“固定列 + jsonb 动态列”的混合设计。主键、时间、状态等高频且稳定的字段保留为普通列,享受类型约束和传统索引;产品规格、用户偏好等稀疏且多变的属性放进一个 jsonb 列,随用随增,无需每次 ALTER TABLE。动态字段变得像可变形的家具模块,业务变了只换箱内的标签。

为了不让箱子变成“黑洞”,PostgreSQL 允许在 jsonb 列上加 CHECK 约束和触发器来校验内部结构,例如强制必须包含 name 字段、age 必须是正整数等。这样灵活性没有妥协,数据质量也有了底线——就像收纳箱虽能装万物,但依然要在箱外贴一张物品清单,防止乱塞。

这就像小美设计的“变形家具”——底座永远是那几块标准板材,加个抽屉、加块层板、加个挂杆,随手拼装。业务变了,塞进箱子的属性变了,房子(表结构)却不用拆墙。

r=(a1,a2,…,an)⏟固定列∪{(b1,v1),(b2,v2),…}⏟jsonb 动态属性r = \underbrace{(a_1, a_2, \ldots, a_n)}_{\text{固定列}} \cup \underbrace{\{(b_1, v_1), (b_2, v_2), \ldots\}}_{\text{jsonb 动态属性}}
  1. 固定部分$(a_1, a_2, \ldots, a_n)$ 是关系中类型明确、查询频繁、需要外键或唯一约束的列,它们构成表的“骨架”,像衣柜里固定的横梁和挂杆。
    (a1,a2,…,an)(a_1, a_2, \ldots, a_n)
  2. 动态部分$\{(b_1, v_1), (b_2, v_2), \ldots\}$ 是 jsonb 列中按需添加的键值对,不同行的键集合可以不同,像同一个衣柜中不同客户挂不同的包包和配饰。
    {(b1,v1),(b2,v2),…}\{(b_1, v_1), (b_2, v_2), \ldots\}
  3. 并集语义$r$ 表示一行记录完整的信息 = 固定列与动态属性的并集;查询时固定列走普通索引,动态属性走 GIN 或表达式索引,各司其职。
    r=固定列∪动态属性r = \text{固定列} \cup \text{动态属性}

EAV对比替代

说到“动态属性”,老派的方案叫 EAV(Entity-Attribute-Value,实体-属性-值)。它把所有属性拆成三列一行行排:实体 ID、属性名、属性值。听上去很灵活,但小美一看直摇头——这等于把一把椅子拆成螺丝、木板、布垫,全堆进仓库,用的时候再现场组装。

EAV 查询取一个实体的多个属性,需要做多次自连接:要取 name、age、tags 三个属性,就得把 EAV 表自己连接两三次。属性越多,连接越多,查询越慢。而 jsonb 把同一个实体的所有属性装在一个包里,查询只是“打开包、对标签”一次动作。

因此,在属性集合中等规模(几十到上百个键)、且查询通常针对整个对象而非单属性时,jsonb 是替代 EAV 的理想选择。但如果属性本身需要关联外部字典、需要粒度级的权限控制,EAV 模式仍有存在价值——不是所有旧家具都该丢,有的古董确实要拆开才能修复。

小美常对客户说:与其把椅子拆了到处找螺丝,不如直接搬整把椅子过去。EAV 是“拆了再装”,jsonb 是“整箱搬运”,当你要的是“完整对象”时,后者显然更省力。

QEAV=σattr=a1(E)⋈σattr=a2(E)⋈⋯⋈σattr=an(E)Q_{\text{EAV}} = \sigma_{\text{attr}=a_1}(E) \bowtie \sigma_{\text{attr}=a_2}(E) \bowtie \cdots \bowtie \sigma_{\text{attr}=a_n}(E)
  1. 单属性选择$\sigma_{\text{attr}=a_i}(E)$ 先从 EAV 表 $E$ 中筛出属性 $a_i$ 的所有记录,得到一张“只有该属性”的临时表,像从零件堆里先挑出所有螺丝。
    σattr=a1(E)\sigma_{\text{attr}=a_1}(E)
  2. 多表连接$\bowtie$ 是将这些临时表按实体 ID 连接在一起;取 $n$ 个属性就需要 $n-1$ 次自然连接,开销随属性数线性增长,像每取一种零件就要把半成品拆开重装一次。
    σattr=a1(E)⋈σattr=a2(E)\sigma_{\text{attr}=a_1}(E) \bowtie \sigma_{\text{attr}=a_2}(E)
  3. jsonb 一次过滤$Q_{\text{JSONB}} = \sigma_{\text{doc} \mathbin{\text{@>}} \{a_1:v_1, a_2:v_2\}}(T)$ 只需扫描主表一次,在 jsonb 列上匹配全部键值对,零次连接。
    σdoc@>{a1:v1,a2:v2}(T)\sigma_{\text{doc} \mathbin{\text{@>}} \{a_1:v_1, a_2:v_2\}}(T)

AI 应用场景

用户画像平台

阶段:数据存储与特征管理

解决的问题:用户画像的标签体系几乎每周都在变:这周新增“偏好宠物”,下周新增“兴趣运动”。用传统表结构每加一个标签就要 ALTER TABLE 加列,在千万级用户表上代价极高。改用“主键 + jsonb 标签列”后,新增标签只是往 jsonb 里写一个键,无需动表结构,像往已贴好标签的收纳箱里再塞一个小物件。

具体案例:某推荐系统团队将用户特征表拆为 user_id、updated_at 和 profile jsonb 三列。特征平台通过 jsonb_set 增量更新单个特征,用 GIN 索引加速“找出所有喜欢猫的用户”这类标签查询。

电商商品规格管理

阶段:元数据管理与多租户设计

解决的问题:不同品类商品规格差异极大,手机有“运行内存”,服装有“尺码”,食品有“保质期”。EAV 方案在商品数千万、属性上百时会陷入连接地狱。jsonb 将规格作为整体存储,既保留品类差异,又能用 @> 快速筛选满足多条件的商品。

具体案例:某电商后台将 spu、sku 表增加 spec jsonb 字段,查询“红色 且 内存≥128G 且 有货”的手机时,一条 SQL 加 GIN 索引即可完成,替代了原先 5 次 EAV 自连接,查询耗时从秒级降到毫秒级。

埋点事件日志

阶段:数据采集与湖仓入湖

解决的问题:埋点事件字段由前端版本决定,一个老版本 App 可能上报旧字段,新版本则加了新字段。用固定表接收会频繁变更 schema;直接用文本 JSON 又无法查询。jsonb 列可以安全接收形态自由的原始事件,后续用 jsonb_each 或表达式索引展开分析。

具体案例:某数据平台将客户端上报的 event_data 存为 jsonb,配合分区表和 GIN 索引,支持按事件属性(如 platform、version)快速过滤,再通过 jsonb_array_length 统计一次会话内的事件数量。

Python 演示

python69 行
"""模拟 PostgreSQL jsonb 核心行为的小工具"""import jsonfrom typing import Any  def _contains(subset: Any, superset: Any) -> bool:    """递归判断 subset 是否被 superset 包含(模拟 @> 操作符)"""    if isinstance(subset, dict) and isinstance(superset, dict):        return all(            k in superset and _contains(v, superset[k])            for k, v in subset.items()        )    if isinstance(subset, list) and isinstance(superset, list):        return any(_contains(subset_item, superset_item)                   for subset_item in subset                   for superset_item in superset)    return subset == superset  class JSONB:    """一个小型 jsonb 模拟器,演示 PostgreSQL 的常用 JSON 操作"""     def __init__(self, data: Any):        self._data = data     def get_path(self, *keys: str) -> Any:        """模拟 #>> 路径查询,返回最终值"""        cur = self._data        for k in keys:            cur = cur[k]        return cur     def contains(self, query: dict) -> bool:        """模拟 @> 包含查询"""        return _contains(query, self._data)     def set_path(self, keys: tuple, value: Any) -> "JSONB":        """模拟 jsonb_set:按路径把值替换为新值"""        cur = self._data        for k in keys[:-1]:            cur = cur[k]        cur[keys[-1]] = value        return JSONB(self._data)     def to_json(self) -> str:        return json.dumps(self._data, ensure_ascii=False)  def main() -> None:    doc = JSONB({        "user": {"name": "Alice", "age": 30},        "tags": ["vip", "gold"],        "score": 95    })     print("原文:", doc.to_json())    print("路径查询 user.name:", doc.get_path("user", "name"))    print("包含查询 {'score': 95}:", doc.contains({"score": 95}))    doc.set_path(("score",), 96)    print("更新后:", doc.to_json())    print("包含查询 {'score': 96}:", doc.contains({"score": 96}))     # 模拟半结构化场景:动态追加新属性    doc.set_path(("level",), "gold")    print("追加动态字段:", doc.to_json())  if __name__ == "__main__":    main()
目的:用纯 Python 模拟 PostgreSQL jsonb 的路径查询、包含判断与按路径修改三大核心能力,帮助理解 jsonb 的语法语义。
  1. def _contains(subset: Any, superset: Any) -> bool:递归函数,模拟 SQL 中的 @> 包含操作符,逐层比对字典与列表。
  2. k in superset and _contains(v, superset[k])对查询条件中的每个键都去目标文档中查找,全部命中才算包含。
  3. class JSONB:定义一个模拟 jsonb 类型的封装类,内部用 Python 字典表示 JSON 文档。
  4. def get_path(self, *keys: str) -> Any:模拟 #>> 路径查询,依次按 key 深入嵌套结构。
  5. doc.set_path(("score",), 96)模拟 jsonb_set 的调用方式,第一个参数是路径元组,第二个是新值。
  6. cur[keys[-1]] = value路径的最后一层直接替换目标键的值,实现“精准替换”。
  7. if __name__ == "__main__":使脚本既能作为模块导入,也能直接运行演示。

常见误区

误区初学者觉得 `json` 和 `jsonb` 都能存 JSON 文本,随便选一个就行,反正应用层拿到手都是字符串。

正解json 只是原样保存文本,不解析、不排序、不支持索引;jsonb 是解析后的二进制格式,键排序、去空白、自动去重,还支持 GIN 索引和丰富操作符。就像普通快递盒和智能收纳箱都能运东西,但后者能自动整理并快速查找。若只做存档、要求保留原始字节,才选 json;需要查询和索引,选 jsonb。

为何常见:两者都能接受同样的 JSON 输入,从应用层看不出差异,只有深入性能和查询能力时才暴露区别。

误区只要建了 GIN 索引,`->>` 取文本、`-` 删除键等所有操作都能自动加速。

正解GIN 只加速特定操作符(@>、?、?|、?&、@@ 等),->> 等取值操作默认不直接走 GIN 索引,通常需要配合表达式索引。就像扫码盘点器只能识别条码标签,不能帮你数箱子里的糖果数量。建索引前先查 EXPLAIN,确认走的是 Bitmap Index Scan 还是 Seq Scan。

为何常见:看到“JSON 索引”这个词就以为万能,忽略了 GIN 是倒排索引、只针对键值包含关系这一本质。

误区灵活 schema 这么好,不如把业务表都改成一张 jsonb 大表,彻底摆脱表结构设计。

正解jsonb 会丧失外键约束、列级类型校验和传统 B-tree 索引优势;高频过滤字段、需要关联查询的字段,仍应保留为普通列。收纳箱虽然万能,但你不会把洗手间水管也装进卧室收纳箱。合理做法是“固定列 + jsonb 动态列”的混合设计,各取所长。

为何常见:被“无 schema”的兴奋冲昏头脑,忽略数据一致性、完整性和性能需求。

误区`@>`、`?` 都返回 true/false,判断“有没有”,可以混用。

正解@> 判断“整个键值对集合是否被包含”(子集语义),? 只判断“某个键或数组元素是否存在”。前者语义更强,后者只关心键。寻找“存在某个标签”时用 ?,验证“包含全部条件”时用 @>。就像“箱子里有没有 VIP 标签”和“箱子里是否同时有 VIP 和 gold 两种标签且内容都匹配”是两个层次的问题。

为何常见:两者的 SQL 返回都是布尔值,从结果看难以区分,容易忽略深层语义差异。

面试题

Q1. jsonb 相比 json 的核心优势是什么?

单选题 · easy

  • A.文本体积必定更小
  • B.支持索引与高效操作← 正确答案
  • C.更宽松的语法容错
  • D.无需解析即可读取

答案:B。jsonb 以二进制格式存储并支持 GIN 索引与 @>、? 等操作符,这是它相比 json 的核心优势。体积不一定更小(可能有少量内部开销),json 也不存在“无需解析”,两者语法容错能力相同。好比智能收纳箱的价值在于“好找”,而不是“一定装得多”。

Q2. 执行 data ->> 'name' 返回什么类型?

单选题 · easy

  • A.jsonb 类型
  • B.text 类型← 正确答案
  • C.json 类型
  • D.布尔类型

答案:B。->> 会把提取出的值转成 SQL 的 text 类型,方便与字符串函数和文本比较搭配;-> 返回的才是 jsonb 类型。就像 -> 是把带包装的盒子整个拿出来,->> 是拆开盒子把里面的纸条直接递给你。

Q3. @> 操作符的作用是?

单选题 · mid

  • A.判断键是否存在
  • B.判断数组长度
  • C.判断键值对被包含← 正确答案
  • D.提取路径上的值

答案:C。@> 用于判断左侧 jsonb 文档是否包含右侧的键值对集合,是“子集”语义。? 才是判断键是否存在;jsonb_array_length 判断数组长度;->> 提取路径值。收纳箱场景中,@> 就是检查“箱子里是否已经装着这一整套指定的物品”。

Q4. GIN 索引主要加速哪种 JSONB 查询?

单选题 · mid

  • A.->> 文本提取
  • B.@> 包含查询← 正确答案
  • C.jsonb_set 更新
  • D.jsonb_each 展开

答案:B。GIN 是倒排索引,专为 @>、? 等“键值存在与包含”类操作符设计;->> 文本提取默认不走 GIN,通常需要表达式索引;jsonb_set 是更新函数,jsonb_each 是展开函数,都不属于索引加速的查询谓词。扫码盘点器只能定位“带某标签的箱子”,不能帮你把箱子内容平铺展开。

Q5. jsonb_set 的作用是?

单选题 · mid

  • A.删除文档中的键值
  • B.按路径替换或新增值← 正确答案
  • C.合并两个 JSON 文档
  • D.把文本转为 JSON 格式

答案:B。jsonb_set(j, path, new_value) 沿路径定位并替换值,路径不存在时按参数决定是否新增;删除键用 - 操作符,合并文档不是 jsonb_set 的职责,文本转 JSON 是 ::jsonb 类型转换。就像给收纳箱换标签:指明哪个箱子哪个夹层,然后放上新的物品。

Q6. 下列哪些操作符通常能用到 GIN 索引?

多选题 · mid

  • A.@> 包含查询← 正确答案
  • B.? 存在判断← 正确答案
  • C.->> 文本提取
  • D.#> 路径提取

答案:AB。GIN 索引默认支持 @>、?、?|、?& 等布尔类操作符;->> 和 #> 是取值操作,不直接使用 GIN,若要加速需建表达式索引。可以把 GIN 理解为“标签目录”,凡是按标签找箱子的查询都能受益,凡是拆箱取内容的查询都得另想办法。

Q7. 关于 jsonb 的存储行为,正确的说法有?

多选题 · mid

  • A.键会排序存储← 正确答案
  • B.重复键自动合并← 正确答案
  • C.空白字符会保留
  • D.重复键保留多个

答案:AB。jsonb 在解析阶段会按键排序、去掉无意义空白、重复键只保留最后一个,因此语义相同的 JSON 在 jsonb 中完全等价。C 和 D 描述的正是 json 文本类型的“原样保存”行为。收纳箱会把同类物品归位并丢掉填充泡沫,而不是原封不动地塞回去。

Q8. EAV 模型相比 jsonb 的主要劣势是?

单选题 · hard

  • A.无法存储数字
  • B.查询需多次连接← 正确答案
  • C.不支持事务操作
  • D.占用空间更小

答案:B。EAV 将一个实体的多个属性拆成多行,查询时要按实体 ID 多次自连接才能拼出完整对象,属性越多连接越慢。它完全可以存数字、也支持事务、占用空间通常更大。就像把椅子拆成零件堆仓库,每次使用都要重新组装,而 jsonb 是整箱搬运。

Q9. 业务需要频繁新增动态属性,且查询以“整行对象”为主,最合适的做法是?

单选题 · hard

  • A.直接修改表结构加列
  • B.在 jsonb 列中追加键值← 正确答案
  • C.另建一张关联表
  • D.改用纯 NoSQL 数据库

答案:B。动态属性变化频繁、查询以整行对象为主,正是 jsonb 的典型场景:无需 ALTER TABLE,@> 可一次过滤多个条件。频繁加列会锁表且列会越来越稀疏;关联表会引入连接开销;纯 NoSQL 则失去关系型数据库的事务与关联能力。收纳哲学讲究“固定结构用隔板,变动内容用标签箱”。

Q10. 关于 jsonb 使用技巧,正确的说法有?

多选题 · extreme

  • A.高频字段可建表达式索引← 正确答案
  • B.GIN 索引加速所有 JSON 操作
  • C.jsonb_set 会原地修改原值
  • D.CHECK 可约束 jsonb 内部结构← 正确答案

答案:AD。对高频访问的 jsonb 键可以建表达式索引,如 CREATE INDEX ON t (attrs ->> 'name');GIN 只加速包含/存在类操作符,并非所有操作;jsonb_set 返回新文档、原值不变;CHECK 约束能校验 jsonb 内部字段(如 jsonb_typeof(attrs->>'age') = 'number')。A 和 D 属于实战中让 jsonb“又好又快”的标准手法。