必须用 json_merge_patch() 而非 json_merge_preserve():后者遇重复键会聚合成数组(如 "retry_count": [3,5,7]),违背 rest api patch 语义;前者同名键直接覆盖,保留未提及字段,符合局部更新预期。

直接说结论:用 JSON_MERGE_PATCH() 做覆盖式合并,用 JSON_SET() 或 JSON_REPLACE() 做局部更新——但后者必须满足硬性条件,否则 MySQL 会悄悄退化成整列重写。
什么时候必须用 JSON_MERGE_PATCH() 而不是 JSON_MERGE_PRESERVE()
你遇到的“配置被改成了数组”“"retry_count": [3, 5, 7]”这类问题,基本就是误用了 JSON_MERGE_PRESERVE()。它对重复键的处理是收集所有值进数组,而真实业务中几乎没人想要这个行为。
- REST API 的 PATCH 请求只传了部分字段,你要用它覆盖已有 JSON 字段的对应键,而不是拼出一个数组
- 用户偏好更新只改
"theme"和"lang",其他字段必须原样保留,且不能变成"timeout": [30, 60] -
JSON_MERGE_PATCH()遇到同名键直接替换整个值(浅层),不递归合并嵌套对象;JSON_MERGE_PRESERVE()则会把所有同名键的值都塞进数组里 - 注意:
JSON_MERGE()在 8.0.3+ 已弃用,别再用
JSON_SET() 和 JSON_REPLACE() 的关键区别在哪
这两个函数名字太像,但行为完全不同,用错就等于没更新成功,还查不出错。
-
JSON_SET(json_col, '$.status', 'done'):不管status存不存在,都会插入或覆盖——适合“确保字段存在” -
JSON_REPLACE(json_col, '$.status', 'done'):只替换已存在的路径,如果status键根本不存在,语句执行成功但ROW_COUNT()返回 0,值没变 - 想安全地“只更新已有字段”,选
JSON_REPLACE();想“强制补上缺失字段”,必须用JSON_SET() - 批量更新多个键时,优先写成
JSON_SET(json_col, '$.a', 1, '$.b', 2),不要拆成多条语句——单次调用更易触发局部更新
局部更新真正生效的四个硬门槛
写了 JSON_SET() 不代表 MySQL 就会局部更新。它只在严格满足以下条件时,才跳过整列重写,只改几个字节:
- 目标列类型必须是
JSON(不是VARCHAR或TEXT)——用SHOW COLUMNS FROM table_name确认 - UPDATE 语句中只能出现
JSON_SET()、JSON_REPLACE()或JSON_REMOVE()之一,且不能嵌套其他表达式(比如不能JSON_SET(JSON_EXTRACT(...), ...)) - 所有路径都必须指向**已有位置**:不能新增顶层 key(如
$.new_field),也不能往数组末尾追加(如$[99]超出当前长度) - 新值的 UTF-8 字节数 ≤ 原值——超了就会整列重写;可用
JSON_STORAGE_SIZE()对比更新前后大小验证是否真走局部路径
容易被忽略的静默失败点
最麻烦的不是报错,而是“看起来成功了,其实什么都没变”。
-
JSON_MERGE_PATCH()任一参数为NULL,整个函数返回NULL,不会报错——务必检查源字段是否为空或类型不对 - 从表里取 JSON 列时,如果该列实际存的是字符串(比如早期用
VARCHAR存的 fake JSON),JSON_MERGE_PATCH(config, '{"x":1}')会静默返回NULL,而不是报Invalid JSON text - ORM 如 MyBatis 默认把 JSON 当字符串处理,预编译参数可能被转义,导致函数接收的是带引号的字符串而非 JSON 值——必须显式配置
json类型映射 -
JSON_MERGE_PATCH()不支持局部更新逻辑,它内部会重建整个文档,所以别指望它能省 IO











