UPDATE返回0行说明未修改数据,原因有二:一是WHERE条件未匹配任何行(Rows matched: 0),二是条件匹配成功但新旧值相同导致InnoDB跳过写入(Rows matched: N, Changed: 0)。

UPDATE 返回 0 行不是出错,而是数据库明确告诉你:没改任何数据——但原因可能截然不同,得拆开看。
WHERE 条件没匹配到任何行
这是最常见也最容易被忽略的情况:语句语法完全正确,执行不报错,ROW_COUNT() 返回 0,但实际连比对都没发生。
- 立刻用
SELECT *复现你的WHERE条件,比如SELECT id, status FROM orders WHERE order_id = 12345,如果结果为空,那 UPDATE 必然匹配零行 - 字符串字段可能带不可见空格或全角字符,试试
SELECT HEX(status), LENGTH(status) FROM ... - 大小写敏感排序规则(如
utf8mb4_0900_as_cs)下,WHERE status = 'shipped'不会匹配'Shipped' - 数字字段传了字符串,比如
WHERE user_id = '100abc'会被转成 100,而WHERE user_id = 'abc100'会变成 0 —— 若表中无user_id = 0,就彻底失联
条件匹配成功但新旧值完全相同
MySQL InnoDB 在检测到 SET 的值和当前行值一字不差时,会跳过物理写入,ROW_COUNT() 仍返回 0,但 Rows matched: 1、Changed: 0 —— 这类“静默跳过”常出现在状态机更新、时间戳赋值等场景。
-
NULL比较必须用IS NULL,= NULL永远为FALSE,容易写出永远不生效的条件 - 时间戳字段若只设到秒级(如
'2026-09-17 16:00:00'),而数据库存的是微秒级,肉眼一样,实际值不同 - 显式排除不变场景更稳妥:
UPDATE t SET status = 'done' WHERE id = 123 AND (status != 'done' OR status IS NULL) - 触发器(尤其是
BEFORE UPDATE)可能把NEW.status又设回OLD.status,最终白更新
Go / Java 等语言中参数顺序错位
预处理语句里 ? 是从左到右依次绑定的,参数顺序错一位,WHERE 就失效,SET 就错位,结果就是匹配零行。
- SQL:
UPDATE users SET name = ? WHERE id = ?→ 第一个?必须是name值,第二个才是id - Go 中错误写法:
stmt.Exec(123, "alice")实际执行的是SET name = 123 WHERE id = 'alice' - Java JDBC 同理,
ps.setString(1, "alice"); ps.setInt(2, 123);才对;顺序反了就查不到目标行 - 别依赖日志打印的 SQL 字符串,要确认
Exec()或executeUpdate()传参顺序
MyBatis 默认返回“匹配行数”而非“影响行数”
MyBatis 的 update() 方法默认调用的是 JDBC 的“匹配行数”,不是 MySQL 底层的“实际变更行数”。也就是说,哪怕你 SET 的值和原值一模一样,只要 WHERE 匹配上了,它就返回 1。
- MySQL 驱动连接串必须加
useAffectedRows=true,例如:jdbc:mysql://localhost:3306/db?useAffectedRows=true - 否则,
UPDATE t SET x = x WHERE id = 1也会返回 1,掩盖了“根本没变”的事实 - 检查方式:在 MySQL 客户端执行同条语句,看输出是
Query OK, 0 rows affected还是Query OK, 1 row affected - 框架封装越深,越要穿透到底层确认真实
rowcount,不能只信 ORM 返回值
真正麻烦的从来不是语法报错,而是那些不报错、ROW_COUNT() 还显示“成功”、但数据纹丝不动的静默路径——尤其是触发器改值、类型隐式转换、空格干扰、以及参数顺序颠倒这四类,最容易在线上憋大招。

















