作者:胖头鱼的鱼缸(尹海文) Oracle ACE Pro: Database PostgreSQL ACE 10年+数据库行业经验 拥有OCM 11g/12c/19c、MySQL 8.0 OCP、Exadata、CDP等认证 墨天轮MVP,ITPUB认证专家 圈内拥有“总监”称号,非著 名社恐(社交恐怖分子) 全网同名:胖头鱼的鱼缸 ITPUB:yhw1809 除授权转载并标明出处外,均为“非法”抄袭

话说,最近这段时间,遇到好几个性能相关的问题,类型还挺集中: JSON 字段高频更新导致数据库 CPU、IO、内存一起告警。
有做电商的,商品 SKU 详情动辄几 KB,每次改个库存、改个价格,库就要重写整个 JSON;还有些做游戏背包、订单详情、物流轨迹的,场景不一,但症状高度相似:开发的抱怨基本都是"我明明只改了一个字段",数据库这边却在偷偷干着重写整篇文档的活。
这事儿不怪写代码的——JSON 的写入语义本来就是"替换整个值", 问题出在底层到底有没有给你"只动改了的部分"的本事。
本期把这事儿掰开聊,顺便把 MongoDB、PostgreSQL、Oracle 三家在该场景下的表现摆出来做个横评,给出一些"我的选择",以及几个"退而求其次"的方案。
老规矩,先打个预防针:这是一篇纯粹的从写放大和查询路径两个维度做技术对比的文章,不讨论技术路线的优劣,也限定为高频更新 + 查询的应用场景。

在开始对比之前,先把一个底层的事说清楚—— “改一个字段,库为什么动了整篇”。
这事其实跟 MongoDB、PostgreSQL、Oracle 都是 MVCC 数据库有关。MVCC(多版本并发控制)的好处是读不阻塞写、写不阻塞读,代价是 每一次写都要生成新版本。具体到 JSON 文档,就是:
所以不管用哪家, 单从"JSON 字段更新"的角度看,都会触发整篇文档的写 IO——这是 MVCC 的天然代价,问题只是谁家额外给了你一层优化。
引用 Franck Pachot 在 DEV.to 上公开的实测数据:PostgreSQL 更新 10000 个 JSONB 文档生成 641088 个 WAL 记录(约 64/文档),写 57413 个 block(约 46KB/文档)——远大于实际逻辑变更。
换句话说,业务逻辑改了 1KB 的字段,PostgreSQL 实际写了 46 倍的数据。中间被 WAL 放大、TOAST 压缩放大、各种 buffer / page 操作放大——这些都是账。
下面这张表,先把三家的默认存储摆一下:
| 数据库 | 默认存储引擎 | JSON 类型 | 物理存储格式 |
|---|---|---|---|
| MongoDB | WiredTiger | BSON | BSON 二进制,写入 B-Tree 新位置 |
| PostgreSQL | heap + MVCC | JSONB | 二进制 + TOAST 压缩 |
| Oracle | heap + undo | OSON | 二进制 + LOB 存储 |
光看类型不够,下面把"更新时各家到底干了什么"逐个拆开。
下面要对比的不是三个库,而是 四种应对方式:MongoDB 原生、PostgreSQL JSONB 原生、Oracle 原生 OSON,以及 Oracle 26ai 新引入的 JSON Relational Duality View。第三种和第四种都源自于 Oracle,但因为对于 JSON 的使用与实现机制完全不同,所以单独拆开讲。

原理:MongoDB 文档写入走 WiredTiger 的 B-Tree,每次 update 都把整个 BSON 文档写到新位置(旧位置打 stale),后台 checkpoint / compaction 异步清理。
优点:
缺点:
compact,高峰期会影响业务适用场景:文档天生独立、不需要跨文档事务、单文档写入吞吐优先于部分更新性能的场景。
原理:JSONB 是二进制格式 + TOAST 压缩存储,
UPDATE 整条 tuple 写到 heap 新位置,旧 tuple 标记 dead tuple,autovacuum 异步清理。
优点:
jsonb_path_ops)pgvector、Apache AGE 等扩展可在同库共存缺点:
适用场景:JSONB 文档中等大小(KB 级),半静态属性 + 少量高频字段,且接受 autovacuum 调优的场景。
原理:Oracle 12c 起引入 OSON(二进制 JSON 格式),存储为普通 LOB 列。更新走 undo + block 覆盖,部分场景可走 OSON Partial Update 优化——只重写 JSON 内部变更的字节段。
优点:
json_value、
json_exists、
json_transform 等缺点:
适用场景:JSON 列内容半静态(配置、扩展属性),少数字段偶尔修改 + 整体读多写少的场景。
原理:把"JSON 文档"从物理存储降级为访问层—— 物理存储仍然是规范化的关系表,JSON 只是 duality view 这层抽象。一份数据、两套访问接口(JSON 文档视图 / 标准 SQL 关系表)。
优点:
json_mergepatch 更新 duality view 时,Oracle 把 JSON Patch (RFC 6902) 翻译成底层关系表的 DML,改哪个字段就 UPDATE 哪个列;嵌套数组 diff 后只动子表的对应行ORA_ROWSCN 的乐观锁,并发更新无需应用层手动控制缺点:
适用场景:JSON 文档背后有跨表关联 + 单字段高频更新 + 大量查询需要命中索引 + 不能容忍数据冗余的场景。

上面聊完了每个方案的原理和优劣,下面用几张表把关键维度拉通对比。
| 维度 | MongoDB 原生 | PostgreSQL JSONB | Oracle 原生 OSON | Oracle 26ai Duality View |
|---|---|---|---|---|
| 单字段更新 | 整篇重写 | 整篇重写(PG17 前) | 视条件 Partial Update | 只 UPDATE 对应列 |
| 数组元素变更 | 整篇重写 | 整篇重写 | 整篇重写 | diff 后只动子表对应行 |
| 查询路径 | 文档直接读取 | JSONB 索引查找 | JSON 搜索索引 | 自动改写为 SQL,命中底层 B-Tree |
| 跨文档一致性 | 弱(事务或多文档事务) | 强(事务),但写放大 | 强(事务),但写放大 | 强(事务),零数据冗余 |
| 写放大(参考) | 中等 | 严重(实测 ~64 WAL/文档) | 中等 | 接近普通关系表 UPDATE |
| 查询路径上的 JSON 构造 | 无 | 无 | 有(OSON 反序列化) | 仅在最终返回时构造 |
| 并发控制 | 文档级 MVCC + WiredTiger 快照 | tuple 级 MVCC | block 级 + undo | ETag 乐观锁(ORA_ROWSCN) |
| 部分更新优化成熟度 | 无 | 无 | 窄条件适用 | 内核级原生支持 |
| 学习曲线 | 低 | 低 | 中 | 中~高 |
| 维度 | MongoDB 原生 | PostgreSQL JSONB | Oracle 原生 OSON | Oracle 26ai Duality View |
|---|---|---|---|---|
| 部署门槛 | 低(单机起步) | 低 | 中(实例 + 许可) | 中(实例 + 许可 + 26ai 版本) |
| 运维复杂度 | 中(replica set / sharding) | 低~中 | 中~高(Oracle DBA 技能) | 中~高 |
| 部分更新场景的运维代价 | 监控磁盘膨胀 + 手动 compact | 监控表膨胀 + autovacuum 调优 | 监控 LOB + JSON 索引 | 与普通关系表基本一致 |
| 生态工具 | 丰富(Change Stream、Driver 多) | 丰富(pgvector、AGE、psql 等) | 丰富(SQL Developer、ORDS) | 丰富 + Oracle MongoDB API 兼容 |
| 你的情况 | 推荐方案 |
|---|---|
| 文档独立、无跨文档关联、写吞吐优先 | 方案一:MongoDB |
| 已有 PostgreSQL 栈,JSONB 文档中等,半静态属性为主 | 方案二:PostgreSQL JSONB |
| Oracle 现有用户,JSON 列做配置/扩展属性 | 方案三:Oracle OSON |
| JSON 背后有跨表关联 + 高频单字段更新 + 不能容忍冗余 | 方案四:Oracle 26ai Duality View ⭐ |
| 团队无 Oracle 经验,又想要 Duality View 类似的"规范化+JSON 视图"效果 | "退而求其次"方案(见下文) |
上面聊完了四家的优劣,这里直接说我关于高频更新的 JSON 业务场景选型的立场。
核心结论:单字段高频更新的场景,Duality View 是当前唯一在"写放大"这个维度上真正消灭了 JSON 整篇重写的方案——因为它根本不让 JSON 成为物理存储单元。其他三家不管怎么调优,本质上都是在和 MVCC 的"整篇重写"做对抗,而 Duality View 是把这层代价从物理存储里彻底拿掉了。
具体到选型,我的判断是这样的:
理由有三:
SIGMOD 2025 论文公开的数据:Duality View vs Hibernate ORM,吞吐量 2-3 倍;vs 某主流文档数据库,TPC-C 变体下 2 倍以上。这不是营销话,是数据库顶会论文中的实测结果。
现实是残酷的——不是谁都有 Oracle 26ai 的环境(特别是数据库国产化的大环境下),也不是谁都能说动老板换库。这里说的"没有 Oracle 26ai",准确讲是**没有 JSON 关系二元性视图(JSON Relational Duality View)**这个能力(不排除以后有其他数据库会跟进这一功能),而是那套"JSON 文档 ↔ 规范化关系表"映射你没法用。如果你卡在 PostgreSQL 或 MongoDB 上,又遇到高频 JSON 更新场景,下面这几个方案可以参考。
做法:把高频更新的字段 拆出来做 top-level column,JSONB 只存半静态属性。
-- 反例:所有字段都塞 JSONBCREATE TABLE products ( id BIGINT PRIMARY KEY, metadata JSONB -- 库存、价格、状态全在这里);-- 正例:高频字段拆出来CREATE TABLE products ( id BIGINT PRIMARY KEY, stock INT, -- 高频改 price NUMERIC(10,2), -- 高频改 status SMALLINT, -- 高频改 metadata JSONB -- 半静态属性、规格参数、扩展字段);
代价:放弃了"JSON 一把梭"的便利,换来的是 autovacuum 不再是瓶颈。
额外建议:
pg_stat_user_tables.n_dead_tup,autovacuum 调优(
autovacuum_vacuum_scale_factor 调到 0.05 以下)。做法:同样思路—— 高频字段拆出来做 top-level field,嵌套结构只放半静态数据。
// 反例db.products.insertOne({ _id: 1001, metadata: { stock: 100, price: 99.9, status: "on_sale", specs: { /* 一堆规格 */ }
}
});// 正例db.products.insertOne({ _id: 1001, stock: 100, // 顶层字段
price: 99.9, // 顶层字段
status: "on_sale", // 顶层字段
specs: { /* 半静态规格 */ }
});
代价:牺牲了文档"内聚性",换来的是每次只更新对应的 top-level 字段,写入数据量就是字段本身大小。
额外建议:
db.stats().storageSize 与
indexSize 的比值,磁盘膨胀超 30% 跑一次
compact;$set 而非整个文档
replaceOne——虽然底层效果一样,但应用层语义更清晰;PostgreSQL 18 在 JSONB 部分更新上做了一些优化(
MERGE 语句、
jsonb_set 在某些路径上的增量改写),加上
pgvector、Apache AGE 等扩展,
某种程度上能在 PostgreSQL 上模拟 Duality View 的部分能力——但需要明确知道它不是同一类解决方案:
也就是说, PG18 能让你更舒服地用 JSONB,但不能从根上解决写放大问题。如果你真的被这个问题困扰,DBMS 升级之外还是得考虑结构调整。
先说清楚:场景 D 其实又回到了 Oracle 26ai——它严格来说不算"退而求其次",而更像一个"绕回正主"的选项。因为在 Oracle 26ai 中,除了上面方案四那种 SQL 层的 Duality View,还有一个 Duality View 的增强用法:MongoDB 协议兼容 API。
Oracle 26ai 提供了这个 MongoDB 协议兼容 API——老的 MongoDB 应用可以直接对接 Oracle 26ai,MongoDB 那边发的 JSON 写入会被 Oracle 自动落库为规范化的关系表,并通过 Duality View 暴露给老应用。换句话说, 你不用重写应用,就能吃到 Duality View 单列更新的红利——这是 Duality View 在"访问层"上的延伸,本质还是把 JSON 当视图、关系表当物理存储。
适用场景:
JSON-to-Duality Migrator。代价:既然绕回 Oracle 26ai,就依赖它的部署、许可;新团队需要补 Oracle DBA 能力。它唯一省下的是"改应用"那一刀——但库仍然得是 Oracle 26ai。
回到开头那个问题—— JSON 改一个字段,凭什么要重写整篇?
答案分两层:
所以如果你问我推荐哪个方案,我的回答很直白:
新建系统、JSON 背后有跨表关联、单字段高频更新是核心痛点——直接上 Oracle 26ai Duality View(或使用有类似能力的数据库)。不是(或不能) Oracle 26ai / 没有其他数据库有这一能力的场景,按前面说到的的"退而求其次"方案做结构调整,把高频字段拆出 JSON。
Duality View 不是万 能的——它解决的是"JSON 文档 + 高频单字段更新 + 不能容忍冗余"这个特定组合。 文档独立、无跨表关联的场景,MongoDB 仍然更省事;JSONB 文档小、半静态的场景,PostgreSQL 性价比最高。选型永远没有银弹,关键是先搞清楚自己的核心诉求是什么,然后对照上面的矩阵选那个短板最少、跟你团队技能树最匹配的方案。
类似的话题我之前在数据库系列里聊过,本期算是在 JSON 高频更新这个具体场景里再展开一次——本质上还是那句老话: 让数据库把该扛的扛住,别把成本转嫁到应用层。
老规矩,知道写了些啥。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31466763/viewspace-3182008/ ,如需转载,请注明出处,否则将追究法律责任。