刚学MySQL的时候,我干过一件现在想起来还后背发凉的蠢事——要改某个学生的成绩,手一抖写出来的UPDATE语句忘了带WHERE条件,回车一敲,整个表的成绩全变成了同一个分数。当时用的是线上测试库,数据虽然能恢复,但那几分钟的手忙脚乱和“差点出大事”的后怕,比任何教程都让我印象深刻。所以当我决定写这个零基础系列的“修改数据”篇时,第一个念头就是:这篇文章绝不能只讲UPDATE语法怎么写,更得把“怎么安全地改数据”这件事掰开揉碎讲清楚。
这一篇是《MySQL数据库操作指南》零基础篇的第十四篇,目标很单纯:让你彻底搞懂MySQL里“修改数据”这件事。你会学到UPDATE语句的标准写法、WHERE条件为什么是救命稻草、多表关联更新怎么做、事务和锁在修改数据时扮演什么角色,以及我这些年踩过的各种坑。不管你是刚装好MySQL还没写过几条完整语句的纯新手,还是数据库课程设计做到一半卡在“改数据”环节的学生,这篇都能直接拿走照着操作。文中所有示例都基于同一个学生成绩表,你可以一步步跟着敲,跑完就真会了。
1. 修改数据前,先想清楚这三件事
很多零基础的同学看教程喜欢直接跳到语法,以为记住“UPDATE 表名 SET 列名=值 WHERE 条件”就完事了。但根据我自己的经验,真正导致修改操作翻车的,往往不是语法本身,而是动手之前没想明白三个问题:改哪张表、改哪些行、改成什么值。这三个问题想清楚了,你写的每一条UPDATE都是可控的;少想一个,就有可能在测试库里上演全表更新的事故。
1.1 修改操作的本质:一次针对“行”的批量覆盖
先建立一个大前提。MySQL里的数据存在表里,表由行和列组成。INSERT是往表里增加新的行,DELETE是删除某些行,而UPDATE做的事情,是“把满足条件的某些行,在指定列上的值,改成新的值”。
注意这里的两个关键词:“满足条件”和“指定列”。UPDATE从来不是针对某一个单元格单独操作——它是按条件圈定一批行,然后统一修改这些行的某些列。哪怕你只改一行,也是“在条件里精确圈定那一行”。这个认知特别重要,因为零基础阶段最容易犯的错误,就是觉得“我就改一条数据,不写WHERE也没关系吧”,但实际上UPDATE会把所有行都当成修改对象。
拿一张学生成绩表举例。先别急,咱们凭空造一张表出来,后面所有示例都用它:
sql复制CREATE TABLE student_score (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
student_name VARCHAR(50) NOT NULL COMMENT '学生姓名',
course VARCHAR(50) NOT NULL COMMENT '课程名称',
score DECIMAL(5,2) NOT NULL COMMENT '成绩',
updated_at DATETIME DEFAULT NULL COMMENT '最后修改时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入几行测试数据:
sql复制INSERT INTO student_score (student_name, course, score, updated_at) VALUES
('小明', '数学', 82.50, NOW()),
('小红', '数学', 91.00, NOW()),
('小刚', '数学', 58.50, NOW()),
('小明', '语文', 76.00, NOW()),
('小红', '语文', 88.50, NOW()),
('小刚', '语文', 62.00, NOW()),
('小明', '英语', 67.50, NOW()),
('小红', '英语', 95.00, NOW()),
('小刚', '英语', 54.00, NOW());
现在表里一共有9行数据。接下来所有修改操作,都在这张表上演示。
1.2 第一件事:确定表名,注意反引号的坑
“改哪张表”听起来像废话,但实际操作里真有人在这儿翻车。一方面表名可能写错,导致“Table doesn't exist”报错;另一方面有些表名是MySQL的保留关键字,比如order、group、select、user这类词,直接写会报语法错误。解决办法很简单:表名和字段名用反引号(`)包起来。
sql复制UPDATE `student_score` SET `score` = 90.00 WHERE `id` = 1;
虽然这个例子里表名和字段名都不是关键字,包不包都能跑通,但我建议从零基础阶段就养成“表和字段都加反引号”的习惯。这跟你骑车先学刹车一个道理——平时可能用不上,真遇到保留字命名的表时,这个习惯能直接救你一把。
1.3 第二件事:圈定要修改的行,WHERE不能靠感觉
这是三个问题里最关键的一个。如果你写:
sql复制UPDATE `student_score` SET `score` = 60.00;
恭喜你,小明的数学、小红的小学、小刚的英语……9行数据的score全部变成60.00。这条语句语法完全正确,MySQL也会老老实实执行,没有任何报错。它的问题不在语法,而在“没有WHERE条件等于全表所有行都满足条件”。
所以动手写UPDATE之前,脑子里一定要过一遍:我到底想改哪些行?是小明一个人的数学成绩?还是所有数学课成绩?还是所有不及格的成绩?这个“圈定范围”的动作,在SQL里就靠WHERE子句来完成。WHERE后面写什么条件,就圈定哪些行。条件越精确,修改范围越小,越安全。
1.4 第三件事:确定新值,类型和来源都得想清楚
最后是“改成什么值”。这里有两个容易忽略的细节。第一个是数据类型要匹配:score列是DECIMAL(5,2),你往里写'abc',MySQL会报错或者做隐式转换;你写90,MySQL会自动变成90.00,这没问题,但最好养成显式写对格式的习惯。第二个是新值可以来自计算或者查询,不一定是写死的常量。比如这次考试太难,要给所有人的数学成绩统一加5分,你完全不需要先查出来再改,一条语句就能搞定:
sql复制UPDATE `student_score`
SET `score` = `score` + 5
WHERE `course` = '数学';
看到没?SET子句右边不一定是固定数字,它可以是某个表达式的计算结果。对MySQL来说,当前行的score是多少,代入表达式算出新值,再写回该行。这个能力非常强大,也是后面进阶操作的基础。
我给新手的一个建议是:每一条UPDATE写完之后,先别急着执行,在脑子里过一遍“这句话会影响哪些行、把哪些列改成什么值”。能准确回答这个问题,你其实已经比很多人强了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. UPDATE基本语法:照着抄就能跑通的完整姿势
上一章把“改数据”的思路捋清楚了,这一章正式上语法。UPDATE语句的标准写法,说透了就那么几句话,但细节里全是分,我一条一条拆开讲。
2.1 UPDATE完整语法结构,从入门到熟练
MySQL官方文档里UPDATE的完整语法是比较复杂的,但对零基础来说,先把核心骨架记住就行:
sql复制UPDATE `表名`
SET `列1` = 新值1, `列2` = 新值2, ...
WHERE 过滤条件;
拆开看三部分:
- UPDATE 表名:告诉MySQL要改哪张表。
- SET 列=新值:告诉MySQL要把哪些列改成什么值。有多列要改时,用英文逗号分隔,不是AND,也不是分号。
- WHERE 条件:告诉MySQL只改哪些行。可以不写,但后果自负。
举个例子。假设小刚的数学成绩录入错了,实际是68.5分,现在要改过来:
sql复制UPDATE `student_score`
SET `score` = 68.50
WHERE `student_name` = '小刚' AND `course` = '数学';
执行完再看一下表:
sql复制SELECT * FROM `student_score` WHERE `student_name` = '小刚';
你会发现小刚只有数学成绩变成了68.50,语文和英语没动。这是因为WHERE条件同时限定了“学生是小刚”和“课程是数学”,两条同时满足才命中那一行。这告诉我们一件事:修改条件越具体,误伤的可能性就越小。如果只写WHERE student_name = '小刚',小刚的三门课成绩会全变成68.50。
2.2 同时修改多列:SET子句里的逗号是重点
实际业务里,改数据经常是好几列一起改。比如小刚这次数学补考过了,不但成绩从58.50改成6.00(开玩笑,改成78.50),记录一下修改时间:
sql复制UPDATE `student_score`
SET
`score` = 78.50,
`updated_at` = NOW()
WHERE `student_name` = '小刚' AND `course` = '数学';
这里的关键点:SET子句后面多列赋值用逗号分隔。有些零基础的同学会习惯性地写成AND:
sql复制-- 错误写法示例
UPDATE `student_score`
SET `score` = 78.50 AND `updated_at` = NOW()
WHERE ...;
这样写MySQL会直接报语法错误。记住,逗号分隔多列赋值,这是UPDATE语法最容易踩的第一个小坑。
另外注意NOW()这个函数,它返回当前系统时间。在SET子句里直接用函数,可以让“最后修改时间”列跟着更新,非常实用。类似的函数还有很多,比如DATE_ADD、CONCAT等,都可以用来生成新值。
2.3 用表达式在原有值基础上修改:score = score + 5
再回到那个加分的例子。UPDATE最强大的地方之一,就是新值可以引用该行本身的旧值。上一章提过一句,这里展开讲。
需求:所有数学成绩统一加5分。
sql复制UPDATE `student_score`
SET `score` = `score` + 5
WHERE `course` = '数学';
执行时,MySQL会逐行检查course列是否等于'数学'。对于满足条件的每一行,读取当前的score值,加上5,再写回score列。由于排序、赋值顺序等因素,你不用担心“加完这行会影响另一行”——UPDATE在InnoDB里是逐行按当前快照处理的,表达式用的是该行的旧值。
这个技能的实用场景太多了:
- 所有商品价格打八折:
SET price = price * 0.8 - 库存减少10件:
SET stock = stock - 10 - 每个人的年龄加一岁:
SET age = age + 1 - 阅读量加一次:
SET read_count = read_count + 1
有一个热搜词特别有意思,叫“mysql中int+5”,本质就是问“UPDATE时在int字段上做加法怎么写”。答案就是上面这种写法。但注意一个细节:如果你把score + 5写成了score = score + 5,这没问题;但如果你写SET score + 5,那语法就错了——必须写成“列 = 表达式”的格式。
2.4 用子查询动态生成新值:把另一张表的结果查出来更新
再进阶一点。新值不一定非要从当前表算出来,可以从别的表查。这就是子查询的用法。
比如,假设有另一张exam_grade表,记录每次考试每个学生的最终成绩:
sql复制CREATE TABLE exam_grade (
student_name VARCHAR(50),
course VARCHAR(50),
final_score DECIMAL(5,2)
);
现在你想把student_score表里数学成绩统一改成exam_grade表里查到的最终成绩。可以这么写:
sql复制UPDATE `student_score`
SET `score` = (
SELECT `final_score`
FROM `exam_grade`
WHERE `exam_grade`.`student_name` = `student_score`.`student_name`
AND `exam_grade`.`course` = '数学'
)
WHERE `course` = '数学';
这段的核心思路:SET子句右边不再是固定的值或表达式,而是一个标量子查询——对每一行,MySQL执行一遍子查询,得到结果作为新值。子查询里可以用外层表的字段关联,比如student_score.student_name。
不用怕复杂,用的时候只需要记住:子查询的结果必须是单个值。如果查出了多行,MySQL会报“Subquery returns more than 1 row”的错误。真遇到这种情况,就得在子查询里加LIMIT 1,或者让条件更精确。
3. WHERE条件:决定“改哪些行”的生死线
这个章节,是整篇文章里我最想让你认真读的部分。因为我亲眼见过太多新手(包括当年的自己)在WHERE上栽跟头。可以说,SQL的增删改查里,UPDATE对WHERE的依赖程度最高——INSERT不需要WHERE,SELECT写错WHERE最多查错数据,DELETE写错WHERE有风险,但UPDATE写错WHERE,往往一执行就是一片数据的灾难。
3.1 忘记WHERE的全表更新事故:复盘与补救
先说事故。假设有人在你负责的库里执行了这么一条语句:
sql复制UPDATE `student_score` SET `score` = 0;
结果:表里所有行、所有课程的score全部变成0。MySQL不会问你“确定要改这么多行吗”,它执行就是执行了,而且一旦提交就很难回去。
那真遇到这种情况怎么办?如果还没提交,或者操作在事务里(后面会讲),可以立刻ROLLBACK回滚。但如果是默认的自动提交模式,执行完瞬间就落盘了,普通手段很难恢复。唯一有效的办法是:有备份就恢复到备份,没有备份只能手动一条条把旧值改回来——前提是你知道旧值是什么。
这也就是为什么我在第一章强调“先查后改”。SQL里有个非常实用的操作习惯,拿SELECT当UPDATE的“预览器”:
sql复制-- 先查:看看会命中哪些行
SELECT * FROM `student_score` WHERE `course` = '数学';
-- 确认无误后,再改
UPDATE `student_score` SET `score` = 100 WHERE `course` = '数学';
SELECT和UPDATE用一模一样的WHERE条件。查出来的结果如果正是你想改的那些行,再执行UPDATE。这一步多花十秒钟,能省掉后面无数个小时的恢复工作。我把这个习惯叫“UPDATE三连”:先SELECT计数、再SELECT预览、最后UPDATE执行。
3.2 常用WHERE条件写法:从精确匹配到模糊匹配
WHERE条件的写法直接决定你“圈定哪些行”。零基础阶段,下面这些写法足够覆盖绝大多数需求:
- 精确匹配:
WHERE student_name = '小明' - 比较运算:
WHERE score >= 60、WHERE score < 60 - 多条件组合:
WHERE course = '数学' AND score < 60(两个条件同时满足)、WHERE course = '数学' OR course = '英语'(满足其一) - 枚举匹配:
WHERE course IN ('数学', '英语') - 范围匹配:
WHERE score BETWEEN 60 AND 80 - 模糊匹配:
WHERE student_name LIKE '小%'(以“小”开头的名字) - 空值判断:
WHERE updated_at IS NULL(注意不是= NULL)
拿这些条件组合出具体场景。比如,要把所有不及格(低于60分)的数学成绩统一改为60分,但只改小刚和小明的,不动小红的:
sql复制UPDATE `student_score`
SET `score` = 60.00
WHERE `course` = '数学'
AND `score` < 60
AND `student_name` IN ('小刚', '小明');
初看条件有点多,但逻辑很清楚:课程是数学(限定范围)、成绩低于60(圈定要改的行)、学生是小刚或小明(进一步缩小范围)。多条AND条件从上往下逐层过滤,每加一个范围就小一圈。新手写WHERE的最大问题不是不会写,而是条件写得太少,导致范围圈得太大。
3.3 安全防护:让MySQL自己帮你拦住“全表更新”
总有人会问:“有没有办法让MySQL在没带WHERE的时候直接拒绝执行?”答案是有,而且官方就提供了方案——安全更新模式(SQL_SAFE_UPDATES)。
在MySQL命令行或者可视化工具里执行:
sql复制SET SQL_SAFE_UPDATES = 1;
开启之后,MySQL会拒绝执行那些不带WHERE或WHERE条件里没有使用索引列的UPDATE/DELETE语句。换句话说,它就是一道强制保险,在关键时刻拦住你手滑的全表更新。
不过要注意它的两个特性。第一,SET SQL_SAFE_UPDATES = 1只对当前会话生效,新开的连接需要重新设置。第二,如果WHERE条件用的列没有索引,也可能被拦下来,因为MySQL认为这仍然有扫描大量行的风险。比如:
sql复制UPDATE `student_score` SET `score` = 100 WHERE `score` = 60;
如果score列没有索引,即使你写了WHERE,SQL_SAFE_UPDATES也可能报错拒绝执行。解决办法是用主键或唯一键条件,比如WHERE id IN (1,2,3),或者给常用条件列加上索引。这里也建议零基础的同学平时就开着安全更新模式,等熟练掌握之后再考虑关闭。
3.4 可视化工具里的修改操作与事务陷阱
如果你用的是Navicat、MySQL Workbench这类图形化工具,修改数据还有另一种姿势:直接选中结果集里的单元格,改完点提交。
以Workbench为例,你执行SELECT查到结果后,可以直接在表格里编辑某个单元格的值,然后点“Apply”按钮提交。这种方式直观是直观,但有两个容易踩的坑:
- Workbench默认开启了自动提交事务,你在表格里每改一个单元格,Apply之后可能立刻生效,而且有些版本的行为是“一次Apply提交整个结果集的所有改动”,一旦改错就是批量错误。所以用工具直接改之前,最好先确认你只在目标行上做了修改,别手滑动到别的行。
- Workbench有自动生成的UPDATE语句预览。在你点Apply之前,它会弹窗展示“将要执行的SQL语句”,上面会写清楚是UPDATE哪张表、SET哪些列、WHERE条件是什么。这一步务必仔细看,尤其看WHERE条件是否精确。零基础阶段很多“我明明就改了一行,为什么全表都变了”的事故,都出在没看这个预览。
提示:不管在命令行还是在工具里,修改数据前都先看一眼“将要影响的行数”。命令行下执行UPDATE后,MySQL会返回“Rows matched”和“Changed”,如果matched的数值和你预期不符,那就要警惕——条件可能写宽了。
这一章讲的都是“防止改错”,但真正到了生产环境,光有安全习惯还不够,还得靠事务机制兜底。
4. 进阶修改技巧:多表联动、批量替换与排序更新
基础语法熟练之后,你会发现实际业务里的修改需求往往比“把小明数学改成分”要复杂得多。比如要根据另一张表的计算结果来更新当前表,或者一次更新几千行但每行的新值都不同。这些场景基础UPDATE也能做,但效率低、代码丑。下面这几个进阶技巧,是我日常用得最多的。
4.1 多表关联更新:JOIN让UPDATE不再孤军奋战
先看场景:student_score表里有学生姓名,但你想根据学生编号来改成绩,而学生编号存在student_info表里。这时候就要把两张表关联起来。
sql复制UPDATE `student_score` AS s
JOIN `student_info` AS i ON s.`student_name` = i.`student_name`
SET s.`score` = 90.00
WHERE i.`student_no` = '2024001';
这种写法的核心是:UPDATE后面跟的可以是多张表,通过JOIN关联,然后在SET里指定要改哪张表的哪一列,WHERE里也可以引用任意关联表的字段。
再举一个更真实的例子。比如期末考试后,要把每个学生的总分按班级排名前10%的人统一加5分。你需要关联班级表、计算排名,但这里我们先简化,只演示最简单的关联更新逻辑:
sql复制-- 给所有选了“数学提高班”的学生,数学成绩统一加3分
UPDATE `student_score` AS s
JOIN `course_info` AS c ON s.`course` = c.`course_name`
SET s.`score` = s.`score` + 3
WHERE c.`course_type` = '提高班' AND s.`course` = '数学';
多表UPDATE的语法里有个细节:SET子句中的列名前最好带上表别名(比如s.score),否则当两张表有同名列时,MySQL会报“Column 'score' in field list is ambiguous”的歧义错误。
4.2 REPLACE INTO:一条语句搞定“存在则改、不存在则插入”
除了UPDATE,MySQL还有一种看起来像修改数据但其实更“霸道”的操作——REPLACE INTO。它的逻辑是:先尝试插入一条新数据,如果发现唯一键或主键冲突,就先删除旧行,再插入新行。
比如student_score表,如果我们约定(student_name, course)是唯一键,那么:
sql复制REPLACE INTO `student_score` (`student_name`, `course`, `score`)
VALUES ('小明', '数学', 99.00);
如果小明已经有数学记录了,这条语句会先把旧的删掉,再插入新的(score=99)记录。看起来像更新,但本质是删+插。
那什么时候用REPLACE,什么时候用UPDATE?我的个人经验:如果除了“改数值”,你还需要重置整行的其他列、或者不确定这行存不存在时,REPLACE更方便。但如果只是改少数几列,而且你希望保留某些默认值或自增ID,REPLACE就要小心——它会生成新的自增ID,并且删除旧记录可能会触发外键删除行为。所以在有外键关联的复杂业务里,推荐还是老老实实用UPDATE。
4.3 带排序和数量限制的更新:ORDER BY + LIMIT
MySQL的UPDATE支持ORDER BY和LIMIT,只是平时用得不多。但某些场景特别好用。
比如“只把成绩最低的3个学生成绩加2分”:
sql复制UPDATE `student_score`
SET `score` = `score` + 2
WHERE `course` = '数学'
ORDER BY `score` ASC
LIMIT 3;
注意执行顺序:先按WHERE圈定所有数学成绩,然后按score从小到大排序,排序后取前3行进行修改。这个操作的细节在于“先排后改”——LIMIT是在排序之后生效的,加上LIMIT之后它只更新前3行,而不是把所有行都改掉。
同理,如果你想“只把最近修改时间最早的一行重置为0”,可以:
sql复制UPDATE `student_score`
SET `score` = 0
WHERE `course` = '英语'
ORDER BY `updated_at` ASC
LIMIT 1;
这个语法非常适合“小范围定向修改”的场景,但要注意:LIMIT在UPDATE里最好搭配ORDER BY一起使用,否则MySQL可能选任意行来更新,结果不可预测。
4.4 CASE WHEN表达式:一个UPDATE更新多种不同值
有个需求非常经典:根据成绩区间评级。60以下标记“不及格”,60-79标记“及格”,80-89标记“良好”,90以上标记“优秀”。如果要用多条UPDATE确实能完成,但太啰嗦了。用CASE WHEN一个语句就干净利落:
sql复制UPDATE `student_score`
SET `grade` = CASE
WHEN `score` < 60 THEN '不及格'
WHEN `score` < 80 THEN '及格'
WHEN `score` < 90 THEN '良好'
ELSE '优秀'
END;
这里假设表里已经有grade列。CASE表达式按顺序从上往下判断,一旦某个WHEN条件成立,就返回对应的值,后续不再判断。所以把“< 60”放在最前面,然后依次“< 80”、“< 90”,最后的ELSE兜底。
这种写法最大的好处是一次扫描,不同行得到不同结果,而不是每条UPDATE都是又一遍全表扫。对于几万行的大表,性能优势非常明显。
CASE WHEN在UPDATE里还有一个变体用法:根据不同行的类型更新不同列。比如:
sql复制UPDATE `student_score`
SET `score` = CASE
WHEN `course` = '语文' THEN `score` + 2
WHEN `course` = '英语' THEN `score` + 1
ELSE `score`
END
WHERE `course` IN ('语文', '英语');
这个写法相当于“语文加2分、英语加1分、其他不动”,只是用了同一个表达式的分支结构。一句话总结:当不同行要改成不同的值时,优先考虑CASE WHEN,而不是写多条UPDATE。
5. 事务与锁:让修改操作“进可攻、退可守”
改数据这件事,最大的恐惧是“改错了回不来”。而MySQL里专门有个机制解决这个问题——事务。这一章不准备讲太深的理论,就围绕“修改数据时会遇到的实际情况”,让你明白事务怎么用、锁是怎么回事、死锁为什么会出现。
5.1 没有事务的UPDATE:执行完就回不了头
MySQL默认的InnoDB引擎支持事务,但如果你什么都不做,每条语句是自动提交的。也就是说,你执行完UPDATE,它马上把改动写入磁盘,连反悔的机会都没有。
所以要做“可回滚”的修改,就得显式开启事务:
sql复制-- 开启一个事务
START TRANSACTION;
-- 执行修改
UPDATE `student_score` SET `score` = 100 WHERE `id` = 1;
-- 如果发现改错了
ROLLBACK;
-- 如果确认没问题
COMMIT;
流程是这样:START TRANSACTION开启事务后,你执行的UPDATE不会立刻生效(至少对其他会话不可见)。发现改错了就ROLLBACK回滚,回到事务开始前的状态;确认没问题就COMMIT提交,把改动永久落盘。整个过程就像一个“可反悔”的沙盒,在里面随便操作,只要不COMMIT就随时能退回来。
我自己的习惯是:任何手动修改数据的操作,都手动用START TRANSACTION包一层,SELECT验证无误再COMMIT。特别在命令行里,这一步能省掉很多心慌。
5.2 行锁与表锁:UPDATE执行时发生了什么
事务能保证一致性,离不开锁。当你执行UPDATE时,InnoDB会锁定被修改的行,防止其他事务同时改同一行。这是行级锁,粒度小、并发高,也是InnoDB比MyISAM更可靠的原因之一。
但是要注意,加锁的粒度取决于WHERE条件是否走索引。如果WHERE条件能用上索引,InnoDB只需要锁住满足条件的少数几行;如果条件无法用索引、需要全表扫描才能判断哪些行满足条件,InnoDB就可能给表里大量行甚至整个表加锁,这就退化成近似“表锁”的行为,并发性能直线下降。
这个知识点对零基础同学来说,现阶段只需要记住一个结论:UPDATE的WHERE条件要尽量使用索引列。最常见的主键id自然不用说,其他经常用来筛选的字段(比如student_no)可以考虑建索引。这既是为了查得快,也是为了锁得少。
5.3 死锁的成因与最简单的规避方法
先看一个经典死锁场景:
- 事务A:UPDATE id=1 的行,再UPDATE id=2 的行
- 事务B:UPDATE id=2 的行,再UPDATE id=1 的行
如果两个事务同时开始,A锁住了1、B锁住了2,然后A想锁2发现2被B占了,B想锁1发现1被A占了,两个事务互相等待,谁也不让谁,就形成了死锁。
MySQL检测到死锁后,会自动回滚其中一个事务,让你收到类似“Deadlock found when trying to get lock”的错误。对业务来说,这通常意味着某次更新失败,需要重试。
规避死锁最简单的办法:多个事务更新多行数据时,尽量保持相同的顺序。比如都按id从大到小更新,或者都按id从小到大更新,不要A按1->2、B按2->1。另外事务别开太长,尽早COMMIT,也能大幅降低死锁概率。
数据库死锁是热搜词里出现过的概念,很多面试题都会问。零基础阶段先做到“知道有这个东西、知道大概原因、知道用固定顺序更新”,就已经足够了。
5.4 大批量更新的性能优化与分批提交
假设要更新一万行数据,直接一条UPDATE把WHERE条件写上,MySQL会一次性扫描、加锁、修改这么多行。如果量再大,比如几十万行,就可能锁范围过大、日志量暴增、甚至影响线上业务。
我的做法是分批更新。每批只改几百行或一千行,处理完一批提交一次。以id为例:
sql复制-- 第一批
UPDATE `student_score`
SET `score` = `score` + 5
WHERE `id` BETWEEN 1 AND 1000;
-- 第二批
UPDATE `student_score`
SET `score` = `score` + 5
WHERE `id` BETWEEN 1001 AND 2000;
-- 以此类推...
或者用LIMIT:
sql复制UPDATE `student_score`
SET `score` = `score` + 5
WHERE `score` < 60
LIMIT 500;
执行完一批,SELECT count(*)看看剩余数量,再执行下一批。这样做的好处是每次事务都很短,锁持有时间短,对其他请求的影响小。而且一旦中间某批出问题,只需要回滚那一小批,不至于全盘重来。
提示:大批量修改之前,务必确认是否有足够的磁盘空间用于事务日志,最好在业务低峰期执行,并且先备份原表。这不是胆小,是每个老手都会做的常规操作。
6. 实战踩坑记录:字符集、类型转换与备份回滚
最后这一章,我把自己在“修改数据”这件事上踩过的、以及在各种社区看到别人踩过的坑集中列一遍。每一条都是真实教训,每一条对应的解决方案都是经得起验证的。你哪怕只记住其中一两条,都可能在未来某个深夜帮你保住一份数据。
6.1 字符集问题:UPDATE之后中文全变成了乱码
这个坑的核心是“连接字符集”和“表字段字符集”不一致。常见情况:表字段是utf8mb4,但客户端的连接字符集是latin1,你执行:
sql复制UPDATE `student_score`
SET `student_name` = '王芳'
WHERE `id` = 1;
结果表里存进去的可能是一串乱码“çè³”。为什么?因为MySQL认为你发来的字节流是latin1编码,存入utf8mb4字段时,按错误的字符集做了解释和转换。
解决办法很直接:执行修改前,先确认连接字符集。命令行里执行:
sql复制SET NAMES utf8mb4;
这个命令同时设置客户端、连接、返回结果的字符集都是utf8mb4。在Navicat等工具里,也可以在连接属性里把编码设置为utf8mb4。另外,建表时尽量用utf8mb4,这是MySQL 8.0的默认字符集,兼容性最好,连emoji都能存。
万一已经乱码了怎么办?别慌,如果数据还没被后续操作覆盖,可以通过检查和切换字符集来恢复。但这个过程比较曲折,与其学恢复,不如在每次写中文之前都养成“SET NAMES utf8mb4”的习惯。
6.2 隐式类型转换:WHERE条件明明写了却没生效
这个坑特别隐蔽。假设执行:
sql复制UPDATE `student_score`
SET `score` = 80
WHERE `student_name` = 123;
student_name列是VARCHAR字符串类型,但条件里写的是数字123。MySQL不会直接报错,而是做隐式类型转换——把字段值和数字比较,相当于在字符串上运用数字规则。结果往往是:无法匹配到任何行,或者匹配到意料之外的行(比如student_name='123'的会命中,但'小明'这种肯定不中)。
更常见的是身份证号、手机号这种“长得像数字”的字符串字段,如果不小心在WHERE里写了不带引号的数字,可能一条都不中,也可能全表扫描后错误匹配。
规避办法就一条:字符串类型字段的条件值,必须加单引号。哪怕你看到列里存的是“00123”,也要写成WHERE student_no = '00123',而不是= 123。这个习惯在写UPDATE时特别重要,因为SELEC查不出来还能看到结果,UPDATE直接改数据,一旦条件偏移,影响就大了。
6.3 没有备份直接更新:后悔药其实可以自己准备
“先备份再操作”这句话说了无数遍,但我坚持再来一遍。因为我自己就在没有备份的情况下,用一条UPDATE把测试库里几百行数据改坏过。那时候才真正理解,备份不是给“别人”准备的,是给“手滑的自己”准备的。
零基础最简单的备份方式,直接导出原表数据到SQL文件:
bash复制mysqldump -u root -p database_name student_score > student_score_backup.sql
执行完备份后,再随便UPDATE,心里就有底了。万一改错了,恢复也不难:
bash复制mysql -u root -p database_name < student_score_backup.sql
如果只想备份某几张表,mysqldump后面可以跟多个表名;如果只想备份符合条件的数据,也可以在mysqldump里加WHERE条件。平时数据库运维还会有binlog、定时备份等更完善的方案,但对零基础阶段来说,“更新前善用mysqldump”已经能覆盖绝大多数需求。
6.4 唯一键冲突与外键约束:UPDATE被拦住了怎么办
更新数据时,如果目标列上有唯一键约束,新值一旦和别的行撞了,就会报“Duplicate entry”错误。比如把学号2024002改成2024001,而2024001已经存在,MySQL会拒绝执行。
解决办法不是硬改,而是先想清楚业务逻辑:是真的允许重复,还是数据源有问题?如果确实要覆盖,可以先处理旧数据(比如先改旧学号为临时值,再改新学号),或者使用我们在4.2节讲的REPLACE INTO,但要接受它“删+插”的行为。
外键约束也是类似。如果子表里存在引用这条记录的关联数据,父表UPDATE主键或关联字段时,会因为外键约束被拦截。这个时候要么先处理好子表数据,要么在事务里按正确的顺序操作。我的经验是:尽量别更新主键或唯一键的值,这类字段一旦落地,就把它当成“写死的身份标识”。真要改,优先考虑“插入新行+停用旧行”的方案,副作用小得多。
6.5 修改数据之后的验证:不要把UPDATE当作终点
最后分享一个我坚持了很多年的习惯:UPDATE执行完,一定要用SELECT验一遍改动的结果。不是执行完看“Query OK”就完事了,而是:
sql复制-- 改之前
SELECT * FROM `student_score` WHERE `student_name` = '小刚' AND `course` = '数学';
-- 执行UPDATE
UPDATE `student_score` SET `score` = 80 WHERE `student_name` = '小刚' AND `course` = '数学';
-- 改之后,确认结果
SELECT * FROM `student_score` WHERE `student_name` = '小刚' AND `course` = '数学';
这三条连在一起执行,才是一个完整的“修改数据”闭环。前一条SELECT确认目标行和旧值,UPDATE执行改动,后一条SELECT确认新值对不对。特别在手动操作数据库的时候,这个习惯能帮你第一时间发现条件写偏、值算错之类的问题,而不是等到业务反馈才追查。
再补充一个小的验证技巧:如果改动涉及多行,可以用聚合函数快速核对总数和总数变化:
sql复制SELECT COUNT(*), AVG(`score`) FROM `student_score` WHERE `course` = '数学';
改动前跑一次,改动后跑一次,对比数字是否在预期范围内。这也是老手常用的“快速感知是否有异常”的方式,比一行行看效率高得多。
我把自己在实际项目里跟数据打交道这几年浓缩成一句话:改数据永远先看三样东西——表名对不对、WHERE条件范围窄不窄、新值算得准不准。然后记住五个字:备份、事务、验。这话虽然朴素,但每次手放在UPDATE语句的回车键上之前,我都会在心里过一遍。零基础学MySQL,语法本身真的不难,难的是养成那种“对数据始终保持敬畏”的肌肉记忆。希望你从这一篇开始,就能带着这种意识去操作数据库,往后越走越稳。
