触发器中JSON_SET/JSON_REPLACE无法实现局部更新,强制走全量读取-解析-序列化-写入流程,导致I/O激增和锁等待加剧;应改用NEW.col赋值或业务层直接更新。

触发器里不能用 JSON_SET / JSON_REPLACE 做局部更新
MySQL 触发器中对 JSON 字段调用 JSON_SET() 或 JSON_REPLACE(),**不会触发局部更新(in-place update)**,哪怕语句本身合法、执行无报错。这是因为触发器上下文强制走“复制-修改-重写”路径:MySQL 会把整条 JSON 值读出、解析为内部结构、应用函数、再序列化回二进制写入——完全绕过局部 patch 机制。
常见错误现象是:触发器执行后 ROW_COUNT() 返回 1,SELECT 看字段值也变了,但 Innodb_buffer_pool_reads 明显上涨,监控显示磁盘 I/O 激增;或者在高并发下出现锁等待加剧。
- 触发器中所有 JSON 函数调用(包括
JSON_REMOVE())都默认退化为全量更新 - 即使字段类型是
JSON、路径不越界、新值字节 ≤ 原值,也无效 - 想靠触发器“悄悄补字段”(比如自动加
$.updated_at),用JSON_SET()反而比直接赋值字符串更慢
替代方案:用 NEW.col = JSON_SET(NEW.col, ...) + 显式赋值
触发器里真正能控制行为的,只有对 NEW 行的赋值操作。局部更新虽不可达,但你可以避免额外解析开销:
- 只在
BEFORE UPDATE或BEFORE INSERT中操作NEW.col,不要在AFTER阶段再UPDATE同表 - 写法必须是
SET NEW.info = JSON_SET(NEW.info, '$.status', 'pending');,不是UPDATE t SET info = ... - 如果原值可能为
NULL,先判断:IF NEW.info IS NULL THEN SET NEW.info = JSON_OBJECT('status', 'pending'); ELSE ... END IF; - 多路径更新时,参数顺序仍关键:
JSON_SET(NEW.info, '$.a', 1, '$.b', 2)合法;JSON_SET(NEW.info, '$.a', 1, 2)直接报语法错
数组元素更新要特别注意路径写法
在触发器里更新 JSON 数组某一项(比如日志列表第 0 条的状态),路径写成 '$[0].state' 才有效;写成 '$.0.state' 会被当作文本 key 处理,结果静默失败——字段不变,也不报错。
更危险的是追加数组项:JSON_SET(NEW.events, '$[99]', new_event)。若当前数组长度不足 100,MySQL 不会自动扩容,而是忽略该操作(JSON_SET() 不支持越界追加);正确做法是用 JSON_ARRAY_APPEND():
SET NEW.events = JSON_ARRAY_APPEND(NEW.events, '$', new_event);
但注意:JSON_ARRAY_APPEND() 也**不走局部更新**,且它总是追加到末尾,无法指定索引插入。
真正要局部更新,得绕开触发器
如果你的核心诉求是“每次更新主表时,自动、高效地同步改 JSON 字段某个键”,触发器不是解法。实际可选路径只有两个:
- 业务层统一用
UPDATE ... SET json_col = JSON_SET(json_col, ...)替代触发器,由应用控制时机和参数 - 用生成列(generated column)+ 索引固化常用路径,把 JSON 内部字段“投影”出来,再通过普通列更新间接驱动(例如建
status VARCHAR(20) AS (json_unquote(json_extract(info, '$.status'))) STORED)
最后提醒一句:触发器里任何 JSON 函数调用,都别指望它省 IO —— 它的执行代价和 CONCAT('', info, '') 差不多,只是语义更清晰而已。











