最近好几个人在后台问我:MySQL命令背了不少,真到要写增删改查的时候,总感觉没把握,明明看着挺简单的语法,一跑就报错,或者更难受——不报错,但数据不对。其实增删改查是MySQL日常使用频率最高的一组操作,也是所有复杂 SQL 的地基。很多人把它想得太简单,以为记住 INSERT、SELECT、UPDATE、DELETE 四个单词就算会了,但实际工作中坑很多:主键冲突、批量插入失败、WHERE 条件写错导致全表更新、字符集乱码、SQL注入风险……这篇就按我自己平时排查问题、带新人的思路,把这四个操作全部拆开讲透。无论你是刚学数据库的初学者,还是写Java/Python后端想补基础的同学,都能照着做。
1. 先解决最基础的问题:CRUD在MySQL里操作的最小工作单元是什么
1.1 表和行:你增删改查的对象从来不是数据库,而是“表里的行”
很多刚开始学 MySQL 的人会犯一个概念性错误:以为“增加数据库”就是增,“删除表”就是删。实际上,数据库服务、库、表、列、行这几个结构层级要分清楚。
- 数据库服务(MySQL Server)下面可以建多个库(Database)
- 库里可以建多张表(Table)
- 表由列(Column)和行(Row)组成
- 我们通常说的增删改查,是对一张表里的行数据进行操作
你写 INSERT INTO user,是在 user 表里增加一行;写 DELETE FROM user WHERE id = 3,是删除这一行;UPDATE user SET age = age + 1,是修改某些行。而 CREATE DATABASE、DROP TABLE 属于数据库结构定义语句,不属于今天说的 CRUD。
理解这个概念有什么实际价值?最大价值在于:你排查问题时会更有方向。例如“为什么删除之后空间没变小”这类问题,如果明白 DELETE 只是逻辑删除行数据,表文件未必立即缩小,后面就不会被表象带偏。
1.2 CRUD 与 SQL 关键字的映射关系
CRUD 是 Create(增)、Read(查)、Update(改)、Delete(删)四个单词的缩写。在 MySQL 中,它们对应的主要命令是:
| CRUD | SQL 关键字 | 常见扩展 |
|---|---|---|
| Create | INSERT | INSERT INTO ... VALUES / SET / SELECT |
| Read | SELECT | SELECT ... FROM ... WHERE / GROUP BY / ORDER BY / LIMIT |
| Update | UPDATE | UPDATE ... SET ... WHERE |
| Delete | DELETE | DELETE FROM ... WHERE / TRUNCATE / DROP |
另外还有个高频操作 REPLACE INTO,它名字里带个“Replace”,行为上像“先删后插”,严格说它属于 MySQL 对标准 SQL 的扩展,后面插入部分我会讲到它和 INSERT ... ON DUPLICATE KEY UPDATE 的区别。
1.3 动手前的必备动作:连接库、看表结构、看已有数据
别急着写增删改查。你连库里有哪些表、每张表有哪些字段都不知道,写出来的 SQL 多半要报错。实际操作时,我会先做这几步:
bash复制mysql -uroot -p
然后执行:
sql复制SHOW DATABASES;
USE test_db;
SHOW TABLES;
DESC user;
DESC user 会列出 user 表的字段名、类型、是否允许 NULL、是否有默认值、是否是主键等信息。这一步比去翻建表语句还快。知道每个字段的类型之后,写 INSERT 才不容易出现“字符串没加引号”“日期格式写错”这类低级问题。
很多时候我们写不出正确的 CRUD,不是因为语法不熟,而是对要操作的表结构不了解。所以我建议把“先 DESC 再看数据”变成肌肉记忆:
sql复制SELECT * FROM user LIMIT 5;
看一眼现有行的样子,再写后续语句,会稳很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 新增数据 INSERT:字段映射、默认值与批量插入的细节
2.1 INSERT 的基础语法,以及为什么推荐“显式写字段名”
INSERT 的基本写法有两种:
sql复制-- 写法一:不指定字段名(不推荐日常使用)
INSERT INTO user VALUES (1, '张三', 'zhangsan@example.com', 25);
-- 写法二:显式指定字段名(推荐)
INSERT INTO user (id, name, email, age)
VALUES (1, '张三', 'zhangsan@example.com', 25);
不指定字段名的写法有个致命问题:它要求 VALUES 里的内容必须和表结构里的字段顺序、数量完全一致。一旦你后来给表增加了一个字段,或者字段顺序发生变化,这条 INSERT 很有可能会报错,或者把数据插错列。我在教新人的时候反复强调:日常开发中请养成写字段名的习惯。多写几个字段名,换来的是可读性和稳定性。
而且显式指定字段名还有一个好处,可以只插入部分字段:
sql复制INSERT INTO user (name, email)
VALUES ('李四', 'lisi@example.com');
只要其他字段允许为 NULL 或有默认值,这条语句就能成功。比如自增主键 id 你不写,MySQL 会自动生成。
2.2 主键、NULL、DEFAULT 这几个细节,决定了 INSERT 能不能成功
INSERT 能不能成功,不只取决于语法。它还依赖字段约束。常见的坑有三个:
第一,主键重复。如果你的 id 是主键,而表里已经有 id=1,再插入 id=1 会报错:
text复制ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
解决办法要看业务场景。如果确实想忽略该次重复插入,可以使用 INSERT IGNORE;如果想冲突时改成更新,可以使用 ON DUPLICATE KEY UPDATE。这块我会在第 2.4 节展开。
第二,NOT NULL 字段没给值。如果某字段定义是 NOT NULL 且没有默认值,你 INSERT 时不传这个字段,执行会直接失败。所以拿到表结构时,先看哪些字段是 NOT NULL,哪些可以留空。
第三,字符串与数字的引号问题。往 VARCHAR 或 DATE 类型字段插入数据时,值需要加单引号;INT 类型可以不加,但加了 MySQL 也能隐式转换。稳妥做法是:字符和日期统一加引号,数字按情况加不加都行,但不要养成“全靠 MySQL 自动转换”的习惯,避免埋雷。
sql复制INSERT INTO user (name, birthday, age)
VALUES ('王五', '1998-06-15', 26);
2.3 批量插入:一次插多行,比循环单条 INSERT 快得多
很多初学者写代码时,会在循环里一条一条执行 INSERT。这样做在数据量小的时候没问题,数据量一旦上千上万,性能差别就很明显了。
MySQL 支持一次插入多行:
sql复制INSERT INTO user (name, email, age)
VALUES
('赵六', 'zhaoliu@example.com', 28),
('孙七', 'sunqi@example.com', 32),
('周八', 'zhouba@example.com', 24);
上面这条语句会插三行,但客户端与 MySQL 服务端只交互一次。对比循环单条 INSERT,它节省了大量网络往返时间。在实际项目中,如果写入几千行,我一般会把数据分批插入,每批几百到一千行,这样既不会让单条 SQL 过于庞大,写入速度也远快于逐行插入。
2.4 INSERT 遇主键或唯一键冲突时的三种处理姿势
插入数据时最怕遇到“重复数据”。这要分场景处理:
sql复制-- 1. 直接插入,冲突就报错,适合数据绝不允许重复的场景
INSERT INTO user (id, name) VALUES (1, '张三');
-- 2. 忽略冲突,适合导入数据时“已存在就跳过”的场景
INSERT IGNORE INTO user (id, name) VALUES (1, '张三');
-- 3. 冲突时改为更新,适合“有则更新、无则插入”的 upsert 场景
INSERT INTO user (id, name, email)
VALUES (1, '张三', 'newemail@example.com')
ON DUPLICATE KEY UPDATE
email = VALUES(email), age = VALUES(age);
如果你用 MySQL 8.0.20 及以上版本,官方推荐优先用 AS new 这种新别名写法,因为 VALUES() 函数在未来版本可能被弃用。但我见过很多老项目里仍然是旧写法,所以读懂旧代码也很重要。在处理批量导入、接口幂等等业务时,选择对冲突策略,比祈祷数据不会重复靠谱得多。
另一个很容易搞混的是 REPLACE INTO。它遇到主键冲突时会先删除旧行,再插入新行。这有一个副作用:如果表里有自增列,会重新生成自增值;如果表上有外键级联删除,还可能把关联数据删掉。所以不是特殊需求,我一般不会推荐无脑用 REPLACE INTO,ON DUPLICATE KEY UPDATE 的可控性明显更好。
3. 查询数据 SELECT:从 SELECT * 到能解决 80% 需求的查询套路
3.1 SELECT 的基本结构,以及实际执行时 MySQL 是怎么“想”的
SELECT 是 CRUD 里最核心、查询场景最丰富的一个。它的基本结构是:
sql复制SELECT 列名
FROM 表名
WHERE 条件
GROUP BY 分组字段
HAVING 分组后的过滤条件
ORDER BY 排序字段
LIMIT 偏移量, 行数;
写 SELECT 的时候,很多人是按照“先 SELECT 还是先 WHERE”的直觉去思考的,但 MySQL 服务端实际执行逻辑顺序是:
- FROM:确定从哪张表取数据
- WHERE:过滤数据行
- GROUP BY:分组
- HAVING:对分组结果过滤
- SELECT:投影出需要的列
- ORDER BY:排序
- LIMIT:分页截断
这有什么用?最大的用处是帮你定位“为什么报错”。比如你写 SQL 时想在 WHERE 里用 SELECT 阶段的别名,MySQL 基本不认识,因为执行到 WHERE 时别名还没算出来;反过来,在 ORDER BY 里用别名一般没问题,因为排序阶段已经过了投影阶段。
3.2 WHERE 条件与 NULL 判断是新手翻车重灾区
查询里最常用的是 WHERE。等值查询、范围查询、模糊查询、多条件逻辑组合,都是必须掌握的:
sql复制-- 等值查询
SELECT * FROM user WHERE name = '张三';
-- 范围查询
SELECT * FROM user WHERE age BETWEEN 20 AND 30;
-- IN 列表查询
SELECT * FROM user WHERE city IN ('北京', '上海', '广州');
-- 模糊查询
SELECT * FROM user WHERE name LIKE '张%';
-- 逻辑组合
SELECT * FROM user
WHERE age >= 18 AND city = '北京'
OR status = 1;
注意:AND 优先级高于 OR。如果上面的 SQL 想表达的是“年龄大于等于18 且(北京 或 状态为1)”,那必须加括号:
sql复制SELECT * FROM user
WHERE age >= 18
AND (city = '北京' OR status = 1);
不加括号是很多初级工程师写坏线上查询的经典原因之一。多条件组合时,能用括号明确逻辑就不要靠记忆去赌优先级。
还有一个 NULL 的坑。NULL 不是 0,也不是空字符串,它表示“未知值”。与 NULL 做比较不能直接用 = 或 !=。以下条件永远查不出东西:
sql复制-- 错误示范
SELECT * FROM user WHERE email = NULL;
正确写法是:
sql复制SELECT * FROM user WHERE email IS NULL;
SELECT * FROM user WHERE email IS NOT NULL;
只要记录里 email 是 NULL,你用 = NULL 去查,结果就是空。这一点我在面试时经常拿来当考察点,实际开发里也特别容易遇到。
3.3 ORDER BY 排序和 LIMIT 分页,以及容易踩的坑
排序用 ORDER BY,默认是升序 ASC,降序要写 DESC:
sql复制SELECT name, age FROM user
WHERE age > 18
ORDER BY age DESC, id ASC
LIMIT 10;
很多人会忽略:如果排序字段有重复值,建议再加一个唯一字段作次级排序,例如上面用 age DESC 后,还加了 id ASC。否则分页查询时,可能出现同一行数据在不同页里重复出现或漏掉的情况。原因很简单:只按一个非唯一字段排序时,MySQL 无法保证相同值之间的返回顺序稳定。
LIMIT 分页的语法也容易混淆。常见的两种写法:
sql复制LIMIT 10; -- 返回前10行
LIMIT 20, 10; -- 跳过20行,返回接下来的10行
第二种写法第一个数字是偏移量,第二个数字是行数。很多人会写成 LIMIT 10, 20,想表达“每页20行,取第2页”,结果取成了“跳过10行取20行”,数据就不对了。建议在代码中直接用 LIMIT offset, pageSize 的格式,并先用计算好的 offset,不要弄反。
3.4 聚合统计:GROUP BY 和 HAVING 的分工
当你想统计“每个城市有多少人”,或者“每个分类的平均价格”,就要用到聚合函数和分组了。
sql复制SELECT city, COUNT(*) AS user_count
FROM user
GROUP BY city;
聚合函数常见有:
- COUNT(*):行数
- SUM(列):求和
- AVG(列):平均值
- MAX(列)、MIN(列):最大最小值
GROUP BY 之后如果要过滤分组,不能用 WHERE,而要用 HAVING:
sql复制SELECT city, COUNT(*) AS user_count
FROM user
GROUP BY city
HAVING COUNT(*) >= 100;
背后的原则是:WHERE 是在分组之前对原始行进行过滤,HAVING 是在分组之后对聚合结果过滤。当年我一度认为两者差不多,直到写了一条带聚合条件的 SQL 把 WHERE 和 HAVING 混用,查出来的分组结果里包含了不该出现的分组,才彻底理解这两者的执行顺序差异。
4. 修改数据 UPDATE 与删除 DELETE:把“事故高发区”讲透
4.1 UPDATE 的基础语法,以及忘写 WHERE 的代价
UPDATE 的语法很直白:
sql复制UPDATE user
SET age = 30
WHERE id = 1;
它是“定位到符合 WHERE 条件的行,然后把 SET 指定的字段改成新值”。但很多人第一次写的时候会漏掉 WHERE,或者 WHERE 条件写得太宽,导致全表更新。而且 MySQL 默认在客户端里执行不带 WHERE 的 UPDATE 不一定会拦截,很多图形化工具也只是弹个提示,你一确认,整张表的数据就改了。
所以我给新手定的铁律是:任何 UPDATE 和 DELETE 执行之前,先把 WHERE 条件单独拿出来跑一遍 SELECT。比如你要执行上面的 UPDATE,先跑:
sql复制SELECT id, age FROM user WHERE id = 1;
确认你选中的就是你想改的那一行,再执行 UPDATE。这不是速度问题,而是保命问题。
4.2 DELETE 与 TRUNCATE 的区别,以及软删除的思路
DELETE 的基础语法:
sql复制DELETE FROM user WHERE id = 1;
关于删除,初学者最容易混淆的是“DELETE、TRUNCATE、DROP”三者区别:
| 操作 | 类型 | 是否可回滚 | 是否重置自增 | 说明 |
|---|---|---|---|---|
| DELETE | DML | 事务内可以回滚 | 不重置自增 | 删行数据,可加 WHERE |
| TRUNCATE | DDL | 通常不可回滚(各版本有差异) | 重置自增 | 清空整张表 |
| DROP | DDL | 通常不可回滚 | 删除表 | 直接连表结构一起删 |
如果不是真的要把整张表清空并重置自增,日常开发中主要用 DELETE。但 DELETE 也有限制:比如删除一个大表里的历史数据时,如果一次 DELETE 删除百万行,会锁很多行、产生大量 binlog,甚至拖垮主从。实际项目里更多会分批删除:
sql复制DELETE FROM operation_log
WHERE create_time < '2024-01-01'
LIMIT 1000;
循环执行这条语句,直到影响行数为 0。这样每次只删除少量数据,避免长事务和锁竞争。
另一个更强推荐的设计思路是“软删除”。就是在表里加一个 deleted 字段(0 表示正常,1 表示已删除)。用户点“删除”时,执行的不是 DELETE,而是 UPDATE:
sql复制UPDATE user
SET deleted = 1
WHERE id = 1;
查询时统一加条件 AND deleted = 0。这样做有几个好处:数据还能恢复、保留业务链路的审计信息、不会因为物理删除而外键断裂。我现在做业务表设计时,十有八九都会加软删除字段。
4.3 用事务给 UPDATE / DELETE 上一道保险
说到修改和删除,必须提事务。事务是保证多条 SQL 要么全部成功、要么全部失败的一种“回滚保险丝”。MySQL 中 InnoDB 引擎支持事务,最常用写法是:
sql复制START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
执行 START TRANSACTION 后,后面的增删改查就处在同一个事务中。如果中途发现第二步写错或影响行数不对,可以执行 ROLLBACK; 回滚到事务开始之前的状态,数据库数据不被更改。
有一次我帮同事排查线上问题,他手动执行了一条 UPDATE,把整张表的会员等级都改错了。好在他是先开了 START TRANSACTION,执行后再 SELECT 检查,发现影响行数异常,立刻 ROLLBACK,几秒钟就恢复了。如果直接执行并自动提交,那就得去找备份恢复了。
所以我的习惯是:手动修改生产数据,一律先开事务,改完立刻查证,查完再提交。
4.4 UPDATE 中 JOIN 子查询:改关联表数据时,先想清楚逻辑
实际业务里你还会遇到这种需求:把订单表里状态为 1 的用户,在用户表里的等级提升到 2。如果只用单表 UPDATE,需要在应用层先查出 id 再逐条 UPDATE,太笨重。MySQL 支持多表 UPDATE:
sql复制UPDATE user u
JOIN orders o ON u.id = o.user_id
SET u.level = 2
WHERE o.status = 1;
这种方式能大幅提升效率,但前提是 JOIN 条件必须准确,否则会互相影响。我建议在执行前先写一条 SELECT,把要更新的行数和范围确认一遍:
sql复制SELECT DISTINCT u.id, u.level
FROM user u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 1;
确认无误后,再把 SELECT 换成 UPDATE。记住了:能先查清楚就不要拿线上数据当试验品。
5. 那些会让“简单增删改查”翻车的边界情况:字符集、SQL注入与索引意识
5.1 字符集问题:插入中文乱码,十有八九是字符集没统一
一个非常常见的现象是:用客户端工具直接 INSERT 中文没问题,但通过程序写进去就成了乱码,或者表里显示乱码、网页上显示问号。这个问题经常不在 CRUD 语句本身,而在字符集。
MySQL 8.0 默认字符集一般是 utf8mb4,可以支持绝大部分文字和 emoji。如果你在建表时用了 DEFAULT CHARSET=utf8(注意这个不是真正的 utf8mb4,只支持部分字符),插入生僻字或 emoji 就可能报错或丢失。整条链路里数据库连接也要统一字符集。
建表时建议明确指定:
sql复制CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
JDBC 连接串里也可以加上 characterEncoding=utf8 之类的参数。写增删改查时,如果发现中文显示异常,先不要怀疑 SQL 语法,优先排查库、表、连接三层的字符集是否一致。
5.2 SQL注入:别把查询条件直接拼进字符串
在讲 SELECT、UPDATE、DELETE 的时候,如果你是把用户输入直接拼到 SQL 里,比如:
python复制sql = "SELECT * FROM user WHERE name = '" + user_input + "'"
一旦用户输入 ' OR '1'='1,拼出来就是:
sql复制SELECT * FROM user WHERE name = '' OR '1'='1';
这个条件恒成立,表里的数据可能全被查出来。更严重的,如果在 DELETE 或 UPDATE 上拼接,攻击者还可能造成全表数据删除。这类问题本质是“数据和 SQL 代码没有分开”。
正确做法是使用参数化查询。以 Python 的 pymysql 为例:
python复制cursor.execute("SELECT * FROM user WHERE name = %s", (user_name,))
以 Java 的 JDBC 为例,用 PreparedStatement:
java复制String sql = "SELECT * FROM user WHERE name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, userName);
ResultSet rs = ps.executeQuery();
说白了,写增删改查时,不要相信任何来自外部输入的数据,参数化是底线。
5.3 加个 EXPLAIN,看你的 UPDATE/DELETE 是不是全表扫描
有时候 CRUD 写得没问题,但跑起来特别慢。比如:
sql复制SELECT * FROM user
WHERE email = 'someone@example.com'
ORDER BY created_at DESC
LIMIT 10;
如果 email 字段上没有索引,MySQL 大概率要全表扫描,一行一行比对,数据量大了自然慢。这时可以用 EXPLAIN 看一下执行计划:
sql复制EXPLAIN SELECT * FROM user
WHERE email = 'someone@example.com'
ORDER BY created_at DESC
LIMIT 10;
看到 type 列为 ALL,通常意味着全表扫描;如果显示 ref 或 eq_ref,说明用到了索引,情况好不少。对经常作为查询条件、更新条件的列建立索引,能显著提升增删改查中“查”的性能。
但要注意:索引不是越多越好。索引会占用空间,也会拖慢 INSERT、UPDATE 和 DELETE 的性能,因为每次修改数据至少会同步更新相关索引。增删改查是一个整体,不是只优化某一个操作。
5.4 UPDATE 里的“字段 = 字段 + 5”:别把数值当成字符串拼
热搜词里有个“mysql中int+5”,正好是 UPDATE 里非常典型的场景。比如用户做积分操作:
sql复制UPDATE user
SET points = points + 5
WHERE id = 1;
这个写法的含义是“把当前 points 值加 5 后再写回去”。它天然适合并发场景吗?其实它也有并发覆盖风险。如果两个连接同时读到 points=100,各自加 5,最后结果可能是 105 而不是 110。更稳妥的方法是加上条件约束,或使用事务配合行锁。
不过重点是:很多人会把 points + 5 写成 points = 'points + 5',把表达式当成字符串存进去。这样修完后 points 就变成了字符串 points + 5,这条数据基本废了。数值运算一定不要加引号,否则就是给自己埋雷。
6. 一个最小闭环:用一组增删改查完成用户管理的核心数据操作
6.1 从建表到插入:模拟用户注册
前面讲了大量细节,现在我们把它们串起来,做一个用户管理的最小闭环。假设业务场景是“用户注册、查列表、改昵称、删除用户”。第一步,建一张用户表:
sql复制CREATE TABLE user (
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
name VARCHAR(50) NOT NULL COMMENT '昵称',
email VARCHAR(100) NOT NULL COMMENT '邮箱',
age INT UNSIGNED DEFAULT NULL COMMENT '年龄',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0正常,1禁用',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
这里用了唯一索引 uk_email,目的是禁止两个用户共用同一个邮箱。注册用户时,就是执行 INSERT:
sql复制INSERT INTO user (name, email, age)
VALUES ('张三', 'zhangsan@example.com', 25);
如果邮箱已经存在,MySQL 会报主键/唯一键冲突错误。在业务里捕获这个错误,然后提示“邮箱已被注册”,比注册前先 SELECT 一遍再插入要更可靠。因为并发注册时,两次 SELECT 并不能保证中间没有其他事务插入同名邮箱,唯一索引才是真正的防重门槛。
6.2 SELECT 验证注册结果,以及实现列表分页查询
注册完成之后,我们可以查询看看:
sql复制SELECT id, name, email, age, status, created_at
FROM user
WHERE email = 'zhangsan@example.com';
列表页通常需要返回 id、昵称、邮箱和注册时间,并且按创建时间倒序排列,加上分页:
sql复制SELECT id, name, email, age, status, created_at
FROM user
WHERE status = 0
ORDER BY created_at DESC
LIMIT 0, 20;
如果想做模糊搜索,可以在 WHERE 里加:
sql复制AND name LIKE CONCAT('%', '张', '%')
注意不要直接写 LIKE '%张%' 以外的字符串拼接时产生 SQL 注入问题。实际项目里,参数化查询是最优先选择。
6.3 UPDATE 修改用户资料,并在修改前做一次身份确认
用户要改昵称,前端传来新的昵称和用户 id:
sql复制UPDATE user
SET name = '张三丰'
WHERE id = 1;
在代码层面,更安全的做法是先传原始版本号或当前数据,再执行“乐观锁更新”。所谓乐观锁,就是给表加一个 version 字段,每次更新把版本号带上,只有版本号一致才允许更新:
sql复制UPDATE user
SET name = '张三丰', version = version + 1
WHERE id = 1 AND version = 0;
影响行数为 1,说明更新成功;影响行数为 0,说明版本已经变化,其他人先改过了,需要重新读取再提交。这个模式很好地在并发场景下保护了数据一致性,也是增删改查从“能用”进阶到“可靠”的一步。
如果要禁用用户登录,可以用 UPDATE 而不是 DELETE:
sql复制UPDATE user
SET status = 1
WHERE id = 1;
6.4 删除用户:物理删除、软删除与最终选择
如果产品要求彻底删除用户,就执行:
sql复制DELETE FROM user WHERE id = 1;
如果做软删除,我通常会加 deleted 字段,执行的是:
sql复制ALTER TABLE user ADD COLUMN deleted TINYINT NOT NULL DEFAULT 0;
UPDATE user
SET deleted = 1
WHERE id = 1;
查询时所有业务 SQL 都要加上 deleted = 0:
sql复制SELECT id, name, email
FROM user
WHERE deleted = 0
AND email = 'zhangsan@example.com';
改完这张 user 表之后,你会发现增删改查看似简单,但每一类操作都牵扯到表结构设计、约束、事务、索引、并发和安全性。把这些考虑进去,你写的就不是“能跑的 SQL”,而是“能扛业务、能上生产的 SQL”。
最后分享一个我自己的实操习惯:我很少直接在命令行里执行生产环境的 UPDATE 或 DELETE,即便执行也一定会先开事务、后 SELECT 验证、再 UPDATE、再 SELECT 验证、最后 COMMIT;写程序里的 SQL 时,也尽量用参数化语句而不是字符串拼接。保持这个习惯,能让你少熬很多个深夜。CRUD 不难,真正难的是每一次操作前多想一秒钟它们的副作用。
