不能直接update大表,因易引发锁表、日志膨胀、oom及主从延迟;须按主键分片、限流执行,如where id > last_id and id

为什么不能直接 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
<h3>PostgreSQL:用 ctid 或窗口函数避免重复扫描
PG 不支持 <code>LIMIT</code> 在 UPDATE 中直接使用,但可用子查询+ctid规避。ctid 是物理位置标识,稳定且高效,适合一次性分批。
<ul>
<li>
<code>ctid</code> 不是主键,但能唯一定位行;只要没 VACUUM FULL 或表重写,它就有效</li>
<li>用 <code>SELECT ctid FROM tbl WHERE ... ORDER BY ctid LIMIT 5000</code> 先取批次,再用 <code>IN (SELECT ctid...)</code> 更新</li>
<li>如果业务允许,也可用 <code>ROW_NUMBER() OVER ()</code> 分桶,但需注意窗口函数在大表上开销不小</li>
<li>务必加 <code>WHERE</code> 过滤条件缩小初始集,否则 <code>ctid</code> 扫描仍是全表</li>
</ul>
<pre class="brush:php;toolbar:false;">WITH batch AS (
SELECT ctid FROM users
WHERE last_login
<h3>通用陷阱:WHERE 条件变化导致漏更或死循环
分批逻辑看似简单,但实际最容易栽在这里。例如:
<ul>
<li>更新字段本身被用作分批条件(如 <code>WHERE status = 'pending'</code>,而你正在把 status 改成 'done')——后几批可能永远扫不到剩余行</li>
<li>并发写入插入新行,ID 跳变,导致某段 ID 区间被跳过</li>
<li>没加 <code>FOR UPDATE SKIP LOCKED</code>(PG)或 <code>SELECT ... FOR UPDATE</code>(MySQL),多进程并发跑时重复处理同一行</li>
<li>忘记检查影响行数(<code>ROW_COUNT()</code> 或 <code>pg_affected_rows</code>),当返回 0 却没退出循环,陷入空转</li>
</ul>
真正麻烦的不是怎么写第一批次,而是让整个批次链可靠收尾——尤其是中间失败后如何续跑、如何幂等、如何监控进度。这些往往比 SQL 本身更耗精力。</h3>
id 或 updated_at),否则 LIMIT 仍会全表扫描WHERE id > ? ORDER BY id LIMIT 5000
id,作为下次查询条件UPDATE 涉及索引字段变更,需确保排序字段(如 id)本身不变,否则游标失效










