前置知识: MySQL

DML 数据操作语言:INSERT、UPDATE、DELETE 的完整语法

10 min入门

以 players 排行榜表讲透 MySQL 写操作:多行 INSERT、INSERT SET、UPSERT 预告、事务防误操作、DELETE 与 TRUNCATE 的回滚代价与影响行数读法,附 1136/1048 真实报错调试。

前置知识

本文与 SQL 数据操作与查询 互为搭子:110 串工作流,本文讲全每条语法分支。统一案例 players 排行榜表(五列 id/name/score/level/updated_at)的建表脚本在 110 第 2 节,先复制执行再回来;没学过 DDL 也能读,建表只是「准备一张空表」。

学习目标

读完本文你将能够:

  1. 用单行、多行、INSERT ... SET 三种写法插入数据,说清没写的列拿到了什么;
  2. 用一条 UPDATE 同时改多列、基于原值更新,并在事务保护下执行写操作;
  3. 说出 DELETE 与 TRUNCATE 在类别、回滚、自增编号、速度上的四个区别;
  4. 读懂 Rows matched 与 Changed 的差异,发现「改了个寂寞」;
  5. 修复 ERROR 1136、ERROR 1048,并把无 WHERE 事故扼杀在事务里。

预计 45 到 60 分钟,全程动手。

1. 你现在要解决什么问题

排行榜上线了,你是它的后端。今天的需求单长这样:

  • 上午:新玩家「豆豆」「萌萌」注册,一个用常规写法,一个试试逐列赋值;
  • 下午:运营提交了 Rain 的最新成绩,还要给零分新玩家发「新手礼包」,分数和等级一起改;
  • 傍晚:玩家 Nova 注销账号,数据要删掉;
  • 深夜:你困得手抖,把一条 UPDATE 的 WHERE 写丢了——五分钟内必须救回数据。

这四件事对应 DML(Data Manipulation Language,数据操作语言)的三条语句:INSERT、UPDATE、DELETE——难的不是语法,是「写对」和「写错了能救回来」。

2. 准备战场:把数据灌成标准状态

全新的表直接灌数据;练过旧数据的表先 TRUNCATE TABLE players; 清空(自增编号从 1 重新开始,它和 DELETE 的区别见第 5 节)。然后灌入 110 篇同款的五位玩家——这就是多行 INSERT:一个语句、多组值,组与组用逗号隔开:

sql
INSERT INTO players (name, score, level) VALUES
  ('小明', 1250, 8),
  ('阿黄', 980, 6),
  ('Rain', 2410, 15),
  ('Nova', 760, 5),
  ('老K', 1520, 10);

预期输出:

text
Query OK, 5 rows affected (0.01 sec)
Records: 5  Duplicates: 0  Warnings: 0

用 SELECT id, name, score, level FROM players; 验证:五行依次是小明 1250/8、阿黄 980/6、Rain 2410/15、Nova 760/5、老K 1520/10。TRUNCATE 后 id 恰好 1 到 5;你的表若删过数据,id 会接续,示例里换成你查到的即可。

3. INSERT:把新行写进表

3.1 单行插入与「没写的列」

豆豆注册了,此刻只有名字:

sql
INSERT INTO players (name) VALUES ('豆豆');
SELECT id, name, score, level FROM players WHERE name = '豆豆';

预期:一行结果,id 6、score 0、level 1。没写的两列拿到了建表声明的默认值(NOT NULL DEFAULT,见 110 第 2 节)。INSERT 的规矩:列清单写了哪几列,VALUES 就给哪几列的值;没写的列交给默认值或 NULL。

3.2 INSERT … SET:逐列赋值

萌萌也注册了,换一种写法——每列一个等号,像给变量赋值:

sql
INSERT INTO players SET name = '萌萌', score = 0;

读起来直观,但只能插单行;批量场景一律回到多行写法。

3.3 撞车了怎么办:ON DUPLICATE KEY UPDATE 预告

假设你在写「存档同步」:客户端带着 id 反复上报成绩,普通 INSERT 第二次就会撞主键报 ERROR 1062。MySQL 提供一步到位的写法——撞了唯一键就转成更新:

sql
INSERT INTO players (id, name, score, level) VALUES (3, 'Rain', 2600, 15)
ON DUPLICATE KEY UPDATE score = 2600, level = 15;

预期输出 Query OK, 2 rows affected,查询 id = 3 可见 Rain 的分数被覆盖为 2600——撞键转更新时 MySQL 把「尝试插入 + 更新」记成 2 行。这叫 UPSERT;INSERT IGNORE、REPLACE 等家族对比见 MySQL 速查。

4. UPDATE:改数据,以及那条保命铁律

4.1 改单列:基于原值的表达式更新

Rain 又赢了一局加 300 分。注意「在原值上加」,不是先查出来算好再写回:

sql
UPDATE players SET score = score + 300 WHERE id = 3;
SELECT name, score FROM players WHERE id = 3;

预期:Rain | 2900。SET score = score + 300 右边的 score 读的是旧值,原地算好写回。先查后算再写回多一次往返,并发时后写还会覆盖先写。

4.2 改多列:新手礼包

运营活动:所有零分新玩家送 100 分、升 2 级。一条 UPDATE 用逗号分隔可同时改任意多列:

sql
UPDATE players SET score = 100, level = 2 WHERE score = 0 AND level = 1;

预期输出(豆豆、萌萌两人命中):

text
Query OK, 2 rows affected (0.01 sec)
Rows matched: 2  Changed: 2  Warnings: 0

多条件用 AND/OR 连接,查询条件完整用法见 DQL。

4.3 保命铁律:无 WHERE 即全表

现在复现深夜事故。你本想给阿黄加 10 分,手一抖少打了半句:

sql
START TRANSACTION;
UPDATE players SET score = score + 10;
SELECT name, score FROM players;
ROLLBACK;

第一行输出是 Query OK, 7 rows affected——全表七个人都被加了 10 分。规则只有一句话:UPDATE 没有 WHERE,作用范围就是全表,DELETE 同理。

没出事是因为套了事务(transaction):START TRANSACTION 开始记账,修改只对你可见;ROLLBACK 整体反悔;COMMIT 确认落账。生产库写 UPDATE/DELETE 因此固定三步——开事务、验证、提交:

sql
START TRANSACTION;
UPDATE players SET score = score + 10 WHERE id = 2;
SELECT name, score FROM players WHERE id = 2;   -- 确认只有阿黄变成 990
COMMIT;

本文只用事务「会回滚」这一层,隔离级别与锁的原理见 SQL 数据定义与高级对象 第 2 节。

5. DELETE 与 TRUNCATE:删数据的两种代价

5.1 按条件删除,并且能反悔

Nova 注销账号:

sql
DELETE FROM players WHERE id = 4;
SELECT id, name FROM players;   -- Nova 消失,剩 6 行

预期:Query OK, 1 row affected。DELETE 与 UPDATE 规则完全一致:WHERE 决定删谁,没有 WHERE 就是清全表。它同样能被事务反悔,完整体验一次「删了又救回来」:

sql
START TRANSACTION;
DELETE FROM players WHERE level = 2;   -- 豆豆、萌萌
SELECT COUNT(*) FROM players;          -- 5
ROLLBACK;
SELECT COUNT(*) FROM players;          -- 7,全都回来了

5.2 DELETE 与 TRUNCATE 的四个区别

清空整张表有两条路,代价完全不同:

维度DELETE FROM playersTRUNCATE TABLE players
语言类别DML(数据操作)DDL(数据定义)
回滚事务里可以 ROLLBACK隐式提交,不可回滚
自增编号保留,新行接着旧编号重置,从 1 重新开始
大表速度逐行删、逐行记日志,慢直接重建表,快得多

「不可回滚」不是吓唬人:DELETE 每删一行都要在 undo log 记一条「怎么撤销」,5.1 节的反悔才有可能,代价是日志多、速度慢;TRUNCATE 不做这份记录,所以快,也所以没有后悔药。语法细节见 DDL 数据定义语言。

5.3 把战场恢复成标准状态

执行下面的脚本让表回到标准状态——也是「测试数据脚本可重复执行」的示范:

sql
TRUNCATE TABLE players;
-- 重新执行第 2 节的五行 INSERT,然后补上新注册的两人:
INSERT INTO players (name, score, level) VALUES ('豆豆', 100, 2), ('萌萌', 100, 2);

6. 核心概念:读懂影响行数

每次写操作后客户端都会回话,学会读它就有了行车记录仪:

  • Query OK, N rows affected:真正被写动的行数;
  • Records: 5 Duplicates: 0 Warnings: 0:多行 INSERT 专用——插入 5 组、0 组撞键、0 条警告;
  • UPDATE 还多一行 Rows matched: X Changed: Y:matched 是 WHERE 命中的行数,changed 是值真的变化的行数。两者可以不相等:UPDATE players SET score = score WHERE id = 3; 得到 matched 1、changed 0——命中了但值本来就一样;matched 为 0 说明 WHERE 没命中,多半是条件写错。affected 远超预期?立刻想起 4.3 节。

7. 常见错误与调试实录

错误一:列数不匹配。多行 INSERT 时某组值少写了一个:

sql
INSERT INTO players (name, score, level) VALUES
  ('阿豪', 300, 2),
  ('铁蛋', 500);

真实报错:

text
ERROR 1136 (21S01): Column count doesn't match value count at row 2

读报错三步:1136 直译「列数与值的个数不匹配」;at row 2 指第 2 组值——('铁蛋', 500) 只有 2 个值,对不上 3 列;补上即可。

错误二:非空列塞了 NULL:

sql
INSERT INTO players (name, score) VALUES (NULL, 500);

真实报错:

text
ERROR 1048 (23000): Column 'name' cannot be null

报错把列名都指好了,来自建表时的 NOT NULL 约束——「让数据库替你挡脏数据」就是此刻。类似的还有 ERROR 1364(非空列又没给默认值),110 第 7 节演示过。

错误三:无 WHERE 全表更新,4.3 节复盘过,备忘一行:affected 远超预期,先 ROLLBACK,再找 WHERE。

8. 修改实验

  1. 把 3.1 节的注册语句改成插入「分数 50、等级 3」的玩家,先预测四列各是多少,运行验证;
  2. 把 4.2 节的新手礼包改成「等级小于 3 的玩家加 200 分、升 1 级」,先预测影响行数,事务里执行、确认 matched 后 COMMIT;
  3. 事务里 DELETE FROM players WHERE score = 0;,观察 COUNT(*) 前后变化,再 ROLLBACK 救回来。

9. 实际项目中的使用场景

  • 注册、下单类写入:单行 INSERT,靠 NOT NULL DEFAULT 兜底;
  • 初始化测试数据、批量导入:多行 INSERT 一次到位,配 TRUNCATE 做可重复的测试环境;
  • 加分、扣款、库存扣减:永远写 score = score + 300 这类基于原值的表达式,避免「先查后写」的并发覆盖;
  • 生产环境的 UPDATE/DELETE:事务三步走,宁可慢十秒不可裸奔;
  • 玩家注销:想清楚硬删(DELETE)还是软删(加 is_deleted 列打标记,数据可恢复)——真实系统九成选软删。

10. 小练习

预测题(不执行,先写答案):当前表处于 5.3 节的标准状态,执行

sql
UPDATE players SET level = level + 1 WHERE score < 1000;

客户端会报告几 rows affected?改的是哪几位玩家?写完答案再运行验证。

修改题:把 3.3 节的 UPSERT 改成「id = 5 的玩家设为 2000 分、12 级」,连跑两遍,验证第二遍不报错、结果不变。

修 Bug 题:下面的注册语句报 ERROR 1136,按读报错三步定位,再用「补值」或「减列」修复:

sql
INSERT INTO players (name, score) VALUES ('阿豪', 300, 2);

挑战题(不看提示自己写):写一段事务脚本删除「全表分数低于 500 的玩家」,删前先查询确认删的是谁,无误后提交。验收:脚本连跑两遍,第二遍 Rows matched 必须为 0。

11. 与之前和之后的知识的关系

12. 官方文档

13. 自我检查

  • 能不看资料写出单行、多行、SET 三种 INSERT,说出没写的列会拿到什么;
  • 能背出保命三步,并现场用 ROLLBACK 救一次全表事故;
  • 能对照表格说清 DELETE 与 TRUNCATE 的四个区别;
  • 拿到 1136 与 1048 报错,各自知道第一步看哪里。

本章总结

DML 三件套的语法一天就能记全,值钱的是三个习惯:写操作先想 WHERE(没有 WHERE 就是全表);先开事务(ROLLBACK 是唯一的后悔药,TRUNCATE 连后悔药都不给);写完立刻读影响行数。INSERT 进、UPDATE 改、DELETE 删,加上 SELECT,排行榜后端四类操作就齐了。

下一步

进入 SQL 数据操作与查询 把写法和查询串成完整工作流;之后 DQL 数据查询语言 把 SELECT 的每个子句讲透。