首页/数据/SQL 速查/PostgreSQL 分批删除写法与膨胀控制
变更与事务52 条

PostgreSQL 分批删除写法与膨胀控制

PostgreSQL 没有 DELETE LIMIT:用 ctid 子查询分批;删除只是标记死元组,空间回收依赖 VACUUM,批量删除要配合清理策略。

SQL 速查数据更新于 2026-10-13

分批删除与空间回收

PostgreSQL 没有在删除语句里限定行数的语法,要分批得自己切片:用一个子查询先取出这一批要删的行位置,再据此删除。更要紧的是删除之后的账——删除只是把行标记为死元组,空间不会立刻还给操作系统,回收由自动清理任务负责。大批量删除之后如果不关注这一点,表会持续膨胀,查询与写入都会变慢。

基本信息

适用平台PostgreSQL 12 及以上
切片方式用行位置子查询限定每批行数
删除代价标记死元组,空间回收依赖自动清理
膨胀观察查看死元组数量与表实际占用大小的比例
更省的做法按时间分区的表直接摘除或删除整个分区

典型使用场景

  • 按时间清理过期日志:每天删除超出保留期的数据
  • 分批清理历史数据:避免一次性长事务持锁
  • 观察删除后的空间回收情况,评估是否需要人工清理
  • 分区表的历史数据下线
  • 迁移前的数据瘦身

分批删除写法

分批删除写法
-- 每批最多删除 5000 行,用行位置子查询切片
DELETE FROM logs l
USING (
    SELECT ctid FROM logs
    WHERE created_at < now() - interval '90 days'
    ORDER BY id
    LIMIT 5000
) b
WHERE l.ctid = b.ctid;

-- 循环执行直到影响行数小于批次上限,说明已清理完

-- 观察死元组与表大小,判断是否需要额外清理
SELECT relname, n_live_tup, n_dead_tup, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC LIMIT 10;

-- 分区表:直接摘除整段历史,代价远低于逐行删除
-- ALTER TABLE logs DETACH PARTITION logs_2023_01;
-- DROP TABLE logs_2023_01;

删除后的关注点

关注点表现处理方向
死元组堆积死元组数量持续增长检查自动清理是否跟上
表膨胀表占用远大于有效数据评估整表重建或分区改造
长事务阻塞清理被长事务挡住无法回收缩短事务,避免长连接空闲事务
索引膨胀索引体积同步增长重建索引或调整清理频率
分区表摘除分区几乎零成本优先用分区下线替代逐行删除
每批太大就回到长事务问题,太小则事务与索引维护开销占比过高。先用单批耗时与锁等待数据测出合适区间,再固定到脚本里。
行位置在行被更新后会变化,因此每批都要重新取一次位置,不要把位置缓存下来跨批使用。并发写入频繁表上优先考虑主键区间切片。

易错点

  • 只做删除不看空间回收:死元组堆积导致页面膨胀,查询与写入都变慢
  • 自动清理被长事务挡住:删除的空间长期无法回收,表体积持续增长
  • 批太小导致事务与索引维护开销占比过高,批太大又回到长事务问题
  • 把行位置缓存起来跨批复用,行更新后位置变化造成漏删
  • 对可分区的表逐行删除历史数据,代价远高于直接摘除分区

数据来源与参考

  • PostgreSQL Documentation · DELETE
  • PostgreSQL Documentation · Routine Vacuuming
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌