Mongo时间戳转日期以及日期分组

Posted 耳冉鹅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mongo时间戳转日期以及日期分组相关的知识,希望对你有一定的参考价值。

最近遇到的一个数据统计折线图的性能优化点,可以说是一定思维上的转变,就记录下咯
背景: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: <dateExpression>,
    format: <formatString>,
    timezone: <tzExpression>,
    onNull: <expression>
  


  • 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: <expression>, // Group key
      <field1>:  <accumulator1> : <expression1> ,
      ...
    
 
  • _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,
	

完结撒花🎉

以上是关于Mongo时间戳转日期以及日期分组的主要内容,如果未能解决你的问题,请参考以下文章

SQLServer时间戳转日期格式(13位时间戳)

你如何从 Mongo 日期字段中获取 DayHours?

你如何从 Mongo 日期字段中获取 DayHours?

日期转时间戳,时间戳转日期

js时间戳转日期

js 时间戳转日期时间