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索引自动清理

相关推荐
清风66666616 小时前
基于单片机的电加热炉智能温度与液位PID控制系统设计
单片机·嵌入式硬件·mongodb·毕业设计·课程设计·期末大作业
列御寇16 小时前
MongoDB分片集概述
数据库·mongodb
列御寇17 小时前
MongoDB分片集群——集群组件概述
数据库·mongodb
列御寇18 小时前
MongoDB分片集群——mongos组件(mongos进程)
数据库·mongodb
列御寇19 小时前
MongoDB分片集群分片模式——哈希分片(Hashed Sharding)
数据库·mongodb·哈希算法
列御寇20 小时前
MongoDB分片集群——分片键(Shard Keys)概述
数据库·mongodb
橘橙黄又青3 天前
mongodb的基本命令
数据库·mongodb
DBA小马哥3 天前
MongoDB迁移全解析:国产多模融合下的平滑替代实践
数据库·mongodb·dba
The_superstar63 天前
视觉模块与STM32进行串口通讯(匠心制作)
stm32·嵌入式硬件·mongodb·计算机视觉·串口通讯·视觉模块
BinaryBoss3 天前
Python mongodb批量修改数据库某个字段
数据库·python·mongodb