deleteMany / remove 不会释放磁盘空间。
WiredTiger 只做标记删除,空间留给后续复用,不会返还给操作系统。
真正能回收空间的是compact(部分回收)和drop()(完全释放)。
前几天在帮一个朋友问我,MongoDB所在服务的磁盘满了。要删除数据行不行?
我第一反应你要怎么删除?
因为我多年经验告诉我这又是一个开发人员想出来的。因为在Oracle MySQL上我见过太多的开发写的脚本就是delete from。一般的关系型数据库删除数据以后,你可以继续往里面写,但是操作系统上的空间是不会释放的。(除非你做碎片整理,而Oracle上连最大使用空间的高水位线都不会回收)
其实对于NoSQL来说也差不多是这样。MongoDB 底层存储引擎 WiredTiger 的空间管理机制也这样。但是很多开发者不了解数据库都会误以为
deleteMany({}) 和
remove() 会像 MySQL 的
DELETE 一样,删除后空间立即返还给操作系统。
事实并非如此。
为了把这个事情讲清楚,我在 Docker 环境(mongo:4.4)做了一组对照实验,每一步都记录
storageSize 和物理
.wt 文件大小的变化。
| 项目 | 值 |
|---|---|
| MongoDB 版本 | 4.4.30(Docker mongo:4.4) |
| 存储引擎 | WiredTiger(默认 snappy 压缩) |
| 操作系统 | Linux(Docker 容器) |
| 测试数据量 | 20 万条文档 |
| 单条文档大小 | 约 400 字节(含 300 字节 padding) |
| 逻辑数据总量(dataSize) | 约 78.18 MB |
| 测试服务器 | 10.60.143.102:27017 |
说明:MongoDB 5.0+ 要求 CPU 支持 AVX 指令集,本次实验的 102 服务器 CPU 不支持 AVX,因此使用 mongo:4.4 版本。结论在 4.4 ~ 7.x 均适用。
# 清理旧容器docker rm -f mongo_exp 2>/dev/null# 启动 MongoDB 4.4docker run -d --name mongo_exp -p 27017:27017 mongo:4.4 mongod --bind_ip_allsleep 5echo "MongoDB 启动完成"
插入脚本(mongo shell):
var coll = db.exp_coll;var bulk = [];for (var i = 0; i < 200000; i++) {
bulk.push({ _id: i, name: 'user_' + i, email: 'user_' + i + '@test.com', age: i % 80, score: Math.random() * 100, note: 'x'.repeat(300) // 人为放大文档体积
}); if (bulk.length === 1000) { coll.insertMany(bulk); bulk = []; }
}if (bulk.length) coll.insertMany(bulk);print("插入完成");
插入后 stats 结果:
> db.exp_coll.stats()
{ "count" : 200000, "size" : 81999904, // dataSize ≈ 78.18 MB
"storageSize" : 0, // 压缩后几乎为 0
"totalIndexSize" : 0, "nindexes" : 1}
物理文件大小(插入后):
$ docker exec mongo_exp ls -lh /data/db/collection-*.wt /data/db/index-*.wt -rw------- 1 mongodb mongodb 4.0K collection-7-xxxx.wt # 数据文件-rw------- 1 mongodb mongodb 4.0K index-8-xxxx.wt # 索引文件$ docker exec mongo_exp du -sh /data/db 301M /data/db # 数据目录总大小
发现 1:WiredTiger 默认 snappy 压缩效果极强,78MB 逻辑数据压缩后物理文件只有 4K。
删除命令:
> var r = db.exp_coll.deleteMany({})
> print("删除: " + r.deletedCount + " 条")
删除: 200000 条
> var s = db.exp_coll.stats()
> print("count: " + s.count)
> print("storageSize: " + s.storageSize)count: 0storageSize: 0 // 显示 0,但实际物理文件没变
物理文件大小(deleteMany 后):
# deleteMany 前$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt -rw------- 1 mongodb mongodb 4.0K index-8-xxxx.wt# deleteMany 后$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt -rw------- 1 mongodb mongodb 1.3M index-8-xxxx.wt # 反而变大了!$ docker exec mongo_exp du -sh /data/db 303M /data/db # 数据目录反而增加了 2MB
对比表格:
| 文件 | 删除前大小 | 删除后大小 | 变化 |
|---|---|---|---|
| index-8-xxxx.wt | 4.0K | 1.3M | 增大 332 倍 |
| collection-7-xxxx.wt | 4.0K | 4.0K | 无变化 |
| /data/db 目录总大小 | 301M | 303M | 增加 2MB |
核心发现:deleteMany 后:
storageSize显示 0(压缩统计的假象)- 物理
.wt文件大小 完全不变- 索引文件反而从 4K 涨到 1.3MB(删除操作产生了索引空洞)
- 磁盘使用量 不会减少
为什么会这样?
WiredTiger 使用 标记删除(tombstone) 机制:
compact 命令:
> var r = db.runCommand({compact: 'exp_coll'})
> print("compact ok: " + r.ok + " bytesFreed: " + r.bytesFreed)
compact ok: 1 bytesFreed: 1318912 // 回收了约 1.26 MB
物理文件大小(compact 后):
# compact 前$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt -rw------- 1 mongodb mongodb 1.3M index-8-xxxx.wt# compact 后$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt -rw------- 1 mongodb mongodb 12K index-8-xxxx.wt # 明显缩小$ docker exec mongo_exp du -sh /data/db 301M /data/db # 减少了 2MB
对比表格:
| 文件 | compact 前 | compact 后 | 变化 |
|---|---|---|---|
| index-8-xxxx.wt | 1.3M | 12K | 减少 1.29MB |
| collection-7-xxxx.wt | 4.0K | 12K | 小幅增加 |
| /data/db 目录总大小 | 303M | 301M | 减少 2MB |
compact 确实可以回收空间,但有两个重要限制:
- 会阻塞写入(MongoDB 4.4 的 compact 是阻塞操作,5.0+ 支持在线 compact)
- 只能回收集合内部的碎片,不能把空间返还给其他集合或系统
drop 命令:
> var r = db.exp_coll.drop()
> print("drop result: " + r)
drop result: true
drop 后物理文件:
$ docker exec mongo_exp ls -lh /data/db/collection-*.wt /data/db/index-*.wt 2>/dev/null# 部分文件仍存在(WiredTiger 延迟清理机制)# 但集合元数据已完全删除,空间会被自动复用
drop() 是最彻底的方式:集合元数据完全清除,空间最终会返还给操作系统(可能有延迟)。
| 操作 | 释放磁盘空间? | storageSize 变化 | 物理文件变化 | 空间复用 | 生产建议 |
|---|---|---|---|---|---|
deleteMany({}) /
remove() |
不释放 | 不变(压缩后显示 0) | 不变 | 可复用 | 业务删除首选,不影响性能 |
compact |
部分回收 | 减少(bytesFreed) | 缩小 | 可复用 | 维护窗口执行,会阻塞写入 |
drop() |
完全释放 | 集合消失 | 最终删除 | - | 不再需要的集合直接 drop |
WiredTiger 使用 B-Tree + 页管理 的存储结构:
插入数据: -> 分配新的 B-Tree 页 -> 数据写入页中 -> 页满后分配新页 删除数据: -> 在 B-Tree 页中标记文档为"已删除" -> 页不会立即合并或释放 -> 被标记的空间记录为空洞(hole) 后续插入: -> 优先使用空洞空间 -> 空洞用完才分配新页
类比:就像你删除了硬盘上的文件,文件系统只是标记这些扇区为"可用",但硬盘的物理容量不会变少。
compact 命令会:
代价:整理过程中集合不可用(MongoDB 4.4),5.0+ 支持在线 compact 但仍有性能影响。
实验中有一个有趣现象:
dataSize 78MB,但
storageSize 几乎为 0。
这是因为 WiredTiger 默认使用 snappy 压缩:
生产建议:不要仅凭
storageSize 判断数据量,要结合
dataSize 和压缩率一起看。
很多从 Oracle / MySQL 转过来的 DBA 都会问这个问题:Oracle 有
PARTITION BY RANGE +
DROP PARTITION,MySQL 有
PARTITION BY RANGE +
ALTER TABLE DROP PARTITION,MongoDB 有没有类似做法?
先说结论:MongoDB 没有原生
DROP PARTITION 语法,但有三种等价方案,其中"时间桶集合 + drop()"最接近 Oracle/MySQL 的分区删除,而且正好用上了我们前面实验的结论。
| 数据库 | 建分区 | 删过期分区 |
|---|---|---|
| Oracle |
PARTITION BY RANGE(created_date) |
ALTER TABLE t DROP PARTITION p202607; |
| MySQL |
PARTITION BY RANGE (TO_DAYS(created_at)) |
ALTER TABLE t DROP PARTITION p202607; |
核心思路: 把数据按时间切成物理分区,过期整个分区直接删掉,空间立刻释放。
MongoDB 原生支持 TTL 索引,对某个时间字段设置过期时间,后台线程自动删除过期文档:
// 在 createdAt 字段上建 TTL 索引,30 天后自动删除db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 2592000 })// 查看 TTL 状态db.runCommand({ collMod: 'logs', index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 2592000 } })
特点:
deleteMany,走的是
标记删除,磁盘空间
不会释放,只是空间可复用⚠️ 注意:TTL 索引不能建在
_id上;如果文档被删后又有新文档插入,TTL 不会复活旧文档。
这是 最接近 Oracle/MySQL 分区表的做法:按时间片建集合,过期直接 drop 整个集合。
// 1. 按天建集合:logs_20260801, logs_20260802 ...db.createCollection('logs_20260801')
db.createCollection('logs_20260802')// 2. 写入时按当前日期路由到对应集合// (应用层根据日期拼集合名,或用 MongoDB 4.4+ 的视图 + $merge 自动化)// 3. 每天定时任务:drop 30 天前的集合 = Oracle 的 DROP PARTITION// 等价于:ALTER TABLE logs DROP PARTITION p20260701;db.getCollection('logs_20260701').drop()
特点:
// 创建视图,聚合多个时间桶,应用层无感知db.createView('logs', ['logs_20260801', 'logs_20260802', 'logs_20260803'], [])
清理脚本(定时任务,每天执行):
#!/bin/bash# 删除 30 天前的日志集合(等价 Oracle: ALTER TABLE logs DROP PARTITION)RETENTION_DAYS=30
CUTOFF=$(date -d "$RETENTION_DAYS days ago" +%Y%m%d)# 列出所有 logs_ 开头的集合,删除过期桶for coll in $(docker exec mongo_exp mongo exp_db --quiet --eval "
db.getCollectionNames().filter(c => c.startsWith('logs_'))
"); do
TS=${coll#logs_}
if [[ "$TS" < "$CUTOFF" ]]; then
docker exec mongo_exp mongo exp_db --quiet --eval "db.${coll}.drop()"
echo "已删除过期集合: $coll"
fidone
如果用的是 MongoDB Atlas 云服务,还有 Online Archive 功能:
| 方案 | 空间释放 | 自动化程度 | 查询复杂度 | 适用场景 |
|---|---|---|---|---|
| TTL 索引 | 不释放(标记删除) | 全自动 | 无感 | 日志、会话、临时数据 |
| 时间桶 + drop() | 完全释放 | 定时任务 | 视图统一 | 订单、流水、审计数据 |
| Atlas Online Archive | 释放(迁移) | 全自动 | 无感 | 云上大表冷热分层 |
为什么"时间桶 + drop()"是首选?
因为本文实验已经证明:
deleteMany不释放空间、compact会阻塞写入,只有drop()是完全释放且开销最低 的。
时间桶方案把"删数据"变成了"删集合",每次清理都是秒级 drop,空间立即归还,完全避开 deleteMany 的空间陷阱。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/637517/viewspace-3181276/ ,如需转载,请注明出处,否则将追究法律责任。