ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MySQL增删改查全攻略:从基础语法到事务与索引优化

2026/10/1 19:27:47 拓冰建站 浏览量
MySQL增删改查全攻略:从基础语法到事务与索引优化 增删改查这四个字凡是碰过后端开发的人都不会陌生。但你有没有发现越“基础”的东西越容易出问题有人写 UPDATE 忘了带 WHERE 把整张表数据全改了有人删数据删到一半发现没备份也有不少初学者分不清 TRUNCATE 和 DELETE 的区别更别提那些在 WHERE 条件里用错 NULL 判断的经典场面。我在这几年接触过的项目和带过的同学里见过太多这样的情况——严格来说大家不是不会写 SQL而是没把“增删改查”当成一个完整的体系去理解只记住了语法没有形成操作习惯和排查思路。这篇内容我就围绕 MySQL 的增删改查展开从环境准备一直讲到业务场景落地把该注意的细节和常见的坑都梳理一遍。只要你正在用 MySQL不管是刚入门还是写了一段时间想查漏补缺这篇文章都值得通读一遍。1. 增删改查在业务系统里到底撑起了什么很多人把增删改查理解成“数据库四种语句”这个说法没什么毛病但它低估了这套操作的价值。你随便打开一个 App注册账号是在做 INSERT刷新首页是在做 SELECT修改个人信息是在做 UPDATE注销账号是在做 DELETE——后端接口哪怕写得再花里胡哨落到数据库层面最终都是以这四种操作收尾的。这就意味着CRUD 不只是语法它其实是一个业务系统最底层的表达方式。1.1 从接口请求到 SQL 的数据流转链路我习惯用一条链路去理解数据的流转用户在前端页面触发操作请求发到后端后端 Service 层做逻辑校验和组装最后通过持久层框架比如 MyBatis、MyBatis-Plus 或 JPA将逻辑翻译成一条 SQL 语句交给 MySQL 执行。在这个链路里数据库的增删改查处于末端却是整个系统的“定海神针”——前面写得再严谨SQL 写错一步数据就错了数据错了对用户来说就是功能 bug对公司来说可能就是资损或信任危机。举个具体的例子电商下单流程里用户下单要往订单表插一条记录INSERT扣减库存要更新商品表的 stock 字段UPDATE查看订单列表是查订单表SELECT用户申请退款后关闭订单是修改订单状态UPDATE而不是 DELETE因为订单记录需要留痕。你会发现哪怕是最简单的商城项目CRUD 也会交织成一套组合逻辑而不是孤立地使用某一种语句。1.2 为什么说 CRUD 的性能边界取决于索引与约束有些开发者的 CRUD 熟练到可以闭着眼睛写但数据库一卡就说“服务器问题”。实际上大部分查询慢的根源都出在 SELECT 上——查了不该查的全表或者 WHERE 条件里的字段没有索引导致 MySQL 只能一行一行扫。增删改查本身没有魔法决定它们快不快的关键因素是表结构设计里的索引和约束。索引的作用可以类比成书的目录没有目录的书你想找某个知识点只能一页页翻有了目录直接翻到对应页码。MySQL 默认的 InnoDB 引擎使用 B 树索引能快速定位数据行。你的 SELECT、UPDATE、DELETE 只要 WHERE 条件命中索引列MySQL 就不会做全表扫描性能提升往往是几倍甚至几十倍。约束则是保证数据质量的主键约束防止重复记录唯一约束防止业务上的重复值比如同一个手机号只能注册一次外键约束实际工程里用得不多更多靠应用层保证维护表间关系的完整性。所以学习增删改查一定不要把眼光停留在“会写语句”这个层面。今天写的每一条 SELECT、UPDATE、DELETE都应该顺带想一想这条语句的 WHERE 条件有没有走索引这个字段该不该加约束这样写会不会破坏数据完整性2. 动手前的准备MySQL 版本、建表思路与客户端选择不少人在学习 CRUD 时直接拿着 SQL 就往考试系统或者在线练习平台上敲这种方式能学会语法但学不会“真实工程”里需要的操作手感。我建议还是在自己机器上装一个本地 MySQL 环境自己建库建表自己往里灌数据这样对数据变化才有直观的感知。2.1 版本选择和安装后的基础验证MySQL 目前主流稳定版本是 8.0 系列5.7 已经慢慢退出历史舞台社区版完全够用免费且功能齐全。安装完成之后第一步建议先在命令行里验证服务是否正常运行mysql -u root -p输入密码后如果能进入 MySQL 交互终端就说明服务没问题、账号没问题。接着可以查看版本信息SELECT VERSION();顺手验证一下字符集设置避免后面建表时出现中文乱码SHOW VARIABLES LIKE character_set%;如果是 8.0 版本默认字符集已经是 utf8mb4这个字符集支持完整的 Unicode 包括 emoji是目前最推荐的。如果你还在用 5.7建库时可以明确指定CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;2.2 用一张用户表把字段类型一次讲透建表是 CRUD 的前提字段类型选得合理后续的一切操作都会顺畅。我用一张常见的用户表来演示CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(64) NOT NULL COMMENT 用户名, phone VARCHAR(20) NOT NULL COMMENT 手机号, email VARCHAR(128) DEFAULT NULL COMMENT 邮箱, age TINYINT UNSIGNED DEFAULT 0 COMMENT 年龄, balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 余额, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表里每个字段的选择都值得解释一下id主键用 INT UNSIGNED足够存储 42 亿条记录AUTO_INCREMENT 让数据库自动生成唯一值。usernameVARCHAR(64)用户名长度有限没必要用 TEXT。phoneVARCHAR(20)注意手机号虽然像数字但一定不要用 INT/BIGINT 存因为手机号可能涉及前导零、区号、国际号码等格式问题用字符串存最稳妥。ageTINYINT UNSIGNED取值范围 0~255年龄足够用了省空间。balanceDECIMAL(10,2)金钱类字段绝对不能用 FLOAT/DOUBLE二进制浮点数有精度损失DECIMAL 是精确的定点数。statusTINYINT 做软删除/状态标记后面讲删除操作时会提到这个字段的价值。created_at / updated_atDATETIME 带默认值updated_at 用 ON UPDATE 自动维护这样每次 UPDATE 时时间戳自动更新省掉应用层手写。UNIQUE KEYphone 加了唯一索引业务上手机号不该重复直接在数据库层面挡住重复数据。2.3 客户端工具选哪个顺手命令行当然是基本功但日常开发中我更喜欢用图形化工具查看数据变化。几款主流工具我的使用感受如下工具适合场景备注MySQL 命令行服务器排查、脚本执行任何环境都有最稳Navicat日常开发、表结构设计、数据导入导出功能全界面友好原生支持多个数据库DBeaver开源免费、需要连多种数据库对 MySQL 支持很好插件机制强大DataGrip专业 SQL 开发JetBrains 出品适合重度写 SQL 的开发者我的建议是命令行必须会因为线上环境大多没有图形界面图形工具选一个顺手的长期用下去效率差距很大。比如 Navicat 里建表可以勾选字段、设置默认值比敲 SQL 更直观DBeaver 则能在你写复杂查询时实时格式化、提示语法错误。3. 新增数据INSERT 的三种姿势与主键冲突处理INSERT 是所有写操作里最“温和”的一个因为它的风险相对可控——最坏的情况是插了一条错误数据删掉就行。但即便如此INSERT 也有很多讲究。3.1 单行插入的写法与字段选择最标准的写法是指定字段和值INSERT INTO user (username, phone, email, age, balance, status) VALUES (张三, 13800138000, zhangsanexample.com, 25, 100.00, 1);这样写的好处是表结构调整时不容易出错字段和值一一对应。还有一种写法是省略字段名INSERT INTO user VALUES (1, 李四, 13900139000, NULL, 30, 200.00, 1, NOW(), NOW());这种写法要求 VALUES 里的值必须和表结构的字段顺序完全一致一旦表结构发生变动比如新增了字段这条 SQL 就会报错或者插入错位数据。所以我的建议是任何时候都写字段名不要图省事省略这一点在开发规范里也可以直接定死。3.2 批量插入为什么效率更高业务开发中经常遇到循环插入的场景比如用户批量导入、订单明细批量落库。很多新手会写出这样的伪代码for (User user : userList) { // 每次循环执行一条 INSERT INTO user ... }每条 INSERT 单独执行MySQL 要反复做语法解析、权限校验、事务提交网络开销和解析开销都很大。如果改成一条 SQL 搞定INSERT INTO user (username, phone, email, age, balance, status) VALUES (张三, 13800138000, aexample.com, 25, 100.00, 1), (李四, 13900139000, bexample.com, 30, 200.00, 1), (王五, 13700137000, cexample.com, 28, 150.00, 1);执行效率差距非常明显。我自己做过简单对比插入 1000 条数据循环单条插入耗时大约在 1.5 秒左右受网络影响而一条 SQL 批量插入只需要几十毫秒。原因很简单批量插入只做一次语句解析数据按照段提交一次性写多个 row的方式写入日志落盘次数也少得多。在 MyBatis-Plus 里对应就是saveBatch方法底层帮你拼成了批量 SQL这也是为什么持久层框架会提供批量接口的原因。3.3 主键冲突的三种处理思路插入数据的时候最麻烦的就是撞上已经存在的唯一键或主键。比如用户注册时手机号已经存在就会触发唯一约束冲突报错。一般有三种处理方式第一种是插入前先查一遍SELECT COUNT(*) FROM user WHERE phone 13800138000;存在就提示“手机号已注册”不存在再 INSERT。这种方式有并发风险——两个请求同时查发现都不存在然后同时插入还是会冲突所以需要配合唯一索引兜底。第二种是 INSERT IGNOREINSERT IGNORE INTO user (username, phone, email) VALUES (张三, 13800138000, aexample.com);如果发生了唯一键冲突MySQL 不会报错而是直接忽略这条插入影响行数为 0。适合“有就跳过没有就插入”的场景。第三种是 ON DUPLICATE KEY UPDATEINSERT INTO user (username, phone, email, balance) VALUES (张三, 13800138000, aexample.com, 100.00) ON DUPLICATE KEY UPDATE balance VALUES(balance);这条语句的意思是如果插入时遇到唯一键冲突不报错而是改为执行后面的 UPDATE 逻辑。它非常适合“存在就更新不存在就插入”的 upsert 场景比如同步数据、统计计数等。需要留意的一个细节是在 MySQL 8.0.20 之后VALUES()函数在 ON DUPLICATE KEY UPDATE 中已被标记为废弃官方建议使用别名语法INSERT INTO user (username, phone, email, balance) VALUES (张三, 13800138000, aexample.com, 100.00) AS new ON DUPLICATE KEY UPDATE balance new.balance;4. 查询数据SELECT 的基本功比你想的要多得多SELECT 是整个 CRUD 里最常用、也最值得花时间学的一类操作。很多人写 SELECT 只停留在SELECT * FROM 表名这个层面一旦遇到统计、分组、分页、条件过滤就慌了。我把 SELECT 拆成几个关键点一个个说。4.1 一条完整 SELECT 的执行顺序先记住一条 SELECT 的标准书写结构SELECT 字段 FROM 表名 WHERE 条件 GROUP BY 分组字段 HAVING 分组后的过滤条件 ORDER BY 排序字段 LIMIT 偏移量, 行数书写顺序和执行顺序不一样。MySQL 实际执行顺序是FROM确定查哪张表WHERE过滤行级条件GROUP BY分组HAVING过滤分组后的条件SELECT投影需要的字段ORDER BY排序LIMIT分页理解这个执行顺序对排错很重要。比如你想筛选“余额大于 100 的用户”在 WHERE 里写balance 100没问题但如果你想筛选“分组后平均余额大于 100 的分组”就必须用 HAVING因为这个时候AVG(balance)是在 GROUP BY 之后才计算出来的WHERE 执行时还没有这个值。再比如SELECT username, COUNT(*) FROM user WHERE status 1 GROUP BY username HAVING COUNT(*) 1这条语句先去查 status1 的用户再按 username 分组最后只保留记录数大于 1 的分组。每一步都对应执行顺序中的一环。4.2 WHERE 条件里的三个经典坑第一个坑是 NULL 判断。新手常写WHERE email NULL但这是查不出任何数据的。NULL 表示“未知”不能用等号或不等号判断必须用IS NULL或IS NOT NULLSELECT * FROM user WHERE email IS NULL;第二个坑是字符串和数字的隐式转换。如果 phone 字段是 VARCHAR 类型你在 WHERE 里写phone 13800138000不带引号MySQL 虽然能查出来但会隐式地把字符串转成数字这会导致 phone 字段上的索引失效查询变慢。正确写法SELECT * FROM user WHERE phone 13800138000;第三个坑是 LIKE 模糊查询的索引失效。LIKE %abc这种以通配符开头的写法因为无法确定匹配的起始位置MySQL 不会走索引。LIKE abc%则可以利用索引做范围扫描。业务上如果经常需要后缀匹配建议考虑存储反转字符串再加前缀索引等方式。4.3 聚合查询COUNT、SUM、AVG、MAX、MIN 的组合用法聚合函数把多行数据计算成一个结果值配合 GROUP BY 能解决大量统计需求。举几个典型场景统计用户总数、活跃用户数SELECT COUNT(*) AS total, SUM(status 1) AS active_count FROM user;这里SUM(status 1)利用了 MySQL 中布尔表达式返回 1/0 的特性非常实用。统计每个状态的用户数并按数量降序SELECT status, COUNT(*) AS cnt FROM user GROUP BY status ORDER BY cnt DESC;按手机号前缀统计用户分布SELECT LEFT(phone, 3) AS prefix, COUNT(*) AS cnt FROM user GROUP BY prefix;聚合查询的坑主要集中在 GROUP BY 后面 SELECT 的字段。MySQL 的 ONLY_FULL_GROUP_BY 模式下GROUP BY 后出现的字段必须要么在 GROUP BY 里要么被聚合函数包裹否则直接报错。这是 SQL 规范要求不需要绕过按规范写就行。4.4 排序和分页LIMIT 的正确打开方式ORDER BY 默认是升序 ASC降序要写 DESCSELECT * FROM user ORDER BY created_at DESC;分页常见写法SELECT * FROM user ORDER BY id LIMIT 0, 20; -- 第1页 SELECT * FROM user ORDER BY id LIMIT 20, 20; -- 第2页LIMIT 后面的第一个数字是偏移量第二个是行数。LIMIT 20, 20 意味着跳过前 20 条取 20 条。另一种等价写法是LIMIT 20 OFFSET 20这套语法的可读性更好PostgreSQL、MySQL 都支持。还有个常见问题深分页。当 LIMIT 偏移量特别大时比如 LIMIT 1000000, 20MySQL 会先扫描前 1000020 行再丢弃前 1000000 行性能会直线下降。实际业务里可以用“延迟关联”或“基于游标”的分页方式优化典型方案是记住上一页最后一条记录的 idSELECT * FROM user WHERE id 1000000 ORDER BY id LIMIT 20;这种条件分页能充分利用主键索引性能远好于深偏移量分页。如果你的业务数据量大且列表页经常往后翻强烈建议改成这种写法。5. 修改数据UPDATE 的高危场景与安全操作习惯UPDATE 是 CRUD 里危险系数名列前茅的操作。为什么危险因为它的条件写错了影响的不只是一行而是所有符合条件的数据。最经典的悲剧就是执行了UPDATE user SET balance 0;没有 WHERE 条件整张表的余额全被清零了。这种事故只要发生过一次团队就要花大力气做数据恢复还不一定恢复得干净。5.1 UPDATE 的基本语法与 WHERE 的原则标准写法UPDATE user SET balance 150.00, updated_at NOW() WHERE id 1;SET 后面可以跟多个字段的赋值WHERE 指定要更新的行。基本原则是WHERE 条件里一定要有能锁定目标行的字段优先用主键 ID如果是批量更新条件里必须包含一个或一组能唯一定位目标集合的字段。比如给所有状态为 1 的用户增加 10 元余额UPDATE user SET balance balance 10 WHERE status 1;你会发现 UPDATE 和 SELECT 共享同一套 WHERE 写法所以在写 UPDATE 之前我强烈建议你先用同样的 WHERE 条件跑一遍 SELECT看看会命中哪些行SELECT id, username, balance FROM user WHERE status 1;把要更新的数据先看一遍再执行 UPDATE这个习惯能帮你避开一多半的误操作。5.2 如何安全执行一条“高危”UPDATE我给自己定的操作规范是涉及批量更新、金额变更、状态变更的 UPDATE全部按下面流程走用相同 WHERE 条件执行 SELECT确认影响行数和具体数据。在事务中执行 UPDATE先不提交事务。用 SELECT 验证更新结果是否符合预期。确认无误后提交事务发现有误直接 ROLLBACK 回滚。对应到命令行里就是START TRANSACTION; UPDATE user SET balance balance - 10 WHERE id 5; -- 这里手动查看一下数据确认没问题再执行 COMMIT; COMMIT; -- 如果发现扣错了执行 ROLLBACK; ROLLBACK;事务是 InnoDB 最重要的特性之一。事务里所有操作要么全部成功要么全部失败不会出现执行了一半的中间状态。对于 UPDATE 这种高影响操作在事务里跑一遍再提交是最保险的策略。代码里对应就是 Spring 的Transactional注解MySQL 底层通过 undo log 来支持回滚通过 redo log 保证持久性。5.3 批量修改与跨表更新业务中经常需要按条件批量修改数据。比如把过去 30 天没有登录的用户状态改成“沉默”UPDATE user SET status 3 WHERE last_login_at DATE_SUB(NOW(), INTERVAL 30 DAY);DATE_SUB 用来做日期运算这种条件写法在运营活动、用户分层场景里非常常见。跨表更新的场景比如根据订单表的订单总额去更新客户表的消费等级UPDATE customer c JOIN ( SELECT customer_id, SUM(amount) AS total_amount FROM orders WHERE order_status paid GROUP BY customer_id ) o ON c.id o.customer_id SET c.total_spent o.total_amount;UPDATE JOIN 在 MySQL 中是合法语法注意 JOIN 的条件要能锁定唯一行否则会出现一行被多行更新覆盖的意外。我对这类写法的建议是能分成多条简单语句就不要搞得太复杂一条超长 SQL 维护起来很痛苦执行计划也不好控制。牵涉金额、状态同步的逻辑先 SELECT 出来代码里计算好再逐条 UPDATE很多时候比硬拼一条大 SQL 更安全、也更好排查问题。6. 删除数据DELETE、TRUNCATE 和逻辑删除到底怎么选DELETE 是 CRUD 里最需要敬畏的操作。删除不可怕可怕的是删完才发现数据还有用。我见过不止一次因为线上误 DELETE 导致的数据恢复行动过程极其痛苦。所以这一节我会把 DELETE 的多个层面都讲清楚。6.1 DELETE 基础语法与 DELETE 的真相最常见的删除是删除某一行DELETE FROM user WHERE id 1;很简短但值得展开说几个点。第一DELETE 不是直接抹掉物理磁盘上的数据。InnoDB 执行 DELETE 时实际上是把记录标记为“已删除”真正的磁盘空间会被延迟回收由后台 purge 线程处理。这也是为什么 DELETE 一批大记录之后表空间文件大小并没有立即变小。想真正释放空间需要执行 OPTIMIZE TABLE 或 ALTER TABLE 来重建表。第二DELETE 支持 LIMIT。这是很多人不知道的实用技巧。批量删除大量数据时一次性 DELETE 全部符合条件的数据会持有大量行锁事务日志也很大严重时会把数据库拖垮。正确做法是分批删除DELETE FROM user WHERE status 3 LIMIT 1000;每执行一次删除 1000 行循环执行直到受影响行数为 0。这样每条语句只占一小段锁时间对业务影响小得多。线上清理数据时我都是这么干的。6.2 TRUNCATE、DELETE、DROP 的差异三张表的对比来一次说透操作类型是否可回滚是否重置自增ID速度释放空间DELETE FROM 表DML事务内可回滚不重置慢逐行删不立即释放TRUNCATE TABLE 表DDL不可回滚重置为 1快重建表段释放重建表DROP TABLE 表DDL不可回滚表没了最快全部释放TRUNCATE 的本质是直接创建一个新的表结构并丢弃所有数据页所以速度极快、且自增 ID 重置。但正因为它是 DDL无法在事务里回滚一旦执行就基本没有后悔药。我的态度是TRUNCATE 只允许用在明确需要清空的临时表、测试表上任何带业务价值的表都别碰它。DROP 则连表结构一起干掉使用场景基本限定在“这个表确定永远不再使用”。6.3 为什么很多系统采用“逻辑删除”真正的工程实践中业务数据很少会被物理删除。用户的“删除订单”“删除评论”底层往往是执行了一个 UPDATE把某个标记字段改成“已删除”。这就是逻辑删除。比如我那张 user 表里的 status 字段可以约定 0 为正常、1 为删除。删除用户的时候执行UPDATE user SET status 1 WHERE id 5;以后所有查询默认都带WHERE status 0从用户视角看数据就像“消失”了一样。逻辑删除有几个实实在在的好处数据可追溯。误删了还能改回来运营数据也能做历史分析。不破坏关联数据。比如订单表中关联了用户物理删除用户会让关联记录悬空。避免频繁的大 DELETE 操作带来的性能问题。MyBatis-Plus 对逻辑删除有内置支持只需要在实体字段上加TableLogic注解框架会在你调用 deleteById 时自动转化成 UPDATE 语句查询时自动追加deleted0条件非常方便。它本质上就是用 UPDATE 替代了 DELETE这也是为什么我把逻辑删除放在这一节里讲——它不是一劳永逸的方案也有缺点比如所有查询都要带上状态条件数据量增长后还需要定期归档和清理但它符合绝大多数业务系统对数据安全性的要求。7. 把增删改查串起来一个商品管理系统的完整演练单项语法会了还要能把它们拼装到真实业务里。这一节我用一个简单的商品管理场景把 CRUD 完整走一遍。这个场景里没有复杂的框架就用 SQL 说话。7.1 场景设计与建库建表需求很简单一个后台要管理商品和库存商品可按名称搜索、按价格排序支持上下架删除商品走逻辑删除。先建商品表CREATE TABLE product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(128) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 库存, status TINYINT NOT NULL DEFAULT 1 COMMENT 上架状态1上架 0下架, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0正常 1删除, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;7.2 一次完整的增删改查组合新增商品INSERT INTO product (name, price, stock, status) VALUES (机械键盘, 299.00, 200, 1);批量初始化几件商品INSERT INTO product (name, price, stock, status) VALUES (无线鼠标, 89.00, 500, 1), (显示器, 1299.00, 50, 1), (USB-C 扩展坞, 159.00, 300, 1);查询商品列表按价格从低到高排序SELECT id, name, price, stock FROM product WHERE deleted 0 AND status 1 ORDER BY price ASC;搜索名称包含“键”的商品SELECT id, name, price, stock FROM product WHERE deleted 0 AND name LIKE %键%;修改某个商品的价格和库存UPDATE product SET price 279.00, stock 180 WHERE id 1;下架某件商品UPDATE product SET status 0 WHERE id 3;逻辑删除某件商品UPDATE product SET deleted 1 WHERE id 2;以上每一步都是前面讲过的语法落到了具体场景里你会发现这里没有任何“新东西”但组合起来的业务含义完全不同。这就是 CRUD 的实际使用状态——每条语句都不难难的是知道在什么业务节点该用哪一条、怎么保证它安全高效地执行。7.3 把多步操作包进事务里的正确姿势场景升级一下用户下单购买商品系统需要同时做两件事——往订单表插入一条订单记录、扣减商品的库存。这两步必须同时成功或同时失败——如果订单插入了但库存没有扣减会出现超卖如果库存扣了但订单没插入用户会莫名其妙少了钱。在命令行里体现START TRANSACTION; INSERT INTO order (user_id, product_id, quantity, amount) VALUES (1, 1, 2, 558.00); UPDATE product SET stock stock - 2 WHERE id 1 AND stock 2; COMMIT;这里有个细节UPDATE 的 WHERE 条件里带了stock 2并且扣减是通过stock stock - 2而不是先 SELECT 再在代码里算好了回填。这样做的意义是如果库存不够这条 UPDATE 影响行数为 0不会扣成负数同时利用行锁避免并发超卖。这是并发场景下非常经典的安全写法。如果你用的是代码框架事务边界一般放在 Service 方法上上面两条 SQL 分别对应两个 Mapper 方法逻辑完全一致。事务能保证要么订单和库存一起变化要么什么都不变不会出现中间态的脏数据。写在最后的一些实际操作心得增删改查看着简单但它背后承载的是一个系统最核心的数据安全底线。我在真正写过业务系统、处理过线上故障之后才明白CRUD 的学习重点从来不仅仅是语法——比语法更重要的是操作习惯UPDATE 必带 WHERE删除前先 SELECT 确认批量操作先开事务线上数据动手前先备份。如果你能把这条操作习惯刻进肌肉记忆里增删改查这门基本功就算真正过关了。建议你拿这张商品表从头到尾执行一遍再把文中的坑逐个踩一遍感受一下报错信息和执行结果这东西只看文章是不行的必须亲手敲一遍才有体感。