登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

部分更新 怎样减少 JSON 写放大

来源:17golang原创

时间:2026-10-25 10:58:51 231浏览 收藏

JSON 列里只改一个字段时,不要先在应用层读取整份文档、改完再把完整 JSON 写回去。更合适的做法是让 UPDATE 直接使用 JSON_SET()、JSON_REPLACE() 或 JSON_REMOVE(),并保证输入列和目标列是同一个 JSON 列。这样 MySQL 才有机会对二进制 JSON 做原地部分更新,减少存储层的写放大;如果使用行格式复制,再配合 binlog_row_value_options=PARTIAL_JSON,也可以减少副本日志中的 JSON after-image。

要点速览
  • JSON_SET() 既能替换已有路径,也能添加新路径;只替换已有值时可考虑 JSON_REPLACE()。
  • 部分更新的关键是“同一 JSON 列作为输入和目标”,直接赋整段字符串会退化为完整替换。
  • JSON_STORAGE_FREE() 可辅助判断列内是否出现可复用的空闲空间,但它不是性能基准。

先把整段回写改成路径更新

假设订单表的 attributes 是 JSON 列,现在只想把配送偏好改成 express。下面两种语句结果可能相同,但写入路径不同:

-- 只更新 attributes 中的 delivery.speed,输入列和目标列保持一致
UPDATE orders
SET attributes = JSON_SET(attributes, '$.delivery.speed', 'express')
WHERE id = 1001;

-- 只在路径已经存在时替换;路径不存在时不会新增字段
UPDATE orders
SET attributes = JSON_REPLACE(attributes, '$.delivery.speed', 'express')
WHERE id = 1001;

-- 删除一个路径,避免把剩余键重新拼成完整 JSON
UPDATE orders
SET attributes = JSON_REMOVE(attributes, '$.delivery.legacy_code')
WHERE id = 1001;

JSON_SET() 的路径和值按从左到右处理,因此一次语句里多个路径要注意前一个修改会成为后一个路径的输入。需要“字段不存在就不动”的语义时,用 JSON_REPLACE();需要删除字段时使用 JSON_REMOVE()。这些函数只解决 JSON 文档内部的局部修改,不会替你判断业务字段是否允许变更。

MySQL JSON_SET、JSON_REPLACE 和 JSON_REMOVE 从同一 attributes 列定位不同 JSON 路径的结构说明图
图1:JSON 路径更新结构说明图,展示同一 JSON 列与三个修改函数的静态关系,不是数据库截图或运行证据。

让 MySQL 保留原地部分更新条件

MySQL 官方文档给出的核心条件有三个:目标列确实声明为 JSON;更新表达式使用上述三个修改函数;输入列和目标列必须是同一列。例如 attributes = JSON_SET(attributes, ...) 满足这个方向,而把 profile 的内容计算后写入 attributes,即使结果只改变一个键,也不能按同样方式利用该优化。

因此,下面这种“从应用层带回完整文档再赋值”的写法不要作为局部更新方案:

-- 这是完整替换,不是 JSON 列的路径级部分更新
UPDATE orders
SET attributes = '{"delivery":{"speed":"express"},"coupon":"NEW"}'
WHERE id = 1001;

如果业务一次确实要重建整个文档,完整替换并不错误,只是不要把它误认为减少 JSON 写放大的路径。尤其是复杂数组的大范围重排、跨字段迁移或 schema 重建,局部函数不一定能带来同等收益。

用 JSON_STORAGE_FREE 做低成本检查

更新后可以查看 JSON 二进制表示中当前可复用的空闲空间。这个指标适合检查“是否可能发生了原地更新”,不适合直接当作吞吐量或延迟结论:

-- 查看这一行 JSON 文档的占用和可复用空间
SELECT
  JSON_STORAGE_SIZE(attributes) AS stored_bytes,
  JSON_STORAGE_FREE(attributes) AS free_bytes
FROM orders
WHERE id = 1001;

用 JSON_SET()、JSON_REPLACE() 或 JSON_REMOVE() 缩短某个值后,JSON_STORAGE_FREE() 可能变大;直接给 JSON 列赋新文档,或更新不能利用原地优化时,通常会看到 0。结果还会受到新值大小、列布局和更新表达式的影响,所以要在代表性数据上配合慢日志、I/O 与事务耗时观察。

MySQL JSON_STORAGE_SIZE 与 JSON_STORAGE_FREE 连接 JSON 列更新结果和二进制日志边界的结构说明图
图2:存储空间检查结构说明图,展示 JSON 列、占用空间、空闲空间与日志设置的关系,不是运行结果截图。

复制场景再处理二进制日志写放大

存储层的部分更新和复制日志不是一回事。MySQL 8.4 中,行格式复制默认仍可能把 JSON 完整文档写入 after-image;可以在确认副本版本和运维流程支持后设置:

-- 仅对当前会话启用,先在灰度链路观察副本应用情况
SET SESSION binlog_row_value_options = 'PARTIAL_JSON';

-- 生产环境通常由运维配置持久化,而不是把全局变更写进业务事务
-- SET GLOBAL binlog_row_value_options = 'PARTIAL_JSON';

官方说明中,PARTIAL_JSON 只影响行格式日志中 JSON 修改部分的记录方式;如果服务端无法生成部分更新,仍会使用完整文档。语句格式复制对 JSON 部分更新有自己的记录规则,因此不要只看一个会话变量就推断所有链路都变小。

常见问题

JSON_SET 和 JSON_REPLACE 怎么选?

希望路径不存在时自动新增,用 JSON_SET();只允许替换已有路径,用 JSON_REPLACE(),这样能避免拼写错误悄悄创建新键。

JSON_STORAGE_FREE 返回 0 就代表没有更新成功吗?

不代表。它只表示当前没有可报告的空闲空间;更新可能成功,但新值更大、表达式不满足原地条件,或旧空间被重新利用后没有剩余空洞。

把整个 JSON 文档放进变量再 JSON_SET 还算部分更新吗?

对用户变量而言,值会被完整替换;要让列级部分更新生效,应直接在 UPDATE 中以目标 JSON 列作为 JSON_SET() 的输入。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>