我带新手学 MySQL 的时候感觉最明显:前面几课学的都是“怎么把数据放进去、怎么把数据查出来”,哪怕操作出点小问题,最多是结果不符合预期,不至于出大事。但到了“数据表内容的修改和删除”这一课,画风一下就变了。UPDATE 和 DELETE 这两条 SQL,写不好真的会一波带走整张表的数据。这篇入门第四课的内容,我会把修改和删除的核心逻辑、语法结构、实操案例,还有一堆新手踩过的坑一次讲透,适合刚学完建表、插入和简单查询的读者继续往下走。
很多教程会把 UPDATE 和 DELETE 分开讲成两节课,但我更喜欢合并在一节课里。原因很简单:它们俩的安全隐患完全一样,都需要你先把“我要操作哪些行”这个问题想清楚。WHERE 条件写对了,两条语句都很安全;WHERE 条件写漏了,后果也一模一样,整张表的内容都有可能被改掉或清空。所以学这一课,重点不是背语法,而是建立对数据操作的敬畏感。
1. 为什么要把“修改和删除”单独拎出来讲
1.1 从一条出事的 UPDATE 看边界
有次我在练习环境里演示 UPDATE,为了省事,直接写了一条这样的语句:
sql复制UPDATE student SET score = 60;
本来我只想给某个考砸了的学生把成绩改成 60 分,结果因为漏写了 WHERE,整张表所有学生的成绩全部被改成了 60 分。
当时练习环境里只有几十条测试数据,所以问题不大。但这件事给我的印象特别深,因为在真实业务中,这个后果是完全不可接受的。数据库里存的可能是商品价格、用户余额、订单状态,一条没带 WHERE 的 UPDATE 会把所有商品价格改成同一个值,把所有用户余额清零。
DELETE 也一样。如果执行的是:
sql复制DELETE FROM student;
那含义不是“删除某个学生”,而是“清空学生表里的所有记录”。即便 MySQL 命令行通常会提示影响了多少行,真到线上环境里,这个提示救不了你,数据已经没了。
所以我在课程里反复强调一个概念:UPDATE 和 DELETE 是“先选行,再操作”,选行靠的是 WHERE 条件。WHERE 才是这两条语句的灵魂,语法只是形式。
1.2 本课内容在整个学习路线中的作用
很多初学者会有一个误区,觉得数据库入门就是学会 SELECT,修修改改等到工作了再学也不迟。其实不是。
在真实的开发和运维场景里,UPDATE 和 DELETE 的使用频率并不低。用户改头像、改昵称,是 UPDATE;用户注销账号,是 UPDATE 或 DELETE;清空过期日志、删除错误数据,是 DELETE。甚至可以说,一个系统只要不是只读的,就永远离不开这两条语句。
这一课处在入门路线中的“分水岭”位置。前面学的 INSERT 是追加数据,SELECT 是读取数据,它们都不会破坏已有内容。但 UPDATE 和 DELETE 会。从这一课开始,你需要养成一个习惯:每次执行操作之前,先问自己三个问题。
- 我要操作的是哪张表?
- 我要操作哪些行?WHERE 条件能不能唯一圈定这些行?
- 如果操作出错,我有没有后悔药?
最后一个问题很重要。SQL 初学者往往没概念:没有开启事务的情况下,执行完 UPDATE 或 DELETE,数据修改是直接落盘的,没有“Ctrl+Z”可用。所以在后面的内容里,我会专门讲事务、备份和逻辑删除这些“后悔药”方案。
1.3 动手之前先建立三个底层习惯
第一,写 UPDATE 和 DELETE 之前,先用 SELECT 试试条件。
比如你想删除学生表里分数低于 60 的记录,不要直接写 DELETE,先写:
sql复制SELECT * FROM student WHERE score < 60;
看一眼返回的结果,是不是你想删的那些行。如果 SELECT 出来 200 行,而你预期只有 3 行,说明条件写错了。等确认无误,再把 SELECT * 换成 DELETE,其他部分不动。
这个习惯听起来很简单,但它能挡掉绝大多数误操作。我见过太多因为条件写反、大于号小于号弄混、日期边界没算对而导致的灾难。
第二,始终带着“影响行数”的意识去执行。
MySQL 执行 UPDATE 或 DELETE 后,会返回一个“Query OK, 1 row affected”之类的提示。这里的 affected 就是指这次操作实际影响了多少行。如果你预期影响 1 行,实际影响 1000 行,那就说明 SQL 出了问题,要马上停下来排查。
第三,线上操作前确认备份或事务。
哪怕是刚学 MySQL 的新手,也应该在一开始就建立备份意识。后面我会示范通过事务配合 ROLLBACK 回滚来保护操作,这算是入门阶段最容易掌握的“后悔药”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. UPDATE 与 DELETE 核心语法拆解
2.1 UPDATE:怎么“改”才算改得准
先看标准语法结构:
sql复制UPDATE 表名
SET 列名1 = 新值1, 列名2 = 新值2
WHERE 条件;
来拆开理解。
UPDATE 后面跟表名,表示要修改哪张表。SET 负责“赋值”,可以同时给多个字段赋值,字段之间用英文逗号分隔。WHERE 负责“圈定范围”,只有满足条件的行才会被修改。
举一个最基础但也最典型的例子。
学生表结构假设是这样的:
sql复制CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
class_id INT,
score DECIMAL(5,2)
);
想把 id 为 1 的学生姓名改成“小新”,成绩改成 88.5:
sql复制UPDATE student
SET name = '小新', score = 88.5
WHERE id = 1;
执行结果是:
text复制Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
注意 MySQL 客户端返回的信息里有三个指标:matched 表示 WHERE 匹配到了几行,changed 表示实际修改了几行。如果你把某个字段的新值设成和旧值一样,matched 可能是 1,但 changed 会是 0,这属于正常情况。
再来看几个新手容易出问题的细节。
第一,SET 里给多个字段赋值时,用的是逗号,不是 AND。有的同学会把 UPDATE 写成“SET name = ‘小新’ AND score = 88.5”,这不对。AND 是逻辑操作符,用在这里 MySQL 会直接报语法错误,或者把表达式当成普通值处理。
第二,SET 里可以写表达式。
比如给所有三班学生的成绩统一加 5 分:
sql复制UPDATE student
SET score = score + 5
WHERE class_id = 3;
这里“score = score + 5”的意思是:取当前行的 score 值,加 5,再写回 score 字段。这个操作在真实业务里也常用,比如给订单金额打九折、给库存做扣减:
sql复制UPDATE product
SET stock = stock - 1
WHERE product_id = 10086;
但用到这类自增减表达式时,要格外小心字段可能为 NULL 的情况。NULL 加任何数还是 NULL,结果会把原本正常的字段“改坏”。如果字段允许为空,一般得先用 IFNULL 处理,写成这样:
sql复制UPDATE product
SET stock = IFNULL(stock, 0) - 1
WHERE product_id = 10086;
第三,WHERE 条件是写 UPDATE 的重中之重。如果省略了 WHERE,MySQL 会更新表中所有行,而不是像某些图形化工具那样弹出“是否确认”。命令行环境下没有任何二次确认,直接执行,直接生效。
2.2 DELETE:怎么“删”才不删过头
DELETE 的语法更加直接:
sql复制DELETE FROM 表名
WHERE 条件;
它的逻辑是:从表里删除所有满足 WHERE 条件的行。
比如删除学生表里 id 为 5 的这条记录:
sql复制DELETE FROM student WHERE id = 5;
删除分数低于 60 分的记录:
sql复制DELETE FROM student WHERE score < 60;
WHERE 同样不能省略。如果写成:
sql复制DELETE FROM student;
那就是清空 student 表里所有数据,只留下空表结构。很多初学者在测试库里误执行过这条语句,心里阴影不是一般大。
DELETE 还有一个实际开发中特别常见的“变体”,叫逻辑删除。因为物理删除会把记录彻底抹掉,一旦后面需要追溯历史就麻烦,所以业务系统通常不给用户真正的 DELETE,而是在表里加一个标记字段。比如:
sql复制-- 添加一个逻辑删除标记字段
ALTER TABLE student ADD COLUMN is_deleted TINYINT NOT NULL DEFAULT 0;
-- “删除”某条学生记录
UPDATE student SET is_deleted = 1 WHERE id = 1;
-- 查询时自动过滤掉已删除的记录
SELECT * FROM student WHERE is_deleted = 0;
这样数据还在表里,但业务查询层面已经看不到它了。这个思路在真实项目中非常常见,虽然这一课叫“删除”,但在很多公司里,你写出的 DELETE 反而很少真正执行物理删除。当然,本文后面讲的 DELETE 语法还是要学会,因为清测试数据、删临时表数据、清理过期日志时,物理删除依然有用武之地。
2.3 TRUNCATE、DELETE、DROP,三兄弟怎么区分
除了 DELETE,MySQL 里还经常会碰到两个和“删”有关的操作:TRUNCATE 和 DROP。很多初学者容易搞混,我把它们放在一个表里说清楚。
| 操作 | 类型 | 作用范围 | 能否加 WHERE | 能否回滚 | 是否保留表结构 |
|---|---|---|---|---|---|
| DELETE | DML | 按条件删除某些行 | 可以 | 事务内可以 | 保留 |
| TRUNCATE | DDL | 清空整张表所有行 | 不可以 | 不可回滚 | 保留 |
| DROP | DDL | 删除整张表(连结构和数据一起) | 不可以 | 不可回滚 | 不保留 |
从表格里可以看得很明白:DROP TABLE 是最彻底的,它会把表直接删没了,想找回只能靠备份;TRUNCATE 会把所有行清空,但表结构还在;DELETE 则是最灵活的行级删除,可以通过 WHERE 精确指定范围。
那什么时候用 TRUNCATE?比如开发环境里的临时表数据已经没用了,你只想快速清空表,又不想一条条删,可以用 TRUNCATE TABLE student。
TRUNCATE 比 DELETE 快很多,因为它不逐行触发删除动作,而是直接释放整张表的数据页。但也有几个和 DELETE 不一样的地方要注意:
- TRUNCATE 不能加 WHERE,只能清空全表。
- TRUNCATE 在执行时会隐式提交事务,所以在事务里执行了 TRUNCATE,之后想 ROLLBACK 是回不去的。
- 另外,如果有外键约束引用了这张表,TRUNCATE 通常没法执行,会报错。
还有一个很实际的区别,DELETE 删除记录后,表的自增主键计数器不会重置;而 TRUNCATE 清空表之后,自增主键通常会被重置,下一条插入的数据又从头开始编号。所以如果只想删除数据,又希望以后插入的数据主键保持连续,TRUNCATE 会符合预期,但先要确保没有外键问题。
这里提醒一句:刚入门时,建议优先用 DELETE,少碰 TRUNCATE 和 DROP。因为 DELETE 好歹还能用事务回滚、能用 WHERE 精确控制,而 TRUNCATE 和 DROP 都属于“泼出去的水”,基本收不回来。真要在非测试环境执行它们,请把备份工作做到位再说。
3. 跟着做一遍:数据表内容的修改和删除全流程实操
3.1 准备一套可练习的测试表和测试数据
纸上谈兵没意思,我直接在本地 MySQL 里从头建一套可以跟着跑的表和数据。为了演示外键的联动效果,我建两张表:班级表和姓名,给学生表加上指向班级表的外键。
如果你用的是命令行,先进入 MySQL:
bash复制mysql -uroot -p
然后执行下面的建表和初始化脚本。请看清楚:这里的 DROP TABLE 只是为了让演示可以重复执行,并不属于本课要学的“删除数据内容”,不要把它和生产环境混为一谈。
sql复制DROP TABLE IF EXISTS student;
DROP TABLE IF EXISTS class;
CREATE TABLE class (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
class_id INT NULL,
score DECIMAL(5,2),
CONSTRAINT fk_student_class
FOREIGN KEY (class_id) REFERENCES class(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO class (name) VALUES ('一班'), ('二班'), ('三班');
INSERT INTO student (name, class_id, score) VALUES
('小新', 1, 88.50),
('小月', 1, 73.00),
('小北', 2, 92.00),
('小南', 3, 65.50),
('小谷', 2, 58.00);
现在 student 表里有 5 条记录,class 表里有 3 条记录。我会用这套数据把 UPDATE、DELETE、外键约束、事务回滚全部演示一遍。
3.2 实战操作:按条件修改数据
先来看修改数据。想把学生“小新”的姓名改成“小新同学”,成绩改成 90 分:
sql复制UPDATE student
SET name = '小新同学', score = 90.00
WHERE id = 1;
执行结果:
text复制Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
这里 WHERE id = 1 用的是主键条件,特征非常明确,即使表里有几万条数据,也不会误伤其他人,这就是为什么我建议新手修改单条记录时优先用主键或唯一键做条件。
再来演示按范围批量修改,把一班所有学生的成绩统一加 5 分:
sql复制UPDATE student
SET score = score + 5
WHERE class_id = 1;
执行完以后我想看看结果,顺手执行了一个查询:
sql复制SELECT id, name, class_id, score FROM student;
结果大致是这样:
| id | name | class_id | score |
|---|---|---|---|
| 1 | 小新同学 | 1 | 90.00 |
| 2 | 小月 | 1 | 78.00 |
| 3 | 小北 | 2 | 92.00 |
| 4 | 小南 | 3 | 65.50 |
| 5 | 小谷 | 2 | 58.00 |
注意一个小细节:小新原来的成绩我改成了 90,又执行了加 5 分,但因为它的 class_id 是 1,所以会再次加 5,变成 95,这里我为了表格简洁没有重新查询最新值。你可以自己多跑几次 SQL 看看数值变化,这正是理解 UPDATE 执行过程的好方法。
这里要特别提醒一点:UPDATE 的 SET 表达式是从“当前行读取旧值”开始计算的。如果一条 SQL 要更新多行,行与行之间互不影响,不用担心前一行改了会对后一行产生额外干扰。但如果你在同一张表上执行了两次 UPDATE,第一次的结果会成为第二次的输入,这点要心里有数。
再做一次带多条件的练习。把二班且成绩低于 60 分的学生,成绩统一改成 60 分:
sql复制UPDATE student
SET score = 60.00
WHERE class_id = 2 AND score < 60;
这条 SQL 应该会命中“小谷”这条记录,把它从 58 分改成 60。通过这个例子也能看得出来,WHERE 里支持 AND、OR、IN、BETWEEN、LIKE 等各种查询条件,凡是 SELECT 里能用的过滤逻辑,UPDATE 里都可以直接用。
3.3 实战操作:按条件删除数据
修改演示完了,来做删除练习。
先删除 id 为 4 的学生“小南”:
sql复制DELETE FROM student WHERE id = 4;
返回结果:
text复制Query OK, 1 row affected (0.01 sec)
然后删除二班里成绩低于 60 分的记录(刚才已经把“小谷”改成 60 分了,所以现在这个条件不会命中它):
sql复制DELETE FROM student WHERE class_id = 2 AND score < 60;
返回 0 row affected 是正常的,因为符合条件的记录已经不存在了。想顺手验证 DELETE 的批量效果,你可以再插入几条测试数据然后删除整个班的人:
sql复制DELETE FROM student WHERE class_id = 3;
这条会把所有三班学生都删掉。在 DELETE 和 UPDATE 中,WHERE 的条件范围完全决定影响范围,这是最值得反复练习的地方。
如果想把整张表的数据全部清空,可以用:
sql复制DELETE FROM student;
它会逐行删除所有记录,但表本身还在。如果想达到同样的清空效果但更快,可以使用:
sql复制TRUNCATE TABLE student;
这里要注意,因为 student 表被外键关系引用,而且它自己引用了 class 表,所以 TRUNCATE 不一定会成功。如果报错也没关系,正好说明外键约束对清空操作有限制,这也是下一节要讲的常见问题。
3.4 遇到外键约束时的处理思路
我用外键冲突来演示一个真实项目里常遇到的问题。
刚才的 student 和 class 之间存在外键关系:student.class_id 引用 class.id。现在我尝试直接删除班级表里的“二班”:
sql复制DELETE FROM class WHERE id = 2;
MySQL 会报错,错误信息大致是:
text复制ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails
原因很简单:student 表里还有学生引用着 class_id = 2 这个班级。如果直接把班级删了,那些学生就成了“无班人员”,外键约束不允许出现这种数据不一致的情况。
解决思路有两种。
第一种,先处理子表里关联的记录,再删除父表记录。如果想把二班的学生全部转到三班,可以这样:
sql复制UPDATE student
SET class_id = 3
WHERE class_id = 2;
然后再执行:
sql复制DELETE FROM class WHERE id = 2;
这样就不会报错了。这里的核心思想是:先解除引用关系,再删除被引用的行。
第二种,把子表相关行的外键字段置为 NULL,然后再删除父表。前提是该字段允许为空:
sql复制UPDATE student
SET class_id = NULL
WHERE class_id = 2;
DELETE FROM class WHERE id = 2;
如果业务上确实希望“删除班级时自动清除该班级下的学生”,可以在建表时使用 ON DELETE CASCADE。但我建议新手不要急着用 CASCADE,因为它会把删除操作“链式放大”,一条 DELETE 可能通过外键关系连带删掉多张表的数据,危险系数高很多。
看到这里,你其实已经顺带掌握了很多正规教程要等到后面才讲的外键知识。数据表的删除从来不是单独的一张表问题,表与表之间的关联关系会直接影响你能删什么、不能删什么,以及在删除前需要做什么准备。
4. 修改和删除遭遇的常见故障与排查技巧
4.1 SQL 报 1175 安全更新错误
很多用 MySQL Workbench 的同学应该对这个报错很眼熟:
text复制ERROR 1175: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column
意思是:你正处于“安全更新模式”下,尝试执行的 UPDATE 或 DELETE 没有使用带键(KEY)列的 WHERE 条件,所以 MySQL 拒绝执行。
这是图形化客户端给新手加的一道保护锁。比如你执行:
sql复制DELETE FROM student WHERE score < 60;
如果 score 列不是索引列,MySQL Workbench 就会拦截这条语句,因为它没法快速确定你要操作的范围,怕你误伤。
很多人会直接执行下面这条命令把安全更新模式关掉:
sql复制SET SQL_SAFE_UPDATES = 0;
我不建议你上来就关。更好的做法是让 WHERE 条件用上主键或索引列,或者在执行前先把目标数据查清楚。如果你确实需要非索引列作为删除条件,那么在测试环境里临时关闭安全模式倒也没问题,但千万别形成“遇到报错就关模式”的习惯。
4.2 修改删除超时或等待锁
新手经常会遇到一个灵异现象:明明是一条很简单的 UPDATE,执行后一直卡住不返回,过了几秒甚至几十秒才报错:
text复制ERROR 1205: Lock wait timeout exceeded
多数情况下是有另一个事务已经锁住了你要修改的行,但一直没有提交或回滚,导致你的 UPDATE 和 DELETE 只能排队等待。
排查时先看看当前有哪些事务在运行:
sql复制SHOW PROCESSLIST;
或者在 MySQL 5.7 以上版本查看 InnoDB 事务信息:
sql复制SELECT trx_id, trx_state, trx_started
FROM information_schema.innodb_trx;
如果是你自己在同一个会话里开了事务没提交,那只要执行 COMMIT 或 ROLLBACK 释放锁就行。如果是其他会话占着锁,可以通过 SHOW PROCESSLIST 找到对应的连接,确认安全后,再让对应会话提交或结束。
这个问题的核心教训是:事务一定要“短平快”,开了事务后,不要长时间停留在 SELECT 阶段,更不要忘记 COMMIT。锁持有时间越长,别人修改和删除同一行数据的等待时间就越久。
4.3 误操作后数据还能找回吗
这个问题几乎每个用 MySQL 的人都问过。若你已经执行了 UPDATE 或 DELETE 且事务已提交,那么普通方式确实救不回来了。但如果打开着 binlog,或者有合适的备份,仍有一定概率可以找回。
在入门阶段,我并不建议你去研究太复杂的恢复工具。真正值得做的是把功夫花在前面:
- 重要数据表做好定期备份,可以用 mysqldump 定时导出。
- 执行危险操作前先 SELECT 确认。
- 复杂操作放到事务里执行,给自己留一个 ROLLBACK 的机会。
- 业务代码里尽量使用逻辑删除,而不是物理删除。
对于初学者来说,后两条是最容易上手的后悔药。这里我用一个极简例子演示一下事务的效果:
sql复制START TRANSACTION;
DELETE FROM student WHERE id = 1;
SELECT * FROM student WHERE id = 1;
ROLLBACK;
SELECT * FROM student WHERE id = 1;
执行完 ROLLBACK 之后,你再去查 id 为 1 的记录,会发现它还在。因为 DELETE 被包在了事务里,只有执行 COMMIT 之后修改才会真正永久生效。这个机制可以让你在发现删错时立刻反悔。
有一点要特别提示:有些操作会自动触发隐式提交,比如 DDL 语句、TRUNCATE,甚至某些图形化工具在你执行时可能自动包了一层事务。所以事务也不是万能的,不要因为有了它就连备份都省了。
4.4 删除记录后自增主键不按规矩走
最后说一个很常见但很多人不理解的现象。
你删掉了 student 表里 id 为 4、5 的记录,然后插入一条新数据,发现新数据的 id 变成了 6,而不是 4。这是 MySQL 自增主键的正常行为:它记录的是“历史最大值”,并不会因为你删除了部分记录就倒退。
如果你想在清空所有数据后,让自增主键重新从 1 开始,可以:
sql复制ALTER TABLE student AUTO_INCREMENT = 1;
但前提是表里不能有比 1 更大的记录存在。如果用了 TRUNCATE,MySQL 通常会自动重置自增计数。具体表现可能在不同版本和存储引擎下略有差异,但大体思路差不多。
如果你并不想真的重置主键,单纯是看着“跳号”不舒服,那就大可不必管它。自增主键只是保证唯一,不保证连续,这个认知越早建立越好。
最后再分享一个我很喜欢的工作习惯。每次我准备执行重要的 UPDATE 或 DELETE 之前,都会先复制一条 SELECT 语句,用同样的 WHERE 条件去查一次目标数据。确认完再决定下手。这个习惯帮我挡掉了非常多低级的麻烦,现在推荐给你,希望你在数据表内容的修改和删除这条路上,少踩一些我当年踩过的坑。
