前言
最近遇到的一个数据统计折线图的性能优化点,可以说是一定思维上的转变,就记录下咯
背景:cron定时任务读取当前统计数据的异常值,频率为每五分钟记录一次,折线图要求获取每日的异常项峰值
最一开始的想法:将数据读取到内存中进行条件过滤、计算
首先根据时间戳将数据以日期作为分组,其次在每个分组中获取异常项的峰值数据,时间复杂度O(n*n),最好以日期分组列表+峰值数据列表作为对象返回结果
遇到性能问题:一天的数据量为(60/5)*24
=288,默认日期为15天,则统计的数据量为4230,接口返回甚至需要8、9秒的时间,作为一个项目的门面折线图,这种情况 达咩!
优化的念头:我要拿每天的峰值数据,怎么才能直接取到每天的峰值呢,mongo的聚合是不是可以做到啊? $group可以按日期做分组, $max可以拿到最大值,接下来一个 $sort好像是就成了吧! 说干就干!!
接下来的聚合语句均为mongo pipeline,最后附上golang的bson条件哈
// ResultCountModel _ type ResultCountModel struct { CommonBase `json:",inline" yaml:",inline" bson:",inline"` ErrorCount int `json:"error_count" bson:"error_count"` Timestamp int64 `json:"timestamp" bson:"timestamp"` MaxTime int64 `json:"max_time" bson:"max_time"` }
数据结构定义如上,这里使用CommonBase
,是因为在$group聚合后会得到_id
唯一标识字段,因此便于获取最后的聚合结果,在定义结构体时将其加上;timestamp
单位为毫秒
1、日期筛选
第一步,毫无疑问,对时间戳timestamp
进行日期的过滤
{ $match: { timestamp: { $gte: 1671897600000, // min_timestamp $lt: 1673280000000 // max_timestamp } } }
-
$gte
大于等于 -
$lt
小于
2、日期转换
第二步,根据时间戳大小进行日期的转换,这里是用的是$project, 将具有请求字段的文档传递到管道中的下一阶段。指定的字段可以是输入文档中的现有字段或是新计算的字段
使用$project主要思路是,将timestamp
时间戳转换为标准日期,之后输出为想要的format形式;同时使用 $project保留需要的字段
时间戳转换日期
核心方法:$dateToString
{ $dateToString: { date: , format: , timezone: , onNull: } }
-
date
:要转换的字符串日期,必须是解析为Date、Timestamp、ObjectID 的有效表达式 -
format
: 日期格式规范 -
timezone
:运算结果的时区,常用UTC偏移量 -
onNull
: date为null或缺失时要返回的值
日期格式想要“月份-日期”,那format: “%m-%d”
日期数据这里,如果直接使用输入文档中的现有字段的话 date: “$timestamp”,则会报错:PlanExecutor error during aggregation :: caused by :: can’t convert from BSON type long to Date
因此我们需要将时间戳转换为日期: 即格林威治开始时间(1970-01-01 00:00:00)+时间戳+时差
date:{ $add:[ new Date(0), "$timestamp", 28800000 ] },
注意⚠️:
- MongoDB时间的基本单位为毫秒,所以本文直接使用”$timestamp”即可;若时间单位为秒级时,则需要使用 $multiply进行乘法运算:{ $multiply:[” $timestamp”,1000]}
- MongoDB是UTC时区,即中时区(0度经线), 中国为东八区,因此需要使用timezone添加8小时(即28800000毫秒)
pipeline如下:
day:{ $dateToString:{ format:"%m-%d", date:{ $add:[ new Date(0), "$timestamp", 28800000 ] }, } },
保留需要字段
/** * specifications: The fields to * include or exclude. */ { timestamp:1, error_count:1, }
$project将保留字段置为1即可进行数据保留操作
第二步完整pipeline如下:
{ $project: { day: { $dateToString: { format: '%m-%d', date: { $add: [ ISODate('1970-01-01T00:00:00.000Z'), '$timestamp', 28800000 ] } } }, timestamp: 1, error_count: 1 } },
3、日期分组
第三步,使用$group进行日期分组
{ $group: { _id: , // Group key : { : }, ... } }
-
_id
: 表达式指定组密钥 -
field
: 计算使用的累加器运算符
这里我们需要将第二步获得的日期转换进行分组聚合,同时获取每个分组的异常项最大值即峰值数据
{ $group: { _id: '$day', error_count: { $max: '$error_count' }, max_time: { $max: '$timestamp' } } },
这里额外获取了max_time
字段,主要用于在计算统计数据时的排序,在最后排序部分会使用到
4、日期排序
这里做一个假设,如果不使用max_time
的话,如何将数据进行按日期的排序呢? 如果根据_id
进行排序,则会出现“上年末”排序在“下年初”的情况(感谢现在的📅,不然会忘记这个问题)
所以将每个分组的最大时间戳保留下来时很有必要的!
这里取$max $min都是可以的哈
{ $sort: { max_time: 1 } }
最终完整pipeline:
[{ $match: { timestamp: { $gte: 1671897600000, $lt: 1673280000000 } } }, { $project: { day: { $dateToString: { format: '%m-%d', date: { $add: [ ISODate('1970-01-01T00:00:00.000Z'), '$timestamp', 28800000 ] } } }, timestamp: 1, error_count: 1 } }, { $group: { _id: '$day', error_count: { $max: '$error_count' }, max_time: { $max: '$timestamp' } } }, { $sort: { max_time: 1 } }] [{ $match: { timestamp: { $gte: 1671897600000, $lt: 1673280000000 } } }, { $project: { day: { $dateToString: { format: '%m-%d', date: { $add: [ ISODate('1970-01-01T00:00:00.000Z'), '$timestamp', 28800000 ] } } }, timestamp: 1, error_count: 1 } }, { $group: { _id: '$day', error_count: { $max: '$error_count' }, max_time: { $max: '$timestamp' } } }, { $sort: { max_time: 1 } }]
在golang里面,Aggregate则直接使用pipeline即可,亦可转换为filter使用
filter代码:
filter := bson.A{ bson.D{{"$match", bson.D{ {"timestamp", bson.D{ {"$gte", param.MinTimestamp}, {"$lt", param.MaxTimestamp}, }}}}, }, bson.D{{"$project", bson.D{ {"day", bson.D{ {"$dateToString", bson.D{ {"format", "%m-%d"}, {"date", bson.D{ {"$add", bson.A{ time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), "$timestamp", 28800000, }}, }}, }}, }}, {"error_count", 1}, {"timestamp", 1}, }}}, bson.D{{"$group", bson.D{ {"_id", "$day"}, {"max_time", bson.D{{"$max", "$timestamp"}}}, {"error_count", bson.D{{"$max", "$error_count"}}}, }}}, bson.D{{"$sort", bson.D{{"max_time", 1}}}}, }
完结撒花🎉
补充:解决MongoDB存储时间时差的问题
MongoDB存储时间类型数据时,都是先转换为UTC时间,然后存储到数据库中,当我们取出存储的时间时,就会出现时差的问题。比如我们用的北京时间,读取到的数值就会看到比当前时间少了8个小时,难道说我们在每次读取的时候都要单独处理一下时间吗,这就比较麻烦。其实,我们可以在存储的时候进行相应的处理,只需使用getTimezoneOffset()和toISOString()函数。
getTimezoneOffset函数:返回此地区的时差(当地时间与GMT格林威治标准时间的地区时差),单位为分钟。
// 我们是东八区 var d = new Date(); var tz = d.getTimezoneOffset(); console.log(tz); // -480
toISOString()函数:使用ISO标准将 Date 对象转换为字符串。
该标准称为 ISO-8601 ,格式为: YYYY-MM-DDTHH:mm:ss.sssZ。
封装时间转换函数
localDate(v) { const d = new Date(v || Date.now()); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString(); },
我们在存储时间的时候调用localDate()这个函数就可以了,无论你处在哪个时区结果显示都和当地时间一样。
总结
到此这篇关于MongoDB时间戳转日期及日期分组的文章就介绍到这了,更多相关MongoDB时间戳转日期内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!