MongoDB Shell 数据归档指南:将大表数据归档到另一表

将大表中的部分数据归档到另一个表是数据管理的常见需求,特别是对于需要保持主表性能同时又需要保留历史数据的情况。

下面介绍 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();

最佳实践和注意事项

  1. 性能考虑:对大型集合执行基于 ObjectId 的范围查询可能很慢,确保有适当的索引

  2. 备份:在执行大规模删除操作前先备份数据

  3. 转向显式时间字段:考虑在应用程序层添加显式的创建时间字段,以便未来更容易管理

  4. 时区问题:ObjectId 时间戳基于 UTC,在显示或比较时需注意时区转换

  5. 批量处理:处理大量数据时使用批处理方法,避免内存问题

  6. 日志记录:为所有数据管理操作保留详细日志,特别是批量删除和归档操作

  7. 设置TTL索引:如果归档数据只需保留一段时间,考虑使用TTL索引自动清理

相关推荐
泰勒疯狂展开8 小时前
Java研学-MongoDB(三)
java·开发语言·mongodb
网小鱼的学习笔记15 小时前
python中MongoDB操作实践:查询文档、批量插入文档、更新文档、删除文档
开发语言·python·mongodb
互联网搬砖老肖3 天前
运维打铁: MongoDB 数据库集群搭建与管理
运维·数据库·mongodb
LCY1334 天前
kotlin+MongoTemplate的时间类型为is_date类型 pymongo如何处理
python·mongodb·kotlin
泰勒疯狂展开4 天前
Java研学-MongoDB(二)
java·mongodb
SailingCoder7 天前
MongoDB Memory Server与完整的MongoDB的主要区别
数据库·mongodb
水木石画室7 天前
MongoDB 常用增删改查方法及示例
数据库·mongodb
旷世奇才李先生7 天前
MongoDB 安装使用教程
数据库·mongodb
qq_339282237 天前
mongodb 中dbs 时,local代表的是什么
数据库·mongodb