开头还是介绍一下群,如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, OceanBase, Sql Server等有问题,有需求都可以加群群内有各大数据库行业大咖,可以解决你的问题。加群请联系 liuaustin3 ,(共2500人左右 1 + 2 + 3 + 4 +5 + 6 + 7 + 8)(1 2 3 4 5 群均已满,6群停止进人,新人进7群 8 群)
最近遇到一个关于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脚本,无需安装直接使用。
// 连接到 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,全部都是文字给出的时间,所以为了严谨,将他们的时间给进行了一个重塑,将文本变为了日期,在进行比对。
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 刷脏以及磁盘压力。另一个原因这样操作上面也写到了。
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. 【强制】密码中不要设有 @
置顶文章:
MongoDB 插入更新数据慢,开发问哪的问题?附带解决方案和脚本
用MySql不是MySQL, 不用MySQL都是MySQL 横批 哼哼哈哈啊啊
PostgreSQL 远程管理越来越简单,6个自动化脚本开胃菜
撕逼!PostgreSQL 和 MongoDB 开撕,MySQL却躺枪
MongoDB 系统IOPS 告警系统处于崩溃,优化语句从1秒优化到1毫秒解决问题
MongoDB 入门教学贴 从术语到操作 (约束怎么建立 内部培训贴)
MongoDB 谨献给说MongoDB 这不好那不好的“古董” -- 发展与演进,从3 到 7 的卓越变化
专访唐建法-从MongoDB中国第一人到TapData掌门人的故事
往期热门文章:
微软 “爱” 上PostgreSQL, PG “嫁给” 微软!
PostgreSQL 软肋 “最大连接数” 到底是不是问题?
阿里云数据库--市场营销聊胜于无--3年的使用感受与反馈系列
阿里云数据库产品 对内对外一样的卷 --3年阿里云数据库的使用感受与反馈系列
阿里云数据库使用感受--客户服务问题深入剖析与什么是廉价客户 --3年的使用感受与反馈系列
阿里云数据库使用感受--操作界面有点眼花缭乱 --3年的使用感受与反馈系列
PolarDB 最近遇到加字段加不上的问题 与 使用PolarDB 三年感受与恳谈
PostgreSQL 稳定性平台 PG中文社区大会--杭州来去匆匆
MySQL 的SQL引擎很差吗?由一个同学提出问题引出的实验
临时工访谈:从国产数据库 到 普罗大众的产品 !与在美国创业软件公司老板对话
感谢 老虎刘 刘老师 对 5月20日 SQL 问题纠正贴 ---PostgreSQL 同一种SQL为什么这样写会提升45%性能
PostgreSQL 同一种SQL为什么这样写会提升45%性能 --程序员和DBA思维方式不同决定
PostgreSQL 熊灿灿一句话够学半个月 之 KILL -9
临时工访谈:庙小妖风大-PolarDB 组团镇妖 之 他们是第一 (阿里云组团PK笔者实录)
临时工访谈:金牌 “女” 销售从ORACLE 转到另类国产数据库 到底 为什么?
临时工访谈:无名氏意外到访-- 也祝你好运(管理者PUA DBA现场直播)