码上问学问代码之理,学编程之道

第 6 章 · 数据的增删改

「古典文学网」要能发文、收藏、取消收藏——光会查不够,还得会改。本章讲 INSERT 加数据、UPDATE 改数据、DELETE 删数据,以及 CASE 这类条件表达式。但要先说清楚:查错一行只是看不到,改错一行可能就再也回不来了,DELETE 没有回收站。所以这一章除了语法,更要练「先 SELECT 确认、再动手」的安全习惯。

为什么改数据要更谨慎

DELETE 没有回收站
先讲为什么上一章你查错了,最坏结果是「结果不对」,刷新一下重来即可。但改数据不一样:一条写错的 UPDATE 或 DELETE 会立刻落库,而且没有撤销按钮。
电脑里删文件还有个回收站,数据库里 DELETE FROM article; 一敲回车,6 篇作品瞬间清零——想找回来,只能靠备份。所以这一章的重点不是「怎么写得花」,而是怎么写得稳:改之前先想清楚范围,改之后立刻验证。

INSERT INTO … VALUES

往表里加新行
动手做用户点了「收藏」,就要往 favorite 表插一行。列名和值一一对应,顺序不能错。
单行插入.sqlsql可运行
-- 用户 1 收藏作品 5
INSERT INTO `favorite` (`user_id`, `article_id`) VALUES (1, 5);

-- 插完必须查一眼,确认真的进去了
SELECT id, user_id, article_id FROM `favorite` ORDER BY id;

-- 演示完删掉,恢复种子的 3 条收藏
DELETE FROM `favorite` WHERE `user_id` = 1 AND `article_id` = 5;
查询结果SQL 执行结果
运行结果: id | user_id | article_id 1 | 2 | 1 2 | 2 | 3 3 | 1 | 2 4 | 1 | 5 -- 共 4 行(新增的 id = 4 就是刚插入的那条)
动手做一次插多行,只要把每组值用括号括起来、用逗号隔开——比写多条 INSERT 快得多,因为只跟数据库打一次交道。
多行插入.sqlsql可运行
-- 一次给点赞表补两条
INSERT INTO `user_like` (`user_id`, `target_type`, `target_id`) VALUES
  (1, 'comment', 1),
  (1, 'article', 3);

SELECT id, user_id, target_type, target_id FROM `user_like` ORDER BY id;

-- 演示完删掉,恢复种子的 3 条点赞
DELETE FROM `user_like`
WHERE `user_id` = 1
  AND (`target_type`, `target_id`) IN (('comment', 1), ('article', 3));
查询结果SQL 执行结果
运行结果: id | user_id | target_type | target_id 1 | 1 | article | 1 2 | 2 | article | 1 3 | 2 | comment | 1 4 | 1 | comment | 1 5 | 1 | article | 3 -- 共 5 行(一次插进来 2 条:id 4、5)
讲透
  • id 不用你操心:favorite.id 是 AUTO_INCREMENT(自增主键),插入时留空,数据库自动给你一个新编号——所以种子是 1/2/3,新插的就是 4。
  • 没写的列会走默认值:favorite.created_at 有 DEFAULT CURRENT_TIMESTAMP,不写也会自动填上当前时间。但如果某列是 NOT NULL 且没有默认值,漏写就会报 Field 'xxx' doesn't have a default value。
  • 字符串要加单引号:'comment'、'article' 是字符串;数字 1、3 不加引号。
  • 批量插入的收益:一次插 1000 行,写 1 条多值 INSERT 比写 1000 条单条 INSERT 快一个量级——这是数据导入时的标准做法。
避坑别以为「多插一次没关系」。收藏表有联合唯一键 uk_fav_user_article (user_id, article_id)——用户 1 已经收藏了作品 2,如果你再 INSERT (1, 2),MySQL 会直接报错 Duplicate entry '1-2' for key 'favorite.uk_fav_user_article',整条语句失败。这正是唯一约束的价值:它帮你挡住了「重复收藏」这种脏数据。业务代码里通常用 INSERT IGNORE 或先查后插来兜住。

INSERT … SELECT:把查询结果灌进表

备份、归档、汇总都靠它
先讲为什么要备份「浏览量超过 300 的作品」,你不可能一条条手抄。INSERT … SELECT 说的是:把后面那个 SELECT 查出来的结果,整批塞进前面那张表。归档旧数据、生成统计表、表结构迁移,全靠它。
动手做先建一张空的「热门作品」表,再把 article 里符合条件的行灌进去。(建表的细节第 7 章讲,这里先照抄。)
归档热门作品.sqlsql可运行
-- ① 建一张结构一致的空表
CREATE TABLE `article_hot` (
  `id`    INT UNSIGNED NOT NULL,
  `title` VARCHAR(150) NOT NULL,
  `views` INT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='热门作品备份表';

-- ② 把查询结果整批插入
INSERT INTO `article_hot` (`id`, `title`, `views`)
SELECT `id`, `title`, `views` FROM `article` WHERE `views` >= 300;

-- ③ 验证
SELECT * FROM `article_hot` ORDER BY `id`;
查询结果SQL 执行结果
运行结果: id | title | views 1 | 静夜思 | 328 3 | 念奴娇·赤壁怀古 | 412 -- 共 2 行(只有浏览量 ≥ 300 的两篇被搬了过来)
讲透
  • 列要对得上:INSERT 括号里的列,和 SELECT 出来的列,个数、顺序、类型都要能对应,否则报 Column count doesn't match value count。
  • SELECT 里可以带任何条件、可以 JOIN:所以你能把「多张表算出来的结果」一次性写进一张新表,这是做数据报表最常用的招。
  • 和普通 INSERT 的区别:值不是你手写的,而是「查出来的」。这也意味着——只要 SELECT 的范围写错,插进去的就是错的,所以同样要先跑一遍 SELECT 看看。
  • 幂等提醒:本示例的 article_hot 以 id 为主键,重复执行会因主键冲突报错。生产里做归档通常用 INSERT IGNORE、REPLACE INTO,或先清空目标表。

UPDATE … SET … WHERE

改已有行的值
三种写操作:INSERT 加行、UPDATE 改格、DELETE 抽行——都动「行」这个单位INSERT INTO在表尾加一行行1行2行3新行 INSERTUPDATE … SET把选中行的某些格改掉行1行2 (改)行3行4DELETE FROM把选中行整行抽走行1行2行3 已删除行4⚠ UPDATE / DELETE 不带 WHERE = 全表沦陷(所有行都被改/删,且没有回收站)肌肉记忆:写 UPDATE / DELETE,先写 WHERE,再补 SET / DELETE
动手做用户打开作品详情页,浏览量要 +1。把「当前值」写进算式里(views = views + 1),数据库就会在这个值的基础上加。
(下面用 START TRANSACTION 包起来,演示完 ROLLBACK 还原;关于事务,本章第 7 节细讲。)
更新浏览量.sqlsql可运行
START TRANSACTION;

-- 只改 id = 1 这一行
UPDATE `article` SET `views` = `views` + 1 WHERE `id` = 1;

-- 改完立刻查,确认只动了这一行
SELECT id, title, views FROM `article` WHERE `id` = 1;

ROLLBACK; -- 演示完还原(原值 328)
查询结果SQL 执行结果
运行结果: id | title | views 1 | 静夜思 | 329 -- 共 1 行(328 + 1 = 329;ROLLBACK 后回到 328)
讲透
  • 语法三段:UPDATE 表 SET 列 = 新值 WHERE 条件。SET 后面可以同时改多列,用逗号隔开:SET views = views + 1, likes = likes + 1。
  • WHERE 决定「改哪些行」,是 UPDATE 的命门。写 WHERE id = 1 只改一行;写 WHERE dynasty = '唐' 就改 3 行;什么都不写,就是改整张表。
  • 值可以引用自身列:views = views + 1 表示「在原值上加 1」,这是计数器类更新的标准写法。
  • 执行 UPDATE 前先跑 SELECT:把 WHERE 条件单独放进 SELECT id FROM 表 WHERE 条件,看看会命中几行。命中数不对,说明条件写错了,赶紧改。
避坑:没有 WHERE 的 UPDATE 会更新全表。UPDATE article SET status = 1; 会把 6 篇作品的状态全部刷成 1——哪怕你只想改一篇,只是漏写了条件。这个错误在真实项目里能直接造成线上事故(比如「全员密码被重置」)。
两条保命规则:① 敲下回车前,盯着 WHERE 那一行看三秒;② 先 SELECT 确认范围,再改成 UPDATE。本示例用 ROLLBACK 兜了底,但真实环境没有后悔药。
危险示范.sqlsql可运行
START TRANSACTION;
UPDATE `article` SET `status` = 1; -- 漏了 WHERE!
ROLLBACK;
查询结果SQL 执行结果
运行结果: Query OK, 6 rows affected -- 6 行——整张 article 表都被改了

DELETE FROM … WHERE

删掉符合条件的行
动手做把「加数据 → 查确认 → 删数据 → 再查」走一遍,你就摸清了「改数据」的标准闭环。取消收藏的 WHERE 要用能唯一定位一行的条件(这里是「用户 + 作品」两个字段)。
收藏的增删闭环.sqlsql可运行
-- ① 插入:用户 1 收藏作品 6《陋室铭》
INSERT INTO `favorite` (`user_id`, `article_id`) VALUES (1, 6);
SELECT id, user_id, article_id FROM `favorite` ORDER BY id;

-- ② 取消收藏:删掉刚才那条
DELETE FROM `favorite` WHERE `user_id` = 1 AND `article_id` = 6;
SELECT id, user_id, article_id FROM `favorite` ORDER BY id;
查询结果SQL 执行结果
运行结果: -- ① 插入后 id | user_id | article_id 1 | 2 | 1 2 | 2 | 3 3 | 1 | 2 4 | 1 | 6 -- 共 4 行 -- ② 删除后 id | user_id | article_id 1 | 2 | 1 2 | 2 | 3 3 | 1 | 2 -- 共 3 行(回到种子的 3 条收藏)
讲透
  • 语法:DELETE FROM 表 WHERE 条件。删的是整行,不是某一列——想「清空某列的值」,那要用 UPDATE 表 SET 列 = NULL,别用 DELETE。
  • 删除后主键编号不回收:上面删掉了 id = 4 那条,下一次再插还是 id = 5(自增值不回退)。所以主键有洞是正常的,不要试图让 id 连续。
  • 「软删除」更常见:真实项目里,书评、作品通常不真删,而是把 status 置 0(UPDATE comment SET status = 0 WHERE id = 3)。这样评论还留着、可审计、可恢复——我们库里的 status 字段就是干这个的。
  • 别忘了外键连锁:删一条作品,引用它的书评和收藏会被「一起删掉或置空」(第 7 章的级联),所以删主表要更小心。
避坑:没有 WHERE 的 DELETE 会清空全表。下面这句会删掉全部 3 条收藏,且删除是立刻生效、无法回滚的(除非你是在事务里)。这是第 6 章最危险的一行代码,请务必养成习惯:先 SELECT * FROM 表 WHERE 条件 看命中范围,确认无误,再把它改成 DELETE。
危险示范.sqlsql可运行
START TRANSACTION;
DELETE FROM `favorite`; -- 漏了 WHERE!
ROLLBACK;
查询结果SQL 执行结果
运行结果: Query OK, 3 rows affected -- 3 行——收藏表被清空

CASE:在 SQL 里做条件分支

if…else 的 SQL 写法
先讲为什么产品说:「浏览量过 400 标『热门』,250 以上是『一般』,其余算『冷门』。」这种「按条件给出不同结果」的逻辑,在 Python 里是 if…elif…else。在 SQL 里,它就是 CASE——既能用在 SELECT 里「算出一列」,也能用在 UPDATE 里「按条件改值」。
动手做先在 SELECT 里用 CASE 现算一个「热度」列。注意条件从上往下判断,命中就停。
CASE 分类.sqlsql可运行
SELECT title, views,
       CASE WHEN views >= 400 THEN '热门'
            WHEN views >= 250 THEN '一般'
            ELSE '冷门' END AS 热度
FROM `article`
ORDER BY views DESC;
查询结果SQL 执行结果
运行结果: title | views | 热度 念奴娇·赤壁怀古 | 412 | 热门 静夜思 | 328 | 一般 声声慢 | 298 | 一般 春望 | 256 | 一般 陋室铭 | 203 | 冷门 天净沙·秋思 | 187 | 冷门 -- 共 6 行
动手做CASE 用在 UPDATE 里,就能按条件批量改值:把「浏览量达标」的作品自动设为推荐。
CASE 批量改值.sqlsql可运行
START TRANSACTION;

UPDATE `article`
SET `is_recommend` = CASE WHEN `views` >= 300 THEN 1 ELSE 0 END;

SELECT id, title, views, is_recommend FROM `article` ORDER BY id;

ROLLBACK; -- 演示完还原为种子值:1, 1, 1, 0, 0, 0
查询结果SQL 执行结果
运行结果: id | title | views | is_recommend 1 | 静夜思 | 328 | 1 2 | 春望 | 256 | 0 3 | 念奴娇·赤壁怀古 | 412 | 1 4 | 声声慢 | 298 | 0 5 | 天净沙·秋思 | 187 | 0 6 | 陋室铭 | 203 | 0 -- 共 6 行
讲透
  • 两种写法:① 搜索式 CASE WHEN 条件 THEN 值 … END,最灵活;② 简单式 CASE 列 WHEN 值 THEN 结果 … END,只做等值匹配。日常用搜索式就够了。
  • 顺序敏感:WHEN 是从上往下判断的,命中一个就跳出。所以「400 以上」必须写在「250 以上」前面;写反了,412 会先命中「≥ 250」被判成「一般」。
  • 别漏 ELSE:不写 ELSE 且所有条件都不满足时,结果是 NULL。在 UPDATE 里这就等于把那列清空了——所以改值场景几乎总要写 ELSE。
  • 本节示例的 is_recommend 变化:种子是 1,1,1,0,0,0,改后是 1,0,1,0,0,0——春望因为 256 < 300 被取消了推荐。所以演示用了事务,ROLLBACK 后回到种子状态。

事务与安全习惯

给修改加一道后悔药
事务:把几条写操作捆成一个「要么全成、要么全不作数」的包裹BEGIN三条写操作① 扣发布者积分② INSERT article③ UPDATE category 计数全部成功任何一步失败COMMIT正式生效ROLLBACK全部撤销别人看到的COMMIT 前:还是旧数据COMMIT 后:新数据pymysql 里尤其重要:连接默认 autocommit=False,写完必须 conn.commit()转账式操作(扣积分 + 加作品)不用事务 = 中途崩了积分凭空消失口诀:BEGIN 起手,COMMIT 收货,ROLLBACK 悔棋
先讲为什么本章的每个例子都用了 START TRANSACTION … ROLLBACK,你已经在不知不觉中用上了事务。
事务就是「把若干条修改打包成一个整体」:要么全部生效(COMMIT 提交),要么全部撤销(ROLLBACK 回滚)。转账就是经典场景——「扣我的钱」和「加他的钱」必须同生共死,不能只做一半。
动手做试着改一条收藏,看结果不对就回滚。
事务.sqlsql可运行
START TRANSACTION;

-- 把用户 1 收藏的作品从 2 改成 4
UPDATE `favorite` SET `article_id` = 4 WHERE `user_id` = 1 AND `article_id` = 2;
SELECT id, user_id, article_id FROM `favorite` ORDER BY id;

-- 一看改错了(想改的其实是另一条)→ 撤销
ROLLBACK;

-- 如果确认无误,就改成提交:
-- COMMIT;
查询结果SQL 执行结果
运行结果: id | user_id | article_id 1 | 2 | 1 2 | 2 | 3 3 | 1 | 4 -- 共 3 行(ROLLBACK 后,第 3 行恢复为 article_id = 2)
讲透:两条最实用的安全习惯
  • 习惯一:改前先 SELECT,改后再 SELECT。把要用的 WHERE 条件先放进 SELECT COUNT(*) 里跑一遍,确认命中行数;改完再 SELECT 一遍,核对值对不对。「看一眼」的成本,远低于「改错了再修」。
  • 习惯二:不确定就先关进事务。大批量修改前 START TRANSACTION;,改完 SELECT 检查,对了再 COMMIT;,不对就 ROLLBACK;。这是操作生产库的标配动作。
  • 注意:CREATE TABLE、ALTER TABLE、DROP TABLE 这类「改表结构」的语句在 MySQL 里会隐式提交,包在事务里也回滚不了(第 7 章会再遇到)。
  • 真删之前先备份:CREATE TABLE favorite_bak AS SELECT * FROM favorite; 先把整表复制一份,万一删错还能补救。
避坑事务不是万能的:开了事务却忘记 COMMIT,修改只存在于当前这个连接里,别的窗口看不到,关掉连接还会自动回滚——很多人以为自己「改成功了」,其实是幻觉。另外,事务应该尽量短:开了事务再去泡杯茶,会长时间占着锁,拖慢别人。改完尽快提交或回滚。

综合案例:发布一篇新作品

把本章串起来

「古典文学网」后台点「发布」,背后就是往 article 表插一行。用本章学的 INSERT + 事务,把整个流程走一遍(演示完回滚,不真正入库)。

发布作品.sqlsql可运行
START TRANSACTION;

-- 插入一篇新作品:id 自增,其余字段按业务填写
INSERT INTO `article`
  (`title`, `author`, `dynasty`, `category_id`, `summary`, `views`, `likes`, `is_recommend`, `status`)
VALUES
  ('望庐山瀑布', '李白', '唐', 1, '日照香炉生紫烟,遥看瀑布挂前川。', 0, 0, 0, 1);

-- 用「id 倒序取第一条」验证刚插入的行
SELECT id, title, author, dynasty, category_id, status
FROM `article` ORDER BY id DESC LIMIT 1;

ROLLBACK; -- 真实上线时改成 COMMIT;
查询结果SQL 执行结果
运行结果: id | title | author | dynasty | category_id | status 7 | 望庐山瀑布 | 李白 | 唐 | 1 | 1 -- 共 1 行(种子里 6 篇作品的 id 是 1–6,新的自增到 7)
讲透:一次「写操作」的标准动作① 想清楚要改哪张表、哪些列;② 先 SELECT 确认范围(这里是「查最新一条」来验证插入结果);③ 用事务包住 INSERT / UPDATE / DELETE;④ 检查无误再 COMMIT。注意 id 不用写、created_at 不用写(默认值会填),你只给「业务数据」。这套动作在 Flask 里会由 SQLAlchemy 帮你执行,但底层永远是这几句 SQL。
动手练习
1给「古典文学网」新增一位用户:username = 'libai2'、nickname = '青莲'、email = 'libai2@hiwen.local'、intro = '李白的小号'、gender = 1(password 是 NOT NULL,先填占位符 'PUT_HASH_HERE')。插完查一下 user 表,最后把这条测试用户删掉。
2把《天净沙·秋思》的浏览量 +50、点赞数 +3(提示:SET 里可以让列用它自己的值做运算)。
3迁移练习:给「红影赏析」的 movie 表(id、title、genre、rating)新增一部电影《流浪地球2》(类型「科幻」,评分 8.3),再把它的评分改成 8.5,最后把它删掉。想一想:这跟「收藏 → 取消收藏」是不是同一套「加、改、删」的动作?
参考答案
练习答案.sqlsql可运行
-- 第 1 题
INSERT INTO `user` (`username`, `password`, `nickname`, `email`, `intro`, `gender`)
VALUES ('libai2', 'PUT_HASH_HERE', '青莲', 'libai2@hiwen.local', '李白的小号', 1);

SELECT id, username, nickname, email FROM `user` ORDER BY id;

DELETE FROM `user` WHERE `username` = 'libai2'; -- 清理测试数据

-- 第 2 题
UPDATE `article`
SET `views` = `views` + 50, `likes` = `likes` + 3
WHERE `title` = '天净沙·秋思';

SELECT title, views, likes FROM `article` WHERE `title` = '天净沙·秋思';

-- 第 3 题(红影赏析,表结构不同,思路完全一致)
INSERT INTO `movie` (`title`, `genre`, `rating`) VALUES ('流浪地球2', '科幻', 8.3);
UPDATE `movie` SET `rating` = 8.5 WHERE `title` = '流浪地球2';
DELETE FROM `movie` WHERE `title` = '流浪地球2';
查询结果SQL 执行结果
运行结果: -- 第 1 题 id | username | nickname | email 1 | admin | 管理员 | admin@hiwen.local 2 | libai | 青莲居士 | libai@hiwen.local 3 | libai2 | 青莲 | libai2@hiwen.local -- 共 3 行 -- 第 2 题(187 + 50 = 237,22 + 3 = 25) title | views | likes 天净沙·秋思 | 237 | 25 -- 共 1 行 -- 第 3 题(在红影的库里执行) -- 插入后 rating = 8.3,改成 8.5,再删除,最终 movie 表回到原样 -- 具体条数以库里数据为准

第 3 题的关键:换的是表名和字段名,不变的是「INSERT 加 → UPDATE 改 → DELETE 删」这套骨架。三句话都有 WHERE——这就是安全底线。

本章小结