不能直接UPDATE大表,因易引发锁表、日志膨胀、OOM及主从延迟;须按主键分片、限流执行,如WHERE id > last_id AND id <= last_id + 1000。

为什么不能直接 UPDATE 大表?
直接对千万级表执行 UPDATE 容易锁表、OOM、拖垮数据库响应。MySQL 默认事务下,全表扫描+行锁会持续占用大量 buffer pool 和 undo log;PostgreSQL 则可能触发长事务膨胀(bloat)。更糟的是,语句中途失败,回滚代价极高,甚至导致主从延迟飙升。
本质不是“语法不支持分批”,而是必须主动控制事务粒度、扫描范围和资源消耗。
MySQL:用 WHERE + LIMIT 控制每次更新范围
核心思路是避免全表扫描,用主键或有索引的字段做游标分页。别用 OFFSET——数据变动时会漏/重;改用“上次更新的最大 ID”作为下一批起点。
- 先确认目标字段有索引(比如
id 或 updated_at),否则 LIMIT 仍会全表扫描
- 每次只更新固定数量(如 5000 行),用
WHERE id > ? ORDER BY id LIMIT 5000
- 每次事务结束后查出本次最大
id,作为下次查询条件
- 注意:若
UPDATE 涉及索引字段变更,需确保排序字段(如 id)本身不变,否则游标失效
UPDATE orders SET status = 'processed'
WHERE id > 1000000 AND id <= 1005000;
PostgreSQL:用 ctid 或窗口函数避免重复扫描
PG 不支持 LIMIT 在 UPDATE 中直接使用,但可用子查询+ctid规避。ctid 是物理位置标识,稳定且高效,适合一次性分批。
-
ctid 不是主键,但能唯一定位行;只要没 VACUUM FULL 或表重写,它就有效
- 用
SELECT ctid FROM tbl WHERE ... ORDER BY ctid LIMIT 5000 先取批次,再用 IN (SELECT ctid...) 更新
- 如果业务允许,也可用
ROW_NUMBER() OVER () 分桶,但需注意窗口函数在大表上开销不小
- 务必加
WHERE 过滤条件缩小初始集,否则 ctid 扫描仍是全表
WITH batch AS (
SELECT ctid FROM users
WHERE last_login < '2023-01-01'
ORDER BY ctid LIMIT 5000
)
UPDATE users SET status = 'inactive'
WHERE ctid IN (SELECT ctid FROM batch);
通用陷阱:WHERE 条件变化导致漏更或死循环
分批逻辑看似简单,但实际最容易栽在这里。例如:
- 更新字段本身被用作分批条件(如
WHERE status = 'pending',而你正在把 status 改成 'done')——后几批可能永远扫不到剩余行
- 并发写入插入新行,ID 跳变,导致某段 ID 区间被跳过
- 没加
FOR UPDATE SKIP LOCKED(PG)或 SELECT ... FOR UPDATE(MySQL),多进程并发跑时重复处理同一行
- 忘记检查影响行数(
ROW_COUNT() 或 pg_affected_rows),当返回 0 却没退出循环,陷入空转
真正麻烦的不是怎么写第一批次,而是让整个批次链可靠收尾——尤其是中间失败后如何续跑、如何幂等、如何监控进度。这些往往比 SQL 本身更耗精力。
id 或 updated_at),否则 LIMIT 仍会全表扫描WHERE id > ? ORDER BY id LIMIT 5000
id,作为下次查询条件UPDATE 涉及索引字段变更,需确保排序字段(如 id)本身不变,否则游标失效LIMIT 在 UPDATE 中直接使用,但可用子查询+ctid规避。ctid 是物理位置标识,稳定且高效,适合一次性分批。
-
ctid不是主键,但能唯一定位行;只要没 VACUUM FULL 或表重写,它就有效 - 用
SELECT ctid FROM tbl WHERE ... ORDER BY ctid LIMIT 5000先取批次,再用IN (SELECT ctid...)更新 - 如果业务允许,也可用
ROW_NUMBER() OVER ()分桶,但需注意窗口函数在大表上开销不小 - 务必加
WHERE过滤条件缩小初始集,否则ctid扫描仍是全表
WITH batch AS ( SELECT ctid FROM users WHERE last_login < '2023-01-01' ORDER BY ctid LIMIT 5000 ) UPDATE users SET status = 'inactive' WHERE ctid IN (SELECT ctid FROM batch);
通用陷阱:WHERE 条件变化导致漏更或死循环
分批逻辑看似简单,但实际最容易栽在这里。例如:
- 更新字段本身被用作分批条件(如
WHERE status = 'pending',而你正在把 status 改成 'done')——后几批可能永远扫不到剩余行
- 并发写入插入新行,ID 跳变,导致某段 ID 区间被跳过
- 没加
FOR UPDATE SKIP LOCKED(PG)或 SELECT ... FOR UPDATE(MySQL),多进程并发跑时重复处理同一行
- 忘记检查影响行数(
ROW_COUNT() 或 pg_affected_rows),当返回 0 却没退出循环,陷入空转
真正麻烦的不是怎么写第一批次,而是让整个批次链可靠收尾——尤其是中间失败后如何续跑、如何幂等、如何监控进度。这些往往比 SQL 本身更耗精力。
WHERE status = 'pending',而你正在把 status 改成 'done')——后几批可能永远扫不到剩余行FOR UPDATE SKIP LOCKED(PG)或 SELECT ... FOR UPDATE(MySQL),多进程并发跑时重复处理同一行ROW_COUNT() 或 pg_affected_rows),当返回 0 却没退出循环,陷入空转

















