首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >17000多张MongoDB表的锅 自动分析删除表数据难题--从头到尾的处理过程(文尾有MongoDB开发规范)

17000多张MongoDB表的锅 自动分析删除表数据难题--从头到尾的处理过程(文尾有MongoDB开发规范)

作者头像
AustinDatabases
发布2024-11-25 10:54:32
发布2024-11-25 10:54:32
2700
举报
文章被收录于专栏:AustinDatabasesAustinDatabases

最近遇到一个关于MongoDB棘手的问题,老版本 + 开发胡搞 + 没人管理 = 锅,当然如果我不是这个接锅侠,大家也看不到这个文章。

不会写程序的可以参考

瞬间成为MongoDB专家,8个脚本都写好了,一用一个不吱声

MongoDB 插入更新数据慢,开发问哪的问题?附带解决方案和脚本

我简单描述一下问题,MongoDB3.2,且比我来这个公司都早,开发私自安装,自然是没有章法,弄了一个MongoDB的单机版本,就上线了业务,不过这个业务还算是简单,即使丢失数据可能也不会怎么样,但数据量对比他的这个机器来说,还是大的其中一个逻辑库就 550G了,里面塞满了表。17875个collecions。熟悉且用过MongoDB的同学都明白,这一看就是不会用。MongoDB早期的版本是对一个逻辑库中的collecion 集合有性能要求且建议不要放太多的表,一般500个表到2000个表都还可以,10000多个表这显然是一个异类了。这主要是因为每个集合在 MongoDB 中都由一个独立的 Namespace(命名空间)来管理,当集合数量增加时,MongoDB 需要维护更多的元数据信息,可能使得性能受到影响。

剩下的事情就是,对这个10000多张表进行分析,这里首先我们给出一个脚本来分析这个库里面的每个表行数,那些表是有数据的,那些表是无数据的,把无数据的表单独列出,且分析出到底那些表后续没有数据写入,这里时间点是2023-12-30日。

下面我们用一个脚本来解决问题,这个脚本直接连接到数据库中对数据库中每个业务标准开头的表进行访问,且访问每个表的数据总数,以及最后一条记录中我们要查询的key的日期,来判定这个表到底有没有持续的业务,且写入数据,为我们后面处理这些表,做好的数据的依托。

以下脚本不需要node,js程序支持,MongoDB内部可以识别JS脚本,无需安装直接使用。

代码语言:javascript
复制
// 连接到 MongoDB 数据库
conn = new Mongo("localhost:27017");
db = conn.getDB("logs");

// 输出标题行
print("表名,行数,最后一条记录的opedate时间");

// 获取数据库中所有集合
var collections = db.getCollectionNames();
var collectionsWithData = [];
var collectionsNoDataAfterDate = [];

// 遍历所有集合
collections.forEach(function(collectionName) {
    // 检查集合名称是否以 "collection_ENT" 开头
    if (collectionName.startsWith("collection_ENT")) {
        // 获取当前集合
        var coll = db[collectionName];

        // 统计当前集合的记录数量
        var recordCount = coll.count();

        if (recordCount > 0) {
            // 获取当前集合的最后一条记录
            var lastRecord = coll.find().sort({$natural: -1}).limit(1).next();
            var lastRecordDate = lastRecord ? lastRecord.opedate : "无记录";

            var opedateDate = new Date(Date.parse(lastRecord.opedate)); // 将文本型的 opedate 转换为日期类型
            var targetDate = new Date("2023-12-30");
            if (lastRecordDate != "无记录" && opedateDate <= targetDate) {
                collectionsNoDataAfterDate.push({
                    name: collectionName,
                    count: recordCount,
                    date: lastRecordDate
                });
            } else {
                collectionsWithData.push({
                    name: collectionName,
                    count: recordCount,
                    date: lastRecordDate
                });
            }
        }
    }
});

// 输出有数据的表信息
print("");
print("有数据的表信息:");
collectionsWithData.forEach(function(coll) {
    print(coll.name + "," + coll.count + "," + coll.date);
});

// 输出有数据且在2023年12月30日后没有数据的表信息
print("");
print("有数据且在2023年12月30日后没有数据的表信息:");
collectionsNoDataAfterDate.forEach(function(coll) {
    print(coll.name + "," + coll.count + "," + coll.date);
});

根据业务的要求,我们需要对这些数据保留7个月以上的数据,那么就需要对这些表的数据进行清理的操作,但简单的删除操作会存在以下问题

1 删除数据,需要记录删除多少条数据 2 删除中不能一次性使用 remove 的命令,单机且没有任何的保证的情况下,删除需要控制一次删除的量 3 删除中需要对删除的数据进行备份,以防止删除的时候出现问题

所以后续要处理这17000多张表,还是的分批的处理,且在夜间处理这个部分。

这里我给出几个方案

1 直接打印删除命令法,大家可以看一下这里有一个转换,主要是时间,因为程序员设计mognodb schema中根本没有给ISODATE,全部都是文字给出的时间,所以为了严谨,将他们的时间给进行了一个重塑,将文本变为了日期,在进行比对。

代码语言:javascript
复制
var conn = new Mongo("localhost:27017");
var db = conn.getDB("logs");

var collections = db.getCollectionNames();
var deleteBeforeDate = new Date();
deleteBeforeDate.setMonth(deleteBeforeDate.getMonth() - 7);

collections.forEach(function(collectionName) {
    var cursor = db.getCollection(collectionName).find({});
    cursor.forEach(function(doc) {
        var opedate = new Date(doc.opedate.replace(/(\d+)-(\d+)-(\d+)\s(\d+):(\d+):(\d+)/, "$1/$2/$3 $4:$5:$6"));
        if (opedate < deleteBeforeDate) {
            // 打印要执行的删除命令,但不实际执行
            print("db.getCollection('" + collectionName + "').remove({_id: ObjectId('" + doc._id + "')});");
        }
    });
});

最终这个脚本将打印出,需要删除的数据的主键的命令。

最终要删除这些数据的语句文件就5.4个GB,大小。可能有同学说,为什么我不用直接的语句删除 这个原因有

1 你写的语句删除你怎么控制每次删除的量,如果一个表很大,你一个语句下去,直接造成删除几百万,几千万行的情况,你怎么收场。

2 预先将删除的语句打印出来,是可以校验你删除的数据是否正在,你的脚本是否正确

3 这里有17000多张表,且都没有索引,也就是说就是我指定条件,也是全表扫描,且这里大部分表都是几百行,几千行,少部分有百万行,所以这里采用便利的方式进行数据的处理,而没有使用更高效的方案来处理。

那么如果验证了脚本是正确的,且合理,下面就可以自动的去运行了。下面的脚本就是上面的升级版,直接运行删除语句进行数据的删除。通过下面的脚本,可以避免一个问题,就是遇到一次性删除数据量大,且你用条件来撰写删除脚本中,给数据库带来的大事务(MongoDB 也有事务的概念),如果那样操作也会导致MongoDB 刷脏以及磁盘压力。另一个原因这样操作上面也写到了。

代码语言:javascript
复制
var conn = new Mongo("localhost:27017");
var db = conn.getDB("logs");

var collections = db.getCollectionNames();
var deleteBeforeDate = new Date();
deleteBeforeDate.setMonth(deleteBeforeDate.getMonth() - 7);

collections.forEach(function(collectionName) {
    var cursor = db.getCollection(collectionName).find({});
    cursor.forEach(function(doc) {
        var opedate = new Date(doc.opedate.replace(/(\d+)-(\d+)-(\d+)\s(\d+):(\d+):(\d+)/, "$1/$2/$3 $4:$5:$6"));
        if (opedate < deleteBeforeDate) {
            var deletionCommand = db.getCollection(collectionName).remove({_id: doc._id});
            print("Deleting document from collection " + collectionName + " with opedate: " + doc.opedate);
            printjson(deletionCommand);
        }
    });
});
        

文章总结:在任何的系统设计中,滥用数据库的情况比比皆是,传统数据库如此,MongoDB NoSQL数据库更是如此,如何合理的使用MongoDB,且合理开发都需要有指导和规范。


MongoDB规范

规范⽬的

此规范主要⽬的在于帮助开发⼈员更好的使⽤ mongodb ,避免开发⼈员在开发使⽤中出现问

题,或者疑问不知如何解决。规范的核⼼思想是服务于开发,⽽不是限制开发,希望此规范能帮助

到开发⼈员。

⼆、版本及架构选择

2.1 版本

2.2 架构

⽬前⽀持副本集架构,不建议使⽤单机和分⽚模式。

因为单机安全性低,⽽分⽚模式,需要等到业务单 Collection 达10亿级别以上,再考虑。

三、设计规范

3.1 数据库设计

以下所有规范会按照【⾼危】、【强制】、【建议】三个级别进⾏标注,遵守优先级从⾼到

低。

对于违反【⾼危】和【强制】两个级别的设计,DBA 会强制打回要求修改。3.1.1 Database

1. 【强制】命名规则

2. ⻓度 ≤ 64 个字符

3. 不使⽤关键字

4. 只能包含字⺟;数字;下划线并且不以数字为⾸

5. ⼀律⼩写

6. 【强制】单 Database 容纳 ≤ 100个 Collection。

7. 集合(表)中的时间,必须使⽤ ISODATE 类型,如果不使⽤ISODATE 类型的⽇期,则⽆法启动

expiredata 过期索引

3.1.2 Collection

1. 【强制】命名规则,不能使⽤ - _ $ % …… & @ # ! ( ) + / ? 等任何的符合在collation名中

2. ⻓度 ≤ 64 个字符

3. 不使⽤关键字

4. 只能包含字⺟;数字;下划线并且不以数字为⾸

5. ⼀律⼩写

6. 【强制】单 Collection ≤ 10000 万⾏ Document。

7. 【强制】将同样类型的⽂档存放在⼀个 Collection 中,将不同类型的⽂档分散在不同的

Collection

中。

1. 【建议】流⽔型 Collection 设计固定⼤⼩的轮询集合,或时间字段增加TTL索引,来⾃动清理过

数据。

3.1.3 Document

1. 【强制】⽂档键 命名规则

2. ⻓度 ≤ 64 个字符

3. 不使⽤关键字

4. 只能包含字⺟;数字;下划线并且不以数字为⾸

5. ⼀律⼩写

6. 【强制】 不要向 _id 字段中写⼊⾃定义内容中写⼊⾃定义内容。1

7. 【强制】 嵌套的层数要符合查询的原理

8. 经常读取的字段 ≤ 3 层,添加索引的字段 ≤ 2 层。

9. 不经常读取的字段 ≤ 5 层。

10. 【强制】 含有 ISODATE 类型的时间字段,标明这⼀⾏插⼊的时间。

11. 【强制】 不要让数组类型字段,成为查询条件。

3.1.4 Index

1. 【强制】被索引字段⼤⼩ ≤ 1KB。

2. 【强制】不使⽤全⽂索引。

3. 【建议】优先使⽤覆盖索引。

4. 【建议】多考虑将单列索引并⼊组合索引,并把区分度最⾼的字段放在最前⾯。

3.2 SQL 编写

2 / 41. 【强制】 ne ; not ; exists ; nin ; or ; 等操作符在业务中不要使⽤。2

2. 【强制】update 操作应⾛主键。

3. 【强制】只查询使⽤到的字段,⽽不查询所有字段。e.g. 类似不要⽤ select * 。

4. 【建议】避免在及时性的业务逻辑中使⽤聚合运算。31. _id是MongoDB中的默认主键,⼀旦_id的

值为⾮⾃增,当数据量达到⼀定程度之后,每⼀次写⼊都可能导致主键的⼆叉树⼤幅度调整,这将是

⼀个代价

极⼤的写⼊, 所以写⼊就会随着数据量的增⼤⽽下降,所以⼀定不要在_id中写⼊⾃定义的内容。↩

1. $exist :因为松散的⽂档结构导致查询必须遍历每⼀个⽂档

$ne :如果当取反的值为⼤多数,则会扫描整个索引

$not :可能会导致查询优化器不知道应当使⽤哪个索引,所以会经常退化为全表扫描

$nin :全表扫描

or :有多少个条件就会查询多少次,最后合并结果集,所以尽可能的使⽤ in ↩

1. 因为scheme设计的不合理,聚合过滤完,结果集依旧很⼤

解决的办法可以是,阶段性的写⼊,⽤空间换时间思路。↩

1. 参考:Compatibility Changes with Legacy mongo Shell — MongoDB Shell ↩

四、连接规范

1. 【强制】连接数据库请求,不可超过数据库连接上限。合理控制连接池的⼤⼩,限制连接数资源

费。

1. 【强制】开发⼈员及应⽤程序只给予 readWrite 权限。

2. 【建议】MongoDB Driver可通过设置的 Read Preference 来将读请求路由到其他的节点。

3. 【强制】密码中不要设有 @

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AustinDatabases 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档