☰
UPDATE与DELETE底层原理、性能陷阱与安全实践
2026/10/8 20:14:21 网站建设 项目流程

UPDATE 和 DELETE 大概是所有后端开发最早学会、也最容易写“出事”的两条 SQL。SELECT 写错了顶多结果不对,UPDATE 和 DELETE 写错了,轻则改错数据,重则锁表拖垮整个库。网上关于单表语法、加 WHERE 条件的文章已经够多了,但这俩语句真正难的地方在于:它们内部怎么执行、和 JOIN 结合时性能为什么崩、为什么用错了就造成主从延迟、以及它们和 SQL 注入之间的微妙关系。这篇文章不打算重复教科书内容,我直接结合这几年在项目里踩过的坑,把 UPDATE 和 DELETE 从语法到原理、再到性能和安全,完整地梳理一遍。

这两条语句,表面上只是一次“数据修改”,但放到数据库引擎里看,它其实是一次“读”加一次“写”的组合操作。如果你不理解这层组合逻辑,就永远想不明白为什么一条简单的 UPDATE 也能把数据库 CPU 打满,也不知道为什么 DELETE 明明删了几万行,磁盘文件却一点没变小。这篇文章适合所有写过 SQL 的开发者,不管你是刚入行的新人,写了好几年业务代码的老手,还是正在面试前突击 SQL 知识的人,都能从中拿到一些直接能用的东西。

1. UPDATE 和 DELETE 并不只是“改一行,删一行”

很多开发者对 UPDATE 和 DELETE 的理解停留在语法层面:“UPDATE 表 SET 字段=值 WHERE id=1”,仅此而已。但实际在 InnoDB 引擎里,这两条语句执行时要走的路径比 SELECT 复杂得多。

1.1 一条 UPDATE 背后的“读-改-写”三步

我举个例子,有一条最简单的更新语句:

UPDATE users SET age = age + 1 WHERE id = 100086;

这条语句在执行时,InnoDB 并不是直接找到 id=100086 这一行,然后把 age 改成新值。它实际做的是:

  1. 定位行:通过主键索引 B+ Tree 定位到 id=100086 这条记录。定位的过程和 SELECT 完全一样,走索引、拿回记录。
  2. 当前读:注意,这里的读取是“当前读”,不是普通 SELECT 的“快照读”。当前读会读取这行记录的最新已提交版本,并且要加上排他锁(X Lock)。这就是为什么 UPDATE 和 SELECT 的锁行为完全不同。
  3. 写入新值:在内存中修改这行数据。但这里的关键是,修改之前要把旧值(age 修改前的值)写入 Undo Log,这样才能保证事务回滚时能恢复原状。

也就是说,一条 UPDATE 在实际执行时,相当于“SELECT ... FOR UPDATE”加一次内存写入。这也是为什么很多时候性能瓶颈不在“写”,而在“读”——如果 WHERE 条件没走索引,那么引擎就要一行一行地扫、一行一行地加锁。扫描多少行,就要锁多少行,这才是 UPDATE 慢和无故锁表的根源。

1.2 二级索引在实际更新中会被“延迟处理”

这一节可能是很多 DBA 都不一定主动讲的细节。如果 users 表在 age 字段上建立了二级索引,那么上面那条 UPDATE 不仅改主键索引里的数据,还要改二级索引。但 InnoDB 对二级索引的更新并不总是同步完成——它会把“修改二级索引”这个动作标记为需要回表处理,真正的索引维护往往发生在后台,由 Purge 线程异步完成。

这就是为什么有时你 UPDATE 完一条数据,立刻用二级索引字段去查,会发现数据“没变”。其实行数据已经变了,但二级索引的变更还没落定,或者要等事务提交后可见性判断才能感知。这个现象在低版本 MySQL 或长事务场景下特别明显。理解了这一点,你就能明白:给频繁 UPDATE 的字段建索引,不一定能加速 UPDATE,反而会让写放大更严重。索引不是越多越好,这条原则对 UPDATE 频繁的表尤其适用。

1.3 DELETE 也不是真物理删除

至于 DELETE,很多人更想不到的是:DELETE 操作在 InnoDB 中默认并不是物理删除磁盘上的数据行。它做的是标记删除——在被删除的记录头里打上一个“已删除”的标记,然后放进 Purge 线程的待清理队列。

所以你会看到,一条 DELETE 把 100 万行垃圾数据删掉之后,表的.ibd文件大小几乎没变化。因为那部分空间变成了“空洞”,等后续新数据插入时复用,或者等 OPTIMIZE TABLE 之后才能真正释放回操作系统。另外一个隐藏问题:如果 DELETE 的量特别大,Purge 线程处理不过来,Undo Log 还会持续膨胀,导致数据库磁盘空间不减反增。我见过有同事在大表上 DELETE 掉 80% 的数据,结果磁盘告警级别反而从 60% 涨到 90%,原因就是 Undo 膨胀。

理解这两条语句的底层行为之后,后面讲 JOIN 更新、批量删除、慢更新排查,才有理论基础。

2. 联合 UPDATE 与关联 DELETE:容易踩坑的两类关联操作

单表 UPDATE 和 DELETE,只要 WHERE 条件写对,基本没什么可说的。真正让人头疼的是联合更新和关联删除。线上业务的数据永远是有关系的,你修改 A 表时需要参考 B 表的状态,删除 C 表时需要把 D 表里对它的引用也处理掉。这时两条语句就会变得复杂起来。

2.1 MySQL 里“UPDATE JOIN”的完整写法与执行陷阱

MySQL 不支持 SQL Server 那种UPDATE ... FROM ...语法,它用的是UPDATE ... JOIN ... SET ...写法。下面是个常见场景:根据订单表里近 90 天的消费总额,给用户表更新会员等级。

UPDATE users u JOIN ( SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE order_time >= DATE_SUB(NOW(), INTERVAL 90 DAY) GROUP BY user_id ) tmp ON tmp.user_id = u.id SET u.member_level = CASE WHEN tmp.total_amount >= 10000 THEN 5 WHEN tmp.total_amount >= 5000 THEN 4 WHEN tmp.total_amount >= 1000 THEN 3 ELSE 2 END;

这写法看起来很自然,对吗?我最初也这么写。但注意一个致命问题:UPDATE JOIN 的加锁范围。MySQL 的 UPDATE 在执行时会对 JOIN 中所有匹配到的行加锁,不只是 users 表,orders 子查询里扫描到的相关行也可能被锁住。

更隐蔽的坑是:如果 JOIN 的右表不是唯一的,会出现不确定更新。比如用户表有一条用户记录,但订单表里有两条总额大于 10000 的记录,JOIN 之后会生成两行,UPDATE 最终按哪一行覆盖,取决于执行计划。这种不确定性和业务规则是不兼容的。所以在 UPDATE JOIN 前,务必要确保 ON 条件关联字段是唯一的。

2.2 关联 DELETE 的三种写法与我的选择

关联删除和联合更新类似,核心是从一个表中删除,但过滤条件依赖另一个表。常见三种写法:

-- 写法一:子查询 + IN DELETE FROM orders WHERE user_id IN ( SELECT id FROM users WHERE status = 'disabled' ); -- 写法二:JOIN 删除(MySQL 特有) DELETE o FROM orders o JOIN users u ON o.user_id = u.id WHERE u.status = 'disabled'; -- 写法三:EXISTS 关联 DELETE FROM orders o WHERE EXISTS ( SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 'disabled' );

这三种写法在结果上等价,但细节差异很大。子查询 + IN 在数据量大的时候特别容易触发临时表,性能非常不稳定;JOIN 删除在某些 MySQL 版本上会有额外的锁行为;EXISTS 写法在关联表数据量不大时通常最稳。

我做过一个小实验,两张表各 50 万行,status='disabled'的用户约有 5 万,关联的订单约 30 万行。EXISTS 写法耗时 1.8 秒,IN 写法耗时 3.2 秒,JOIN 删除耗时 2.4 秒。当然这只是我这边测试环境的结论,执行计划会随数据分布而变。但一个小原则是:左表过滤后结果集较大时,EXISTS 比 IN 更稳妥,因为 EXISTS 是边扫描边判断。在关联删除的 WHERE 过滤条件只涉及单表字段时,务必在两张表之间建立索引——如果外键列没索引,任何关联删除都会退化成全表循环扫描。

2.3 这些操作的一个共同底线:先 SELECT 验证

凡是涉及关联 UPDATE 和关联 DELETE,我在生产环境都会强制加一个前置步骤:先把 UPDATE/DELETE 改成等价的 SELECT,跑一遍看命中的行数和数据范围。这个步骤不会花多少时间,但能避免绝大多数“误操作删错数据”的事故。

比如刚才的例子,我会先执行:

SELECT u.id, u.member_level, tmp.total_amount FROM users u JOIN ( SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE order_time >= DATE_SUB(NOW(), INTERVAL 90 DAY) GROUP BY user_id ) tmp ON tmp.user_id = u.id LIMIT 10;

看返回的数据是否符合预期,再执行真正的 UPDATE。不要嫌麻烦。我在生产环境救过太多次这种“理论上没错,但业务上完全不该改”的操作了。

3. DELETE 大量数据时的顺序、节奏与隐藏风险

如果说 UPDATE 的坑大多出在锁和 JOIN 上,那么 DELETE 的坑基本都出在“量”上。删除 100 条和删除 1000 万条,不是难度上的线性增长,而是量变引发质变,会出现完全不同的故障模式。

3.1 为什么一次性 DELETE 1000 万行会把主库拖垮

假设你要清理 orders 表中一年前的过期订单数据,共约 1000 万行。一条DELETE FROM orders WHERE order_time < '2024-01-01'直接甩上去,会怎样?

  • 单事务锁持有时间过长:这条 DELETE 是一个事务,从第一条到最后一条,所有相关行都持有排他锁,期间任何对该表的 INSERT/UPDATE 都会被阻塞。
  • Undo Log 膨胀:删除 1000 万行产生海量 Undo Log,占用磁盘空间,同时 Purge 线程的处理速度跟不上,Undo 会持续累积。
  • 主从延迟放大:主库删除产生的 Binlog 传到从库后,从库同样需要执行 1000 万行删除,而且通常单线程复制,延迟会像滚雪球一样扩大。

我以前遇到过主库 DELETE 跑了 40 多分钟,从库延迟飙到 5000 多秒的案例。根本原因不在于语句本身写错,而在于没有控制事务大小。

3.2 分批删除的节奏控制方法

正确做法是切分事务,用循环分批删除。这里有个关键细节:每次删除的“批”不能只看行数,还要看数据的物理分布。

-- 基于主键分段的循环删除(推荐) DELETE FROM orders WHERE order_time < '2024-01-01' AND id BETWEEN 1 AND 50000;

注意,不要用 LIMIT 加随机范围,而是要用主键范围切片。这比DELETE ... LIMIT 1000更可控,因为 LIMIT 方式每次都要重新扫描索引定位起点,而主键分段可以稳定地利用索引。

节奏上,我个人的经验值是:单批 2000 到 5000 行,批间 sleep 50 到 200 毫秒。具体数值取决于机器的 IO 能力。批间 sleep 的目的是让 InnoDB 有喘息的机会,Purge 线程可以在间隙里清理 Undo 和已删除的标记,主从也能追上一些进度。

3.3 三抽查法:删除前、删除中、删除后的状态确认

删除前,我会执行三句检查:

-- 1. 确认总量,评估所需批次 SELECT COUNT(*) FROM orders WHERE order_time < '2024-01-01'; -- 2. 确认最小/最大主键范围,便于切片 SELECT MIN(id), MAX(id) FROM orders WHERE order_time < '2024-01-01'; -- 3. 确认当前表大小与磁盘剩余空间,避免 Undo 膨胀打爆磁盘 SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_name = 'orders';

删除过程中,每跑完一批可以用SHOW PROCESSLIST和主从状态确认没有异常。删除完成后,再用COUNT(*)和前后大小对比来验证结果。这套流程看着笨重,但实际只要写个小的存储过程或客户端脚本定时切批,就非常省心。

3.4 DELETE 之后表空间还是很大的处理思路

如果大批量删除之后,表文件确实收缩不下来,而你确认空间必须回收,再做OPTIMIZE TABLE。但注意,这个操作本身就非常耗时,而且是 Online DDL,期间会有轻微锁影响。可以选在业务低峰期执行。如果只是留下了空洞,但后续不会频繁插入新数据,其实可以不管,让空洞留着,性能影响并不大。

4. 慢 UPDATE 与慢 DELETE 的排查链路:一个真实案例

之前在排查一个线上系统时,有一个接口每天凌晨会执行一条 UPDATE,把当天的订单状态批量更新,数据量只有 20 万行。这条语句平时跑 1 秒多,某天突然变成了 40 秒。周围同事的第一反应是加索引,但加索引后并没有好转多少。我接手排查的过程,正好可以拿来当成一个完整的排查链路示例。

4.1 用 EXPLAIN 抓出真正的“元凶”

原始语句大概是这样的:

UPDATE order_task SET status = 'done', processed_time = NOW() WHERE batch_no = 'BATCH2024061301' AND task_date = '2024-06-13';

先执行 EXPLAIN:

EXPLAIN SELECT id, status FROM order_task WHERE batch_no = 'BATCH2024061301' AND task_date = '2024-06-13';

输出里 key 是 idx_batch_no,type 是 ref,rows 显示 56。索引是走了,但问题在于——它只用了 batch_no 这一个单列索引,回表之后还要再过滤 task_date。如果 idx_batch_no 的基数很小(也就是说同一个 batch_no 对应了特别多行),那么“走索引”和“全表扫”就没什么本质区别,只是多了一层索引跳转而已。

检查索引后才发现,order_task 表给 batch_no 和 task_date 分别建了独立的单列索引,MySQL 只能选一个。复合索引(batch_no, task_date)一补上,EXPLAIN 的 rows 从 56 压到个位数,UPDATE 耗时立刻掉到 700 毫秒以内。

4.2 隐式类型转换:最常见的隐藏陷阱

那几天我还顺手看了同库的其他慢 SQL,发现一条很典型的慢 DELETE:

DELETE FROM user_login_log WHERE user_id = '100001';

user_id 是 BIGINT 类型,但 Java 侧传参用了字符串,MySQL 会自动把字符串转成数字去比较。问题不在这,而在于如果字段类型是 VARCHAR,你传了数字,那就会导致索引失效。比如反过来的情况:

DELETE FROM users WHERE phone = 13800138000;

phone 是 VARCHAR,但你传的是数字,MySQL 会把表里每一行的 phone 隐式转成数字再比较,索引全废,瞬间全表扫描。这个坑在 DELETE 和 UPDATE 上比 SELECT 更致命,因为不光慢,还容易把大范围的锁给引出来。排查慢 UPDATE/DELETE 时,第一步永远应该确认:WHERE 条件的字段类型和传入值的类型是否一致。

4.3 锁等待导致的“假慢”

还有一个经常被忽略的维度:语句本身很快,但就是一直卡着。这种多半不是“慢”,而是“等锁”。排查指令就是查锁等待:

SHOW ENGINE INNODB STATUS;

重点看 TRANSACTIONS 段落里有没有锁等待记录的 TRX_ID,配合 information_schema 下的 INNODB_TRX、INNODB_LOCK_WAITS 两张表,可以快速找到谁持有了锁、谁在等待。很多时候只需要把长事务的 COMMIT 节奏对齐,问题就解决了,根本不用改 SQL。

注意:UPDATE 和 DELETE 比 SELECT 对锁敏感得多。同一行数据,SELECT 可以并发读,UPDATE 只能串行改。如果你有一条 UPDATE 经常卡十几秒,首先要做的是查锁等待,而不是琢磨怎么优化执行计划。

5. UPDATE 与 DELETE 的安全红线:注入、误操作与数据恢复

写 SQL 的能力,一半体现在效率和性能上,另一半体现在安全上。UPDATE 和 DELETE 是数据破坏力最强的两条语句,也是 SQL 注入攻击的重点对象。把安全红线讲清楚,比多记几个语法更重要。

5.1 为什么 UPDATE 和 DELETE 的注入比 SELECT 更危险

SQL 注入中最常被提到的是万能密码绕过登录,类似把 SELECT 的 WHERE 条件构造成OR '1'='1'。但攻击者一旦把同样的思路用在 UPDATE 或 DELETE 上,性质就完全不同了——SELECT 注入只是脱库,UPDATE 注入可以直接改你的数据,DELETE 注入可以直接把表清空。

举个例子,一个典型的登录后修改密码功能,SQL 可能是这样拼出来的:

String sql = "UPDATE users SET password = '" + newPwd + "' WHERE username = '" + username + "'";

如果 username 的值是' OR '1'='1' --,拼出来就是:

UPDATE users SET password = 'attacker_pwd' WHERE username = '' OR '1'='1' -- '

这条语句会更新 users 表里所有行的密码,攻击者直接拿下全库账号。这还只是最简单的例子,如果攻击者把 SET 字段也注入,比如把newPwd传成pwd='234' WHERE 1=1 --,那连表结构都能被猜出来并篡改。UPDATE 和 DELETE 注入的破坏是即时生效的,且往往没有 SELECT 那种可以“试探”的机会。

5.2 四个层次的防线

针对 UPDATE 和 DELETE 的注入,我会从四个层次做防护:

  1. 预编译参数化(第一道防线):用 PreparedStatement 或 ORM 框架参数绑定,从根上杜绝字符串拼接。这条做到了,90% 的注入漏洞就没了。
  2. 白名单校验排序和更新字段:即使用了参数化,动态的表名、字段名仍是风险点。比如 MyBatis 里如果用${}拼接字段名,也需要严密的字段白名单校验。
  3. 最小权限数据库账号:线上业务账号不要有 DELETE 权限除非真实需要。很多团队把 DML 权限全部放开,一旦注入爆发就是全库遭殃。我见过比较规范的做法是:写库账号仅授权所需业务表,且 DELETE 权限集中在管理员账号。
  4. 慢查询与异常 SQL 监控:把大范围的 UPDATE/DELETE(没有 WHERE 或影响行数超过阈值)监控起来,发现异常自动告警,这能兜底减少损失。

5.3 误操作的数据恢复思路

防住了注入,还要防“手滑”。UPDATE ... WHERE id = 1少写一个引号变成全表更新,这种事每个团队都发生过一两次。我的建议是:开发环境无所谓,测试环境必须定期造数据;生产环境大的更新先备份后执行,或用事务包一层,确认影响行数后再 COMMIT。

在支持事务的表引擎(如 InnoDB)里,正确的执行方式是:

BEGIN; UPDATE ... ; -- 此处手动检查影响行数,通常客户端会返回 Rows matched / Changed -- 或者紧接着 SELECT 验证变更结果 ROLLBACK; -- 确认无误后,重新执行并 COMMIT

更高阶一点的做法是把 UPDATE 改成 INSERT 到备份表 + UPDATE 原表的两阶段方式。这些小习惯在平时看起来有点“过度谨慎”,但真遇到一次误操作,你会感谢自己保留了这个流程。

6. 面试与实战里的一些硬核细节补充

UPDATE 和 DELETE 相关的知识,不只是日常工作要用,面试时也是高频考点。针对面试题里的“坑”,我这边再做一轮补充,特别是很多高赞解答里都没讲透的点。

6.1 一条 UPDATE 之后,旧的索引数据去了哪

前面说过二级索引的延迟更新机制。很多面试官爱问:“UPDATE 一个在二级索引上的字段,性能为什么比 UPDATE 普通字段更差?”标准回答是:因为 InnoDB 需要维护主键索引和二级索引两份数据,并且二级索引的更新是通过“删除旧索引项 + 插入新索引项”模拟实现的,这个模拟操作需要在 Purge 线程中异步执行。所以频繁更新索引字段的场景性能下降会非常明显。

实战中,如果你的业务存在大量秒级别的状态更新,而其中某些字段又有索引,要评估是不是真的需要每次都更新这个索引字段。有些场景可以通过把高频变化字段从索引中移除来解决,本质上就是减少索引维护成本。

6.2 SQL Server 与 MySQL 的 DELETE/UPDATE 差异对比

虽然现在是 MySQL 当道,但很多团队还在用 SQL Server。我整理了一张两边的关键差异表,方便面试和实际切换时参考:

对比项MySQLSQL Server
关联更新语法UPDATE ... JOIN ... SETUPDATE ... SET ... FROM ... JOIN
删除时是否回收空间标记删除,空间一般不复用同样标记,空间管理机制不同,收缩需 DBCC
默认隔离级别可重复读(RR)读已提交(RC)
UPDATE 是否锁定所有扫描行是,扫描范围大则锁范围大是,但 RC 下部分语句不加行锁可避免阻塞
删除大表时的 Undo/Purge 表现Undo 膨胀风险明显tempdb 与版本仓的使用逻辑类似,同样有膨胀风险

SQL Server 支持DELETE TOP (n)语法,这个比 MySQL 的 LIMIT 删除更优雅,但两者本质上都一样,需要配合循环来控制事务大小。如果你手上是 SQL Server,执行大批量删除时可以考虑把TOP和WHILE循环结合,效率比逐行删除高得多。

6.3 慢查询日志里的几个关键线索

最后聊一个日常排查经验。打开慢查询日志时,面对一堆记录要怎么看?我的习惯顺序是:

  1. 先看 Rows_examined 和 Rows_sent/Rows_affected 的差距,如果扫描 100 万行只影响 10 行,必然有索引问题。
  2. 再对比执行计划里的 key、rows、filtered 三项,确认有没有走错索引或回表过多。
  3. 最后确认是不是等锁:看等待时间占比,如果 InnoDB 状态里锁等待占了大头,就别折腾索引了,去查事务并发。

有一次排查大表分页 DELETE 性能问题时,我就是在慢日志里发现某条语句 Rows_examined 达到 800 万但实际删除量只有 2 万。一查执行计划,果然是 ORDER BY 和 LIMIT 配合时优化器选错了索引,导致每批删除都重复扫前面的大半段。后来把删除条件改成基于主键范围切片,性能立刻正常。这类“大量扫描、少量操作”的 CURD,是所有 UPDATE/DELETE 性能问题里最容易判断、也最容易修复的一类。

提示:大批量 UPDATE 或 DELETE 执行前,把 WHERE 条件同样放在一个 SELECT 里跑一次执行计划,是所有排查手段里性价比最高的事。这是一个不需要权限、不依赖工具、两步完成的小习惯,却能在绝大多数情况下提前暴露问题。

7. 几条可以长期使用的经验总结

到这里,UPDATE 和 DELETE 的核心内容基本已经覆盖。最后把一些散落在这篇文章里、或是这几年积累下来我认为最实用的判断标准集中收一下。

7.1 写 UPDATE/DELETE 前自问四件事

在我带的团队里,我会要求所有后端开发在执行这两类语句前过一遍四个问题:

  1. WHERE 条件是否走索引?
  2. 影响行数的量级是多少?是否需要一个事务分批?
  3. 被更新/删除的数据有没有下游依赖?是不是需要同步清理或二次更新?
  4. 这次操作是否经过了 SELECT 预验证?

这四个问题都回答了,CURD 出故障的概率至少下降一半。尤其是在大表、高并发、核心业务表上操作时,这四个问题直接关系线上稳定性。

7.2 一个实用的安全 DELETE 脚本模板

很多人问有没有直接的模板可以复现。我提供一个我常用的 MySQL 存储过程片段,你可以根据自己的表结构调整后使用:

DELIMITER $$ CREATE PROCEDURE safe_batch_delete( IN p_days INT, IN p_batch_size INT, IN p_sleep_ms INT ) BEGIN DECLARE v_max_id BIGINT; DECLARE v_min_id BIGINT; DECLARE v_start_id BIGINT DEFAULT 0; DECLARE v_end_id BIGINT; SELECT MIN(id), MAX(id) INTO v_min_id, v_max_id FROM orders WHERE order_time < DATE_SUB(NOW(), INTERVAL p_days DAY); IF v_min_id IS NULL THEN SELECT 'No rows to delete' AS info; END IF; SET v_start_id = v_min_id; WHILE v_start_id <= v_max_id DO SET v_end_id = v_start_id + p_batch_size; DELETE FROM orders WHERE id BETWEEN v_start_id AND v_end_id AND order_time < DATE_SUB(NOW(), INTERVAL p_days DAY); SET v_start_id = v_end_id + 1; DO SLEEP(p_sleep_ms / 1000); END WHILE; END$$ DELIMITER ;

使用示例:

CALL safe_batch_delete(365, 5000, 100);

这套模板的精髓在于用主键范围而不是 LIMIT 控制批量,能够把事务大小、扫描范围、锁持有时间都控制在一个固定区间内,避免大量扫描和长事务同时出现。

7.3 关于“要不要用事务包 DELETE”

有些同学会用事务包住整个批量删除流程,然后统一 COMMIT。这种做法有它的场景:如果你确定要删除的数据量小,事务统一提交能保证一致性。但如果是千万级大表,一个事务包住全量删除就是灾难。

更合理的组合是:每一批删除自己形成一个事务,批与批之间互不干扰。这样某一批失败时可以单独重试,之前的批次已经提交,不会出现“删到一半全部回滚且锁还没释放”的极端情况。至于“删到一半失败导致数据不一致”的担忧,可以通过在删除前把要删的主键集合快照到中间表来解决。这个中间表相当于本次任务的执行清单,每一批处理完更新一下任务状态,中断后可以续跑,不需要整体回滚。

我在实际项目里就是用这个思路做订单归档清理的:把 3000 万条历史数据分批搬到归档库,同时按主键范围标记任务进度,跑了几轮都稳定收尾,没有出现过从库追不上和磁盘撑爆的问题。

8. 最后的实用建议

很多人学 SQL 到了 SELECT 就走了,真正让一个人从“会写”到“写得稳”的分水岭,恰恰是 UPDATE 和 DELETE 这类会改变数据状态的语句。它们不像 SELECT 那样试错成本低,所以更需要你在动手前多花十几秒确认索引、影响行数和事务边界。

我个人在实际操作中体会最深的一点是:永远不要用生产环境做实验,但一定要为生产环境准备一个可回滚的方案。UPDATE 和 DELETE 的代码写起来最少,但部署到生产环境之后造成的影响往往最大。把原理吃透,把节奏控制好,比花大量时间研究更复杂的数据库优化技巧,更能让系统稳定地跑下去。

如果你正在准备面试,建议把里面的执行原理、锁行为、索引影响、批量删除节奏这四块内容串成一套自己的表达,面试官听到你能从一句 UPDATE 讲到 Undo Log、Purge 和主从延迟,基本就认了。如果你已经在生产环境踩过类似的坑,也希望这篇内容能帮你把当初的“灵光一现”变成系统化的认知。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询