1. 几亿数据里删三个月前的记录为什么不能直接 deleteManyMongoDB 大批量删除数据这件事真正踩过坑的人都知道deleteMany写起来最爽跑起来最要命。我见过一个集合每天新增上千万条攒到几亿条之后要清理三个月前的历史数据运维同学直接甩了一条db.xxx.deleteMany({ct: {$lt: 三个月前}})上去结果主库 CPU 打满、大量写锁等待、从库延迟飙到几十分钟线上业务跟着抖。问题不在 MongoDB 不行而在于「一次性大事务式删除」这个动作本身。删除几千万条文档时MongoDB 需要逐条标记删除、维护索引、写 oplog整个过程会长时间持有锁资源还会把 oplog 撑爆导致从库追不上。所以真正要解决的不是「怎么删」而是「怎么把一次大删除拆成很多次小删除并且能观测、能中断、能回滚」。这篇聚焦三件事一是把deleteMany、TTL 索引、分批删除三种方案摆在一起对比性能和风险二是给出可复制的config.toml与settings.json骨架通过 TaoToken 统一 Key/API 通道接入 AI 工具辅助生成删除脚本三是给出验证动作——统计删除耗时、锁等待、主从延迟确认方案真的能落地。适合正在写定时清理任务、被大批量删除拖垮过、或者想给团队定一套删除规范的后端同学。先说结论方向TTL 索引适合「按时间自动过期」的日志类数据但它有精度和不可控的问题deleteMany只适合小批量或带精确条件的删除真正面对几亿数据的历史清理分批删除游标或分页查询 按_id批量删才是稳妥选择。下面逐个拆。2. 三种方案横向对比deleteMany、TTL 索引、分批删除在动手写脚本之前先把三种方案的适用边界理清楚不然很容易选错方向。deleteMany的优点是语法简单、一次调用完成缺点是它本质上是一个大操作。当匹配文档数量达到百万级以上时它会长时间占用资源oplog 单条记录可能非常大从库回放压力陡增。它适合「条件精确、命中数量可控比如几万条以内」的场景比如按某个业务 ID 清理。TTL 索引的优点是「零维护」建好索引后 MongoDB 后台线程会自动清理过期文档。但它有几个硬伤一是清理线程默认每 60 秒跑一次删除不是实时的二是 TTL 只能基于 Date 类型字段且不能做复杂条件三是后台删除同样会产生 oplog量大时一样影响从库四是它不可控你没法精确知道什么时候删完。所以 TTL 适合日志、会话、临时缓存这类「过期即弃」的数据不适合需要精确控制删除节奏的业务数据。分批删除是工程上最常用的方案。核心思路是每次只取一批比如 1 万条符合条件的_id然后按_id in [...]删除循环直到没有数据。这样每次删除都是小操作锁持有时间短oplog 记录小从库能跟上而且可以在每批之间加 sleep 控制节奏出问题能随时停。方案适用数据量锁影响主从延迟风险可控性推荐场景deleteMany万级以内高高低精确条件小批量TTL 索引任意自动中中低日志/会话/临时数据分批删除百万到几亿低低高历史数据定时清理分批删除内部还有两种取数方式游标遍历MongoCursor和分页查询find limit。原方案里对比过游标遍历在hasNext()循环上耗时较多而分页查询如果ct字段有索引每次查 1 万条返回很快整体效率更高。所以下面我以分页查询 按_id批量删为主线。3. TaoToken 前置统一 Key 与 API 通道准备写删除脚本时很多细节比如批量大小怎么定、异常怎么重试、怎么加监控埋点可以让 AI 工具帮你补全和 review。这里用 TaoToken 的统一 Key/API 通道接入 AI 工具好处是一个 Key 走通多个模型不用在多个平台之间来回切。TaoToken 官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基地址不带 UTMhttps://taotoken.net/api你需要先拿到 API Key在控制台的 API Keys 页面创建控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite拿到 Key 之后如果你只是想先验证模型能不能正常对话可以直接用模型对话页面试模型对话https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite如果你是要长期写代码、跑 Agent 辅助生成删除脚本建议看 Coding PlanCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite接入文档在这里配置格式以文档为准接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意API Key 属于敏感凭证不要写进代码仓库用环境变量或本地配置文件管理配置文件记得加进.gitignore。4. 可复制配置config.toml 与 settings.json 骨架下面给出两份可直接改用的配置骨架。config.toml用于 AI 工具/CLI 侧接入 TaoTokensettings.json用于删除任务本身的参数管理。两份都只是骨架字段按你的实际环境调整。先看config.toml这是给支持 TOML 配置的 AI 编码工具用的# config.toml - AI 工具接入 TaoToken 统一通道 [provider] name taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读取不要硬编码 default_model claude-sonnet # 按需替换为可用模型名 [request] timeout_seconds 120 max_retries 3 retry_backoff_ms 800 [logging] level info log_file ./logs/ai_client.log对应的环境变量设置Linux/macOSexport TAOTOKEN_API_KEY你的_API_KeyWindows PowerShell$env:TAOTOKEN_API_KEY你的_API_Key再看settings.json这是删除任务自己的参数和 AI 通道分开管理避免混在一起{ mongo: { uri: mongodb://user:pass127.0.0.1:27017/?replicaSetrs0, database: biz_db, collection: event_log, time_field: ct, retain_days: 90 }, delete: { batch_size: 10000, sleep_between_batch_ms: 200, max_batches_per_run: 500, use_id_in_delete: true, projection_only_id: true }, monitor: { log_every_batch: true, slow_batch_threshold_ms: 3000, check_repl_lag: true, repl_lag_limit_seconds: 30 }, schedule: { cron: 0 0 3 * * ?, enabled: true } }几个参数说明一下。batch_size控制每批删除量1 万是常见起点数据量特别大或从库压力敏感可以降到 5000。sleep_between_batch_ms是每批之间的间隔给从库留追赶时间200ms 起步。max_batches_per_run防止单次任务跑太久超过就停下次继续。projection_only_id对应原方案里「只取_id字段」的优化避免拉取整条文档占内存。提示uri里带 replicaSet 参数能确保连到副本集方便后续读取主从延迟指标。生产环境务必用只读账号做查询、单独账号做删除权限最小化。5. 分批删除脚本从查询到按 _id 批量删配置就绪后核心逻辑就是「查一批_id→ 删一批 → 记录指标 → 循环」。下面用 Java Spring Data MongoDB 写一个可落地的骨架思路和原方案二一致但补上了监控和节奏控制。public class BatchDeleteService { Autowired private MongoTemplate mongoTemplate; private static final String COLLECTION event_log; public void deleteExpired(long cutoffMillis, int batchSize, long sleepMs) { Query query new Query(Criteria.where(ct).lt(new Date(cutoffMillis))); query.fields().include(_id); // 只取 _id省内存 query.limit(batchSize); long total 0; int batchNo 0; while (true) { long t0 System.currentTimeMillis(); ListDocument docs mongoTemplate.find(query, Document.class, COLLECTION); if (docs.isEmpty()) { break; } ListObject ids docs.stream() .map(d - d.get(_id)) .collect(Collectors.toList()); Query delQuery new Query(Criteria.where(_id).in(ids)); mongoTemplate.remove(delQuery, COLLECTION); long cost System.currentTimeMillis() - t0; total ids.size(); batchNo; log.info(batch{} deleted{} costMs{} total{}, batchNo, ids.size(), cost, total); if (cost 3000) { log.warn(slow batch detected, costMs{}, cost); } try { Thread.sleep(sleepMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } log.info(delete finished, total{}, total); } }关键点解释。query.fields().include(_id)只投影_id这是原方案里验证过的优化数据量大时能显著降低内存占用。每批删除用_id in [...]命中主键索引删除本身很快原方案实测 1 万条大约 200 多毫秒。真正耗时的是查询和循环所以ct字段一定要建索引否则每次查询都要全表扫越删越慢。ct字段索引创建db.event_log.createIndex({ ct: 1 }, { background: true, name: idx_ct })如果删除条件还带其他字段考虑建复合索引比如{ ct: 1, status: 1 }。索引建好后用explain确认查询走的是索引而不是 COLLSCANdb.event_log.find({ ct: { $lt: ISODate(2024-01-01) } }) .projection({ _id: 1 }) .limit(10000) .explain(executionStats)看executionStats.executionStages.stage是不是IXSCAN如果是COLLSCAN就说明索引没生效得先解决索引问题再跑删除。如果你想让 AI 工具帮你把这段脚本改成 Python 或 Go 版本或者补上 Prometheus 埋点可以把上面的代码贴给模型让它按你的技术栈改写。模型对话入口在上面第 3 节给过长期写这类脚本用 Coding Plan 更顺手。6. 验证请求与成功结果耗时、锁等待、主从延迟脚本能跑不代表方案能落地必须用数据验证。三个核心指标删除耗时、锁等待、主从延迟。删除耗时直接看日志里的costMs和total。正常情况下每批 1 万条应该在几百毫秒到 1 秒之间如果某批突然超过 3 秒说明可能遇到了锁竞争或索引失效脚本里的slow batch告警就是干这个的。锁等待可以通过serverStatus观察db.serverStatus().globalLock.currentQueue重点看currentQueue.total和currentQueue.writers。如果删除期间 writers 队列持续大于 0 且不断增长说明写锁在排队需要降低batch_size或加大sleep_between_batch_ms。主从延迟用rs.status()看rs.status().members.forEach(m { print(m.name, m.stateStr, m.optimeDate, m.lastHeartbeat) })对比主节点和从节点的optimeDate差值就是从库延迟。如果延迟超过你设定的阈值比如 30 秒脚本应该暂停或退出等从库追上再继续。可以在脚本里加一段检查逻辑private boolean replLagTooHigh(long limitSeconds) { Document status mongoTemplate.getDb().runCommand(new Document(replSetGetStatus, 1)); // 解析 members计算主从 optime 差值超过 limitSeconds 返回 true // 具体解析按驱动版本调整 return false; }一次成功的删除任务日志应该长这样每批costMs稳定在几百毫秒total逐批累加没有slow batch告警rs.status()里从库延迟始终在阈值内。跑完之后再查一次集合数量确认目标时间段的数据确实清空了db.event_log.countDocuments({ ct: { $lt: ISODate(2024-01-01) } })返回 0 就说明删干净了。如果还有残留检查是不是有文档的ct字段类型不对比如存成了字符串导致条件没匹配上。7. 本篇常见错排查报错一operation exceeded time limit或游标超时。用游标遍历时如果长时间不消费游标会超时。解决办法是设置noCursorTimeout(true)或者干脆改用分页查询方案每批独立查询不依赖长游标。分页方案还有个好处是任务中断后可以从头再来不用维护游标状态。报错二删除越来越慢。大概率是ct字段没索引或者索引被其他操作影响。先用explain确认走索引再检查是不是每批删除后索引统计信息没更新。另外注意如果删除条件命中的文档在集合里分布很散索引扫描效率也会下降可以考虑按时间范围分片删除。报错三主从延迟持续升高。说明删除速度超过了从库回放速度。三个调节手段降低batch_size、加大sleep_between_batch_ms、在脚本里加从库延迟检查延迟高就暂停。不要为了追求速度把 sleep 设成 0那等于变相的大批量删除。报错四_id in [...]删除时内存溢出。如果batch_size设得太大比如 10 万in列表会占用大量内存。控制在 1 万以内或者改用bulkWrite分批提交。原方案里 1 万条一批是经过验证的平衡点。报错五定时任务重叠执行。如果上一轮删除还没跑完下一轮 cron 又触发了两个任务同时删会互相干扰。加一个分布式锁比如基于 Redis 或数据库的锁确保同一时间只有一个删除任务在跑。settings.json里的max_batches_per_run也能起到限流作用。报错六AI 工具生成的脚本直接跑生产。这是最危险的。任何 AI 生成的删除脚本先在测试库或从库上跑一遍确认逻辑正确、指标正常再上生产。删除操作不可逆务必先备份或确认有回滚方案。8. 按场景选对入口把删除任务跑稳回到最开始的问题几亿数据删三个月前的记录选哪个方案。我的建议是日志类数据优先考虑 TTL 索引省心业务数据的历史清理用分批删除可控deleteMany只在命中量小的时候用。分批删除的关键不是代码多复杂而是把批量大小、间隔、监控三件事做扎实。如果你在写脚本时需要 AI 辅助补全监控埋点、改写语言版本或者排查上面那些报错按场景选入口排查接入问题、看 API 配置格式API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite配合接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite先验证模型能不能正常对话、试提示词模型对话https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite长期写删除脚本、跑 Agent 辅助编码Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite最后留一个实操习惯每次删除任务上线前先在从库上跑一遍全流程把costMs、锁等待、主从延迟三个指标记录下来作为基线。以后任何参数调整都拿新数据和基线对比别凭感觉调。删除这件事稳比快重要。