
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.4
docker run -d --name mongo_exp -p 27017:27017 mongo:4.4 mongod --bind_ip_all
sleep 5
echo "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: 0
storageSize: 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 文件大小完全不变为什么会这样?
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 确实可以回收空间,但有两个重要限制:
drop 命令:
> var r = db.exp_coll.drop()
> print("drop result: " + r)
drop result: truedrop 后物理文件:
$ 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"
fi
done如果用的是 MongoDB Atlas 云服务,还有 Online Archive 功能:
方案 | 空间释放 | 自动化程度 | 查询复杂度 | 适用场景 |
|---|---|---|---|---|
TTL 索引 | 不释放(标记删除) | 全自动 | 无感 | 日志、会话、临时数据 |
时间桶 + drop() | 完全释放 | 定时任务 | 视图统一 | 订单、流水、审计数据 |
Atlas Online Archive | 释放(迁移) | 全自动 | 无感 | 云上大表冷热分层 |
为什么"时间桶 + drop()"是首选? 因为本文实验已经证明:
deleteMany不释放空间、compact会阻塞写入,只有drop()是完全释放且开销最低 的。 时间桶方案把"删数据"变成了"删集合",每次清理都是秒级 drop,空间立即归还,完全避开 deleteMany 的空间陷阱。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。