直接执行大表update/delete易致锁表、日志暴涨、超时失败;应使用带order by和稳定字段的where+limit分批处理,并确保索引有效、避免函数导致失效,最后手动更新统计信息。

为什么直接执行 UPDATE/DELETE 大表会卡死或失败
MySQL 或 PostgreSQL 在执行单条 UPDATE 或 DELETE 涉及百万级以上行时,容易触发锁表、事务日志暴涨、内存溢出或连接超时。尤其在生产环境,innodb_lock_wait_timeout(MySQL)或 idle_in_transaction_session_timeout(PostgreSQL)可能直接中断操作,留下不一致状态。
常见错误现象包括:Lock wait timeout exceeded、out of memory for WAL buffers(PG)、主从延迟飙升、监控报警突增。
关键不是“能不能做”,而是“怎么让数据库和应用都扛得住”。
用 WHERE + LIMIT 分批更新,但必须带确定性排序
无序分批(如 WHERE id > ? LIMIT 1000)看似简单,但若期间有新数据插入或删除,会导致漏改或重复改。必须依赖单调递增且稳定的字段(如自增 id 或带索引的 created_at),并显式加 ORDER BY。
例如安全分批删除旧日志:
DELETE FROM logs WHERE created_at <p>每次执行后,记录上一批最后的 <code>id</code> 值作为下一批起点。不要用 <code>OFFSET</code>——它随数据变动而偏移,且越往后越慢。</p><p>要点:</p>
-
ORDER BY字段必须有索引,否则LIMIT失效且全表扫描 - 每批大小建议 500–5000 行,取决于单行数据大小和事务日志容量
- 批次间留 100–500ms 间隔,避免 IOPS 突刺影响线上查询
每批独立事务,别包进一个大事务里
把全部百万行塞进一个事务,等于要求数据库全程持有所有行锁、写满 undo log、阻塞其他写入。正确做法是:每批 LIMIT 操作单独 BEGIN; ... COMMIT;。
PostgreSQL 示例(psql 中可循环):
DO $$
DECLARE
last_id INT := 0;
batch_size INT := 1000;
BEGIN
LOOP
DELETE FROM orders
WHERE id > last_id AND status = 'cancelled'
ORDER BY id
LIMIT batch_size;
<pre class="brush:php;toolbar:false;"> GET DIAGNOSTICS last_id = ROW_COUNT;
EXIT WHEN last_id = 0;
PERFORM pg_sleep(0.1); -- 控制节奏
END LOOP;END $$;
MySQL 用户注意:GET DIAGNOSTICS 不可用,改用 ROW_COUNT() 并配合客户端逻辑判断。
陷阱:
- 应用层重试机制若没判别“已处理完毕”,可能重复执行某批
- 事务隔离级别为
REPEATABLE READ(MySQL 默认)时,同一事务内多次SELECT看不到自己刚删的数据,影响进度判断
上线前必须验证 WHERE 条件与索引匹配度
EXPLAIN 是唯一可信依据。哪怕 WHERE status = 'old' 看似简单,若 status 列无索引或选择率太高(比如 95% 行都满足),分批就退化为全表扫描。
执行前务必确认:
-
EXPLAIN SELECT id FROM table WHERE your_condition ORDER BY id LIMIT 1显示type为range或ref,且key列出有效索引 - 若条件含函数(如
DATE(created_at) ),索引失效,需改写为范围查询(<code>created_at ) - 复合索引顺序要匹配:如按
status, created_at查询,索引应为(status, created_at),而非反过来
最常被忽略的一点:分批脚本跑完后,ANALYZE TABLE(MySQL)或 VACUUM ANALYZE(PG)必须手动触发,否则统计信息滞后,后续查询计划可能劣化。










