首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >MongoDB 空间又满了,你不会认为删除数据就解决问题了吧?

MongoDB 空间又满了,你不会认为删除数据就解决问题了吧?

原创
作者头像
薛晓刚-
发布2026-09-02 09:04:37
发布2026-09-02 09:04:37
390
举报

先说结论,省流版

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 均适用。


三、实验过程

  • 本身我是知道结论的,为了写这个还是把过程重现一下。重现的过程都是agent去完成的。

3.1 准备:启动干净容器

代码语言:javascript
复制
# 清理旧容器
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 启动完成"

3.2 Step 1:插入 20 万条数据

插入脚本(mongo shell):

代码语言:javascript
复制
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 结果:

代码语言:javascript
复制
> db.exp_coll.stats()
{
  "count" : 200000,
  "size" : 81999904,        // dataSize ≈ 78.18 MB
  "storageSize" : 0,        // 压缩后几乎为 0
  "totalIndexSize" : 0,
  "nindexes" : 1
}

物理文件大小(插入后):

代码语言:javascript
复制
$ 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。


3.3 Step 2:deleteMany({}) 删除全部文档

删除命令:

代码语言:javascript
复制
> 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 后):

代码语言:javascript
复制
# 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) 机制:

  • 删除文档时,只是在 B-Tree 叶子节点上标记"已删除"
  • 这些被标记的空间被记录为"可用空洞"
  • 后续插入新文档时,会优先复用这些空洞空间
  • 但空洞空间不会返还给操作系统

3.4 Step 3:compact 回收空间

compact 命令:

代码语言:javascript
复制
> var r = db.runCommand({compact: 'exp_coll'})
> print("compact ok: " + r.ok + "  bytesFreed: " + r.bytesFreed)
compact ok: 1  bytesFreed: 1318912   // 回收了约 1.26 MB

物理文件大小(compact 后):

代码语言:javascript
复制
# 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 确实可以回收空间,但有两个重要限制:

  1. 会阻塞写入(MongoDB 4.4 的 compact 是阻塞操作,5.0+ 支持在线 compact)
  2. 只能回收集合内部的碎片,不能把空间返还给其他集合或系统

3.5 Step 4:drop() 完全释放

drop 命令:

代码语言:javascript
复制
> var r = db.exp_coll.drop()
> print("drop result: " + r)
drop result: true

drop 后物理文件:

代码语言:javascript
复制
$ 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 空间管理机制详解

5.1 为什么 delete 不释放空间?

WiredTiger 使用 B-Tree + 页管理 的存储结构:

代码语言:javascript
复制
插入数据:
  -> 分配新的 B-Tree 页
  -> 数据写入页中
  -> 页满后分配新页

删除数据:
  -> 在 B-Tree 页中标记文档为"已删除"
  -> 页不会立即合并或释放
  -> 被标记的空间记录为空洞(hole)

后续插入:
  -> 优先使用空洞空间
  -> 空洞用完才分配新页

类比:就像你删除了硬盘上的文件,文件系统只是标记这些扇区为"可用",但硬盘的物理容量不会变少。

5.2 compact 做了什么?

compact 命令会:

  1. 遍历集合的所有 B-Tree 页
  2. 把还在使用的文档重新整理到新的页中
  3. 丢弃全是"已删除"标记的页
  4. 将释放的页空间返还给文件系统

代价:整理过程中集合不可用(MongoDB 4.4),5.0+ 支持在线 compact 但仍有性能影响。

5.3 WiredTiger 压缩的影响

实验中有一个有趣现象:dataSize 78MB,但 storageSize 几乎为 0。

这是因为 WiredTiger 默认使用 snappy 压缩

  • snappy:速度快,压缩率中等(实验中约 100:1,因为测试数据有大量重复 ‘x’)
  • zlib:压缩率高,但 CPU 开销大
  • zstd:MongoDB 4.2+ 支持,压缩率和速度均衡

生产建议:不要仅凭 storageSize 判断数据量,要结合 dataSize 和压缩率一起看。



六、MongoDB 有"分区"吗?过期数据怎么处理?

很多从 Oracle / MySQL 转过来的 DBA 都会问这个问题:Oracle 有 PARTITION BY RANGE + DROP PARTITION,MySQL 有 PARTITION BY RANGE + ALTER TABLE DROP PARTITION,MongoDB 有没有类似做法?

先说结论:MongoDB 没有原生 DROP PARTITION 语法,但有三种等价方案,其中"时间桶集合 + drop()"最接近 Oracle/MySQL 的分区删除,而且正好用上了我们前面实验的结论。

6.1 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;

核心思路:把数据按时间切成物理分区,过期整个分区直接删掉,空间立刻释放。

6.2 MongoDB 方案一:TTL 索引(自动过期,最省事)

MongoDB 原生支持 TTL 索引,对某个时间字段设置过期时间,后台线程自动删除过期文档:

代码语言:javascript
复制
// 在 createdAt 字段上建 TTL 索引,30 天后自动删除
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 2592000 })

// 查看 TTL 状态
db.runCommand({ collMod: 'logs', index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 2592000 } })

特点:

  • MongoDB 后台每 60 秒扫描一次,删除过期文档
  • 完全自动化,无需人工干预
  • 缺点(结合我们实验的结论):TTL 删除本质就是 deleteMany,走的是标记删除,磁盘空间不会释放,只是空间可复用
  • 适合:日志、会话、验证码等"删了不心疼空间"的数据

⚠️ 注意:TTL 索引不能建在 _id 上;如果文档被删后又有新文档插入,TTL 不会复活旧文档。

6.3 MongoDB 方案二:时间桶集合 + drop()(最接近 DROP PARTITION,强烈推荐)

这是最接近 Oracle/MySQL 分区表的做法:按时间片建集合,过期直接 drop 整个集合。

代码语言:javascript
复制
// 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()

特点:

  • drop() 完全释放磁盘空间(我们实验已验证:集合元数据清除、物理文件最终删除)
  • 删除开销极低:drop 是元数据操作,秒级完成,不像 deleteMany 要逐条扫描
  • 查询跨桶时可以用 视图 统一入口:
代码语言:javascript
复制
// 创建视图,聚合多个时间桶,应用层无感知
db.createView('logs', ['logs_20260801', 'logs_20260802', 'logs_20260803'], [])

清理脚本(定时任务,每天执行):

代码语言:javascript
复制
#!/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

6.4 MongoDB 方案三:Atlas Online Archive(云端自动分层)

如果用的是 MongoDB Atlas 云服务,还有 Online Archive 功能:

  • 自动把超过 N 天的冷数据迁移到 S3 对象存储
  • 查询时对应用透明,热数据在集群、冷数据在归档层
  • 相当于"自动分区 + 自动归档",但这是商业版功能,社区版没有

6.5 三种方案对比

方案

空间释放

自动化程度

查询复杂度

适用场景

TTL 索引

不释放(标记删除)

全自动

无感

日志、会话、临时数据

时间桶 + drop()

完全释放

定时任务

视图统一

订单、流水、审计数据

Atlas Online Archive

释放(迁移)

全自动

无感

云上大表冷热分层

6.6 和本文实验结论的呼应

为什么"时间桶 + drop()"是首选? 因为本文实验已经证明:deleteMany 不释放空间、compact 会阻塞写入,只有 drop() 是完全释放且开销最低 的。 时间桶方案把"删数据"变成了"删集合",每次清理都是秒级 drop,空间立即归还,完全避开 deleteMany 的空间陷阱。


七、总结

  1. deleteMany / remove 不释放磁盘空间,这是 WiredTiger 也是几乎所有数据库的特性,
  2. compact 可以回收碎片空间,但会阻塞写入,生产环境需谨慎
  3. drop() 是最彻底的空间释放方式
  4. WiredTiger 压缩率很高,不要仅凭 storageSize 判断数据量
  5. 如果磁盘空间持续增长,优先检查是否有大量"已删除但未 compact"的集合

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先说结论,省流版
    • 一、问题背景
    • 二、实验环境
    • 三、实验过程
      • 3.1 准备:启动干净容器
      • 3.2 Step 1:插入 20 万条数据
      • 3.3 Step 2:deleteMany({}) 删除全部文档
      • 3.4 Step 3:compact 回收空间
      • 3.5 Step 4:drop() 完全释放
    • 四、三种操作对比总结
    • 五、WiredTiger 空间管理机制详解
      • 5.1 为什么 delete 不释放空间?
      • 5.2 compact 做了什么?
      • 5.3 WiredTiger 压缩的影响
    • 六、MongoDB 有"分区"吗?过期数据怎么处理?
      • 6.1 Oracle / MySQL 的做法回顾
      • 6.2 MongoDB 方案一:TTL 索引(自动过期,最省事)
      • 6.3 MongoDB 方案二:时间桶集合 + drop()(最接近 DROP PARTITION,强烈推荐)
      • 6.4 MongoDB 方案三:Atlas Online Archive(云端自动分层)
      • 6.5 三种方案对比
      • 6.6 和本文实验结论的呼应
    • 七、总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档