如果你写了 UPDATE profile SET doc = JSON_SET(doc, '$.name', 'Li'),却发现 JSON 文档仍像被整体重写,先不要把原因归结为 JSON_SET 失效。MySQL 的 JSON partial update 是优化器在满足一组条件时才会选择的路径,函数名本身只是候选入口,并不是原地更新的保证。
官方地址:https://dev.mysql.com/doc/refman/8.4/en/
最常见的判断顺序是:列必须是 JSON 类型;输入列和被更新的目标列必须是同一列;更新要替换已有对象或数组值,不能给父对象或数组添加新元素;新值通常不能比原值占用更多空间。任一条件不满足,都可能退回完整 JSON 更新。
JSON_SET 只是进入候选路径的入口
MySQL 8.4 文档列出的部分更新函数包括 JSON_SET()、JSON_REPLACE() 和 JSON_REMOVE()。优化器会分析 UPDATE 的表达式,只有能够把变更表示成 JSON 二进制差分,并交给支持该能力的存储引擎时,才可能减少完整文档的重写。
-- 只有 doc 列本身是 JSON,且表达式读写同一列,才具备候选形态。
UPDATE profile
SET doc = JSON_SET(doc, '$.name', 'Li')
WHERE id = 7;
-- 直接赋值会生成新的 JSON 值,不属于 JSON_SET 的部分更新形态。
UPDATE profile
SET doc = '{"name":"Li","level":3}'
WHERE id = 7;
因此,“我已经用了 JSON_SET”只能证明语句走到了候选入口,不能证明某一行一定发生了原地写入。尤其是多行更新时,每一行的 JSON 路径是否存在、旧值空间是否够用,都可能不同。
四个条件逐项排查为什么没有原地更新
第一,目标字段要在表结构中声明为 JSON,而不是把 JSON 文本存进 TEXT。第二,函数的输入列和赋值目标必须相同;SET doc_a = JSON_SET(doc_b, ...) 不符合部分更新的同列条件。第三,路径需要指向已有值,替换父对象或数组中的既有值可以被考虑,但给父对象新增成员或把数组扩展到末尾会改变结构。
第四,新值要能放回原值所在的空间。把较长字符串写入较短字符串时,通常没有足够的空洞;如果之前的部分更新留下了可复用空间,较大的值也可能被容纳,所以不能只凭字符串长度做绝对判断。
-- 这是“替换已有路径”的候选写法;实际是否原地仍取决于空间。 UPDATE profile SET doc = JSON_SET(doc, '$.name', 'Li') WHERE id = 7; -- 这是新增路径,改变了父对象结构,不能按同一条件理解为原地替换。 UPDATE profile SET doc = JSON_SET(doc, '$.nickname', '小李') WHERE id = 7; -- 输入列与目标列不同,优化器不能把它当成同列部分更新。 UPDATE profile SET doc_a = JSON_SET(doc_b, '$.name', 'Li') WHERE id = 7;
排查时要把“路径不存在”“新值变长”“表达式读写不同列”和“字段实际不是 JSON”分开记录。它们表面上都表现为 JSON_SET 执行成功,但对存储层的更新机会完全不同。
用 JSON_STORAGE_FREE 观察可复用空间
JSON_STORAGE_FREE() 反映 JSON 列二进制表示中因部分更新释放、可继续利用的空间;JSON_STORAGE_SIZE() 则反映当前文档占用的存储大小。一个较短的替换值成功留下可用空间时,前者可能增加;直接给 JSON 列赋完整新文档时,前者会回到零。
-- 先查看当前文档大小与可复用空间。
SELECT JSON_STORAGE_SIZE(doc) AS used_bytes,
JSON_STORAGE_FREE(doc) AS free_bytes
FROM profile
WHERE id = 7;
-- 用更短的值替换已有值,可能留下可复用空间。
UPDATE profile
SET doc = JSON_SET(doc, '$.description', '短说明')
WHERE id = 7;
-- 这里的两个函数用于观察存储状态,不是强制打开部分更新的开关。
SELECT JSON_STORAGE_SIZE(doc), JSON_STORAGE_FREE(doc)
FROM profile
WHERE id = 7;
这个观察值适合帮助定位“为什么这一行没有复用原空间”,但不能把它当成通用性能证明。实际收益还取决于 JSON 文档大小、变更比例、存储引擎和整体写入负载。
别把存储层更新和 binlog 部分记录混为一谈
MySQL 8.4 还支持在 row-based replication 下用 binlog_row_value_options=PARTIAL_JSON 记录 JSON 变更的部分内容。这是二进制日志的表示方式,和表中 JSON 值是否在存储层原地修改不是同一个开关。即使日志可以采用部分格式,也不能据此反推每一行都完成了存储层原地更新。
-- 只在确认需要压缩 row-based JSON 变更日志时设置。 SET SESSION binlog_row_value_options = 'PARTIAL_JSON'; -- 如果服务器无法生成 JSON 部分差分,日志仍可能记录完整文档。 UPDATE profile SET doc = JSON_SET(doc, '$.name', 'Li') WHERE id = 7;
可以把最终排查清单压缩为六个问题:字段是不是 JSON?表达式是不是同列读写?路径原来是否存在?是否新增了父对象或数组元素?新值是否有机会放进原空间?你观察的到底是列存储、InnoDB 写入,还是 binlog 体积?逐项回答后,通常就能解释为什么 JSON_SET 执行成功,却没有出现预期的“原地更新”。
相关问题
JSON_SET 执行成功就代表发生了部分更新吗?
不代表。它只说明 JSON 表达式得到结果;是否采用部分更新还要看列类型、同列条件、路径是否为已有值以及替换空间等条件。
JSON_STORAGE_FREE 大于零是不是性能变快的证明?
不是。它说明二进制 JSON 表示中有可复用空间,不能单独代表磁盘 I/O、锁等待或复制延迟已经改善。
参考资料:https://dev.mysql.com/doc/refman/8.4/en/json.html、https://dev.mysql.com/doc/refman/8.4/en/json-utility-functions.html、https://dev.mysql.com/doc/refman/8.4/en/replication-features-json.html

MySQL named window 怎样复用多个查询的窗口定义
