DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

MySQL UPDATE 返回 0 rows affected,但目标记录确实存在

MySQL UPDATE 默认按实际变化计数,值相同和条件未匹配都可能返回 0。区分 Rows matched 与 Changed,核对 CLIENT_FOUND_ROWS 连接语义,并修正接口的不存在判断。

更新接口成功执行了 UPDATE,受影响行数却是 0,程序随后返回“记录不存在”。查询同一个主键,行仍然在那里,字段值也正是本次提交的内容。问题可能出在把“实际改变了多少行”当成了“匹配到多少行”。

下面针对 MySQL 8.4 的普通 UPDATE。默认的受影响行数按实际变化计数;连接若启用了 CLIENT_FOUND_ROWS,相应计数会按 WHERE 匹配行数返回。驱动或框架可能采用不同连接选项,所以不能把某个客户端观察到的数字当成所有客户端的固定规则。

零变化和零匹配可以返回相同数字

假设一行的 id 为 1、label 已经是 ready,再次把它设成 ready,MySQL 不需要改变这个值。更新不存在的 id=3 也没有改变任何行。默认受影响行数都可能是 0,但前者匹配到了行,后者没有。

可以在本机测试数据库的新会话中使用独立临时表观察。此会话不要复用业务连接,确认有创建临时表权限,并逐条执行;创建失败时停止,不要继续后面的更新:

CREATE TEMPORARY TABLE update_count_probe (
    id INT PRIMARY KEY,
    label VARCHAR(20) NOT NULL
);
INSERT INTO update_count_probe (id, label) VALUES (1, 'ready');

UPDATE update_count_probe SET label = 'ready' WHERE id = 1;
UPDATE update_count_probe SET label = 'ready' WHERE id = 3;
UPDATE update_count_probe SET label = 'done' WHERE id = 1;

SELECT id, label FROM update_count_probe;
DROP TEMPORARY TABLE update_count_probe;

分别保存三次更新各自的结果信息。预期匹配与变化数量是:

更新 匹配行数 实际变化行数
已有行再次设置为 ready 1 0
设置不存在的 id=3 0 0
已有行从 ready 改为 done 1 1

这里临时表仅对当前会话可见,关闭会话会自动移除。不要把示例表名替换成生产业务表,也不要为了制造非零返回而给真实记录随意改一个值。

读取同一次 UPDATE 的结果信息

客户端如果展示 Rows matched、Changed 和 Warnings,就分别看这三项。Rows matched: 1 Changed: 0 已经说明条件匹配到了行,而字段没有实际变化;警告数量非零时,还应保留警告内容,不能把所有零变化都解释成“输入本来就一样”。

使用 MySQL C API 时,mysql_info() 能提供最近一次 UPDATE 的匹配、变化和警告信息;mysql_affected_rows() 则按该连接的计数语义返回一个数字。应在更新成功后、下一条语句执行前读取结果。先确认 SQL 执行是否成功,再解释行数,不要把错误码与受影响行数混为一谈。

其他驱动应使用自身暴露的更新结果与警告接口。若只有一个计数字段,先核对驱动版本和建连选项,确认它究竟表示匹配还是变化。字段名叫 rowcount 或 affectedRows,不足以单独证明语义。

按业务需要修正“记录不存在”的判断

如果接口允许重复提交相同状态,那么“更新成功、匹配到行、值已经相同”通常应走幂等成功路径。不要仅凭默认变化数为 0 返回不存在,也不要因此立即执行补插入;否则可能制造重复写入或主键冲突。

确实需要通过更新结果判断是否匹配时,可以评估连接层的 CLIENT_FOUND_ROWS。在 MySQL C API 中,它是传给 mysql_real_connect() 的连接标志,不是每次更新前执行的 SQL 开关。启用后,示例第一条更新的计数按匹配返回 1,第二条仍为 0。

这个变更会影响使用该连接的调用者。接入连接池前,检查所有依赖行数的分支,尤其是原本要统计实际变化量的逻辑;不要为了修复一个接口,悄悄改变整个应用的计数约定。不同驱动的选项名称、默认值和布尔含义不一定与 C API 常量一致,不能照搬名称猜配置。

即便采用匹配计数,0 也只是没有满足完整 WHERE 条件的行。如果条件还包含租户、状态或乐观锁版本,不能直接宣称主键不存在。应保留这些条件的业务含义,不要为得到非零数字删掉权限或版本检查。

补一条普通 SELECT 可以帮助排查当前记录,但它与先前的 UPDATE 不是同一个观察时点。存在并发写入时,不能把后来查到的内容当作那次更新瞬间的证明;需要严格区分状态的接口,应另行设计事务与并发控制,而非靠两条无约束查询拼出结论。

回归检查包括重复请求

在测试数据上验证记录不存在、值相同、值不同三种情况,再验证带状态或版本条件的失败分支。确认接口返回与业务约定一致,警告和 SQL 错误仍单独处理,原来统计实际变化数的功能也没有误读匹配数。

保留原连接配置与行数判断代码。若采用连接标志方案,回退时应同时恢复配置和相应业务判断,并按连接池的正常流程建立使用旧配置的新连接;单改配置文件不会保证存量连接立即改变。这里修正的是返回值解释,不需要删除或重建业务数据。