将大表中的部分数据归档到另一个表是数据管理的常见需求,特别是对于需要保持主表性能同时又需要保留历史数据的情况。
下面介绍 MongoDB Shell 中实现数据归档的几种方法:
方法一:使用 aggregate 和 $out 操作符
这种方法适合一次性归档大量数据:
javascript
db.sourceCollection.aggregate([
// 查询条件,选择需要归档的数据
{ $match: { createdDate: { $lt: ISODate("2023-01-01") } } },
// 可选:对数据进行转换或添加额外字段
{ $addFields: { archivedAt: new Date() } },
// 输出到目标集合
{ $out: "archiveCollection" }
])
注意 :$out
会替换目标集合中的所有文档。如果想要往原先已有的归档表追加数据,需要使用以下方法。
方法二:查询后批量插入(适合追加归档)
javascript
// 查询需要归档的文档
const docsToArchive = db.sourceCollection.find(
{ createdDate: { $lt: ISODate("2023-01-01") } }
).toArray();
// 给文档添加归档时间戳
docsToArchive.forEach(doc => {
doc.archivedAt = new Date();
// 可选:删除不需要的字段
delete doc.someTemporaryField;
});
// 批量插入到归档集合
if (docsToArchive.length > 0) {
db.archiveCollection.insertMany(docsToArchive);
// 记录归档数量
print(`已归档 ${docsToArchive.length} 条记录`);
}
方法三:批处理归档(处理超大数据集)
对于非常大的数据集,可以使用批处理来避免内存问题:
javascript
const batchSize = 1000;
const query = { createdDate: { $lt: ISODate("2023-01-01") } };
let totalArchived = 0;
// 获取匹配的文档总数(可选)
const totalToArchive = db.sourceCollection.countDocuments(query);
print(`需要归档的记录总数: ${totalToArchive}`);
// 批量处理
let batch;
do {
// 获取一批数据
batch = db.sourceCollection.find(query).limit(batchSize).toArray();
if (batch.length > 0) {
// 添加归档时间戳
batch.forEach(doc => {
doc.archivedAt = new Date();
});
// 插入到归档集合
db.archiveCollection.insertMany(batch);
// 从源集合删除已归档的文档
const ids = batch.map(doc => doc._id);
db.sourceCollection.deleteMany({ _id: { $in: ids } });
totalArchived += batch.length;
print(`已处理: ${totalArchived} / ${totalToArchive} 条记录`);
}
} while (batch.length > 0);
print(`归档完成,共归档 ${totalArchived} 条记录`);
方法四:使用原子操作(事务)归档
在支持事务的部署(如复制集)中,可以使用事务确保归档过程的原子性:
javascript
// 启动会话和事务
const session = db.getMongo().startSession();
session.startTransaction();
try {
const sourceColl = session.getDatabase("yourDB").sourceCollection;
const archiveColl = session.getDatabase("yourDB").archiveCollection;
// 查找需要归档的文档
const docsToArchive = sourceColl.find(
{ createdDate: { $lt: ISODate("2023-01-01") } }
).toArray();
if (docsToArchive.length > 0) {
// 添加归档时间戳
docsToArchive.forEach(doc => {
doc.archivedAt = new Date();
});
// 插入到归档集合
archiveColl.insertMany(docsToArchive);
// 从源集合删除
const ids = docsToArchive.map(doc => doc._id);
sourceColl.deleteMany({ _id: { $in: ids } });
print(`已归档 ${docsToArchive.length} 条记录`);
}
// 提交事务
session.commitTransaction();
} catch (error) {
print("归档失败: " + error);
session.abortTransaction();
} finally {
session.endSession();
}
特殊情况(基于 ObjectId)
在 MongoDB 中,如果表中没有显式的创建时间字段,但有 _id
字段是 ObjectId 类型,可以利用 ObjectId 中内置的时间戳来查询、管理和删除历史数据。
ObjectId 时间戳的原理
MongoDB 的 ObjectId 是一个 12 字节的标识符,其中前 4 字节代表文档创建时的时间戳(Unix 时间戳,以秒为单位):
scss
ObjectId = 时间戳(4字节) + 机器标识(3字节) + 进程ID(2字节) + 计数器(3字节)
这意味着每个 ObjectId 都内置了文档的创建时间。
查询历史数据
基于 ObjectId 的时间范围查询
javascript
// 查询特定日期之前创建的文档
const targetDate = new Date("2023-01-01");
const query = {
_id: {
$lt: ObjectId.createFromTime(Math.floor(targetDate.getTime() / 1000))
}
};
// 执行查询
const historicalDocs = db.yourCollection.find(query);
// 计算符合条件的文档数量
const count = db.yourCollection.countDocuments(query);
print(`历史数据数量: ${count}`);
提取 ObjectId 的创建时间
javascript
// 查看文档的实际创建时间
db.yourCollection.find().forEach(doc => {
const timestamp = doc._id.getTimestamp();
print(`文档ID: ${doc._id}, 创建时间: ${timestamp}`);
});
归档历史数据
基于 ObjectId 时间归档数据
javascript
// 定义时间边界
const cutoffDate = new Date("2023-01-01");
const cutoffTimestamp = Math.floor(cutoffDate.getTime() / 1000);
const query = { _id: { $lt: ObjectId.createFromTime(cutoffTimestamp) } };
// 归档操作
const batchSize = 1000;
let totalArchived = 0;
// 分批处理
let batch;
do {
// 获取一批数据
batch = db.sourceCollection.find(query).limit(batchSize).toArray();
if (batch.length > 0) {
// 添加归档时间字段(可选)
batch.forEach(doc => {
// 从ObjectId提取原始创建时间
doc.originalCreationTime = doc._id.getTimestamp();
// 添加归档时间
doc.archivedAt = new Date();
});
// 插入到归档集合
db.archiveCollection.insertMany(batch);
// 从源集合删除已归档的文档
const ids = batch.map(doc => doc._id);
db.sourceCollection.deleteMany({ _id: { $in: ids } });
totalArchived += batch.length;
print(`已归档: ${totalArchived} 条记录`);
}
} while (batch.length > 0);
print(`归档完成,共归档 ${totalArchived} 条记录`);
删除历史数据
直接删除特定时间前的数据
javascript
// 删除2022年之前的所有数据
const deleteDate = new Date("2022-01-01");
const deleteQuery = {
_id: {
$lt: ObjectId.createFromTime(Math.floor(deleteDate.getTime() / 1000))
}
};
// 执行删除前先确认删除数量
const countToDelete = db.yourCollection.countDocuments(deleteQuery);
print(`将要删除 ${countToDelete} 条记录`);
// 执行删除操作
const result = db.yourCollection.deleteMany(deleteQuery);
print(`已删除 ${result.deletedCount} 条记录`);
规范化历史数据管理
1. 添加显式的创建时间字段
将隐含在 ObjectId 中的时间提取出来,作为显式字段:
javascript
// 为所有文档添加一个显式的createdAt字段
db.yourCollection.find().forEach(doc => {
db.yourCollection.updateOne(
{ _id: doc._id },
{ $set: { createdAt: doc._id.getTimestamp() } }
);
});
// 为新的createdAt字段创建索引
db.yourCollection.createIndex({ createdAt: 1 });
2. 创建数据管理脚本
javascript
// 创建一个定期运行的归档脚本
function archiveOldRecords(ageInDays) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - ageInDays);
const cutoffTimestamp = Math.floor(cutoffDate.getTime() / 1000);
const query = {
_id: { $lt: ObjectId.createFromTime(cutoffTimestamp) }
};
print(`开始归档 ${ageInDays} 天前的数据...`);
// 添加批处理逻辑...
// 参考上面的归档代码
}
// 示例用法: 归档90天前的数据
archiveOldRecords(90);
3. 实施数据生命周期策略
javascript
// 在归档集合上设置TTL索引,例如保留一年后自动删除
db.archiveCollection.createIndex(
{ archivedAt: 1 },
{ expireAfterSeconds: 31536000 } // 一年
);
4. 创建数据统计分析脚本
javascript
// 按月份统计数据量
function analyzeDataByMonth() {
const stats = {};
db.yourCollection.find().forEach(doc => {
const createTime = doc._id.getTimestamp();
const yearMonth = `${createTime.getFullYear()}-${(createTime.getMonth() + 1).toString().padStart(2, '0')}`;
if (!stats[yearMonth]) {
stats[yearMonth] = 0;
}
stats[yearMonth]++;
});
// 打印统计结果
for (const [month, count] of Object.entries(stats).sort()) {
print(`${month}: ${count} 条记录`);
}
}
analyzeDataByMonth();
最佳实践和注意事项
-
性能考虑:对大型集合执行基于 ObjectId 的范围查询可能很慢,确保有适当的索引
-
备份:在执行大规模删除操作前先备份数据
-
转向显式时间字段:考虑在应用程序层添加显式的创建时间字段,以便未来更容易管理
-
时区问题:ObjectId 时间戳基于 UTC,在显示或比较时需注意时区转换
-
批量处理:处理大量数据时使用批处理方法,避免内存问题
-
日志记录:为所有数据管理操作保留详细日志,特别是批量删除和归档操作
-
设置TTL索引:如果归档数据只需保留一段时间,考虑使用TTL索引自动清理