MongoDB和数据建模的精湛技巧

介绍

这是我们的MongoDB时间序列教程的第三部分,本文将强调数据建模的重要性。 您可能需要检查本系列的第一部分 ,以熟悉我们的虚拟项目要求,而第二部分讨论常见的优化技术。

首次开始使用MongoDB时,您会立即注意到它是无模式的数据模型。 但是,没有架构的情况并不意味着跳过适当的数据建模(满足您的应用程序业务和性能要求)。 与SQL数据库相反,NoSQL文档模型更侧重于查询,而不是数据规范化。 这就是为什么除非您解决数据查询模式,否则您的设计不会完成的原因。

新数据模型

我们之前的时间事件是这样建模的:

{
	"_id" : ObjectId("52cb898bed4bd6c24ae06a9e"),
	"created_on" : ISODate("2012-11-02T01:23:54.010Z")
	"value" : 0.19186609564349055
}

我们得出的结论是,ObjectId对我们不利,因为它的索引大小约为1.4GB,并且我们的数据聚合逻辑根本不使用它。 拥有它的唯一真正好处是可以使用批量插入

先前的解决方案是使用“日期”字段存储事件创建时间戳。 这影响了聚合分组逻辑,最终形成以下结构:

"_id" : {
	"year" : {
		"$year" : [
			"$created_on"
		]
	},
	"dayOfYear" : {
		"$dayOfYear" : [
			"$created_on"
		]
	},
	"hour" : {
		"$hour" : [
			"$created_on"
		]
	},
	"minute" : {
		"$minute" : [
			"$created_on"
		]
	},
	"second" : {
		"$second" : [
			"$created_on"
		]
	}
}

这个组_id需要一些应用程序逻辑来获取正确的JSON日期。 我们还可以将created_on Date字段更改为一个数字值,表示自Unix纪元以来的毫秒数。 这可以成为我们的新文档_id (无论如何默认情况下都会对其进行索引)。

这是我们的新文档结构如下所示:

{ 
        "_id" : 1346895603146, 
        "values" : [ 0.3992688732687384 ] 
}
{
        "_id" : 1348436178673,
        "values" : [
                0.7518879524432123,
                0.0017396819312125444
        ]
}

现在,我们可以轻松地从Unix时间戳中提取时间戳参考(指向当前的秒,分钟,小时或天)。

因此,如果当前时间戳为1346895603146(2012年9月6日星期四,格林尼治标准时间146ms),我们可以提取:

- the current second time point [Thu, 06 Sep 2012 01:40:03 GMT]: 1346895603000 = (1346895603146 – (1346895603146 % 1000))
- the current minute time point [Thu, 06 Sep 2012 01:40:00 GMT] : 1346895600000 = (1346895603146 – (1346895603146 % (60 * 1000)))
- the current hour time point [Thu, 06 Sep 2012 01:00:00 GMT] : 1346893200000 = (1346895603146 – (1346895603146 % (60 * 60 * 1000)))
- the current day time point [Thu, 06 Sep 2012 00:00:00 GMT] : 1346889600000= (1346895603146 – (1346895603146 % (24 * 60 * 60 * 1000)))

该算法非常简单,我们可以在计算聚合组标识符时使用它。

这种新的数据模型使我们每个时间戳可以拥有一个文档。 每个时间事件都会在“值”数组中附​​加一个新值,因此,在同一时刻发生的两个事件将共享同一MongoDB文档。

插入测试数据

所有这些变化需要改变我们使用导入脚本之前 。 这次我们不能使用批处理插入,我们将采用更实际的方法。 这次,我们将使用以下示例中的非批处理upsert

var minDate = new Date(2012, 0, 1, 0, 0, 0, 0);
var maxDate = new Date(2013, 0, 1, 0, 0, 0, 0);
var delta = maxDate.getTime() - minDate.getTime();

var job_id = arg2;

var documentNumber = arg1;
var batchNumber = 5 * 1000;

var job_name = 'Job#' + job_id
var start = new Date();

var index = 0;

while(index < documentNumber) {
	var date = new Date(minDate.getTime() + Math.random() * delta);
	var value = Math.random();	
	db.randomData.update( { _id: date.getTime() }, { $push: { values: value } }, true );	
	index++;
	if(index % 100000 == 0) {	
		print(job_name + ' inserted ' + index + ' documents.');
	}
}
print(job_name + ' inserted ' + documentNumber + ' in ' + (new Date() - start)/1000.0 + 's');

现在是时候插入50M文档了。

Job#1 inserted 49900000 documents.
Job#1 inserted 50000000 documents.
Job#1 inserted 50000000 in 4265.45s

插入5000万个条目比以前的版本慢,但是在没有任何写优化的情况下,我们仍然可以每秒获得1万次插入。 出于此测试的目的,我们假设每毫秒10个事件就足够了,考虑到这样的速率,我们最终每年将有3150亿个文档。

压缩数据

现在,让我们检查新的收集状态:

db.randomData.stats();
{
        "ns" : "random.randomData",
        "count" : 49709803,
        "size" : 2190722612,
        "avgObjSize" : 44.070233229449734,
        "storageSize" : 3582234624,
        "numExtents" : 24,
        "nindexes" : 1,
        "lastExtentSize" : 931495936,
        "paddingFactor" : 1.0000000000429572,
        "systemFlags" : 1,
        "userFlags" : 0,
        "totalIndexSize" : 1853270272,
        "indexSizes" : {
                "_id_" : 1853270272
        },
        "ok" : 1
}

文档大小从64字节减少到44字节,这一次我们只有一个索引。 如果使用compact命令,我们甚至可以进一步减小集合大小。

db.randomData.runCommand("compact");
{
        "ns" : "random.randomData",
        "count" : 49709803,
        "size" : 2190709456,
        "avgObjSize" : 44.06996857340191,
        "storageSize" : 3267653632,
        "numExtents" : 23,
        "nindexes" : 1,
        "lastExtentSize" : 851263488,
        "paddingFactor" : 1.0000000000429572,
        "systemFlags" : 1,
        "userFlags" : 0,
        "totalIndexSize" : 1250568256,
        "indexSizes" : {
                "_id_" : 1250568256
        },
        "ok" : 1
}

基本聚合脚本

现在是时候构建基本的聚合脚本了:

function printResult(dataSet) {
	dataSet.result.forEach(function(document)  {
		printjson(document);
	});
}

function aggregateData(fromDate, toDate, groupDeltaMillis, enablePrintResult) {		

	print("Aggregating from " + fromDate + " to " + toDate);

	var start = new Date();

	var pipeline = [
		{
			$match:{
				"_id":{
					$gte: fromDate.getTime(), 
					$lt : toDate.getTime()	
				}
			}
		},
		{
			$unwind:"$values"
		},
		{
			$project:{         
				timestamp:{
					$subtract:[
					   "$_id", {
						  $mod:[
							"$_id", groupDeltaMillis
						  ]
					   }
					]
				},
				value : "$values"
			}
		},
		{
			$group: {
				"_id": {
					"timestamp" : "$timestamp"
				}, 
				"count": { 
					$sum: 1 
				}, 
				"avg": { 
					$avg: "$value" 
				}, 
				"min": { 
					$min: "$value" 
				}, 
				"max": { 
					$max: "$value" 
				}		
			}
		},
		{
			$sort: {
				"_id.timestamp" : 1		
			}
		}
	];

	var dataSet = db.randomData.aggregate(pipeline);
	var aggregationDuration = (new Date().getTime() - start.getTime())/1000;	
	print("Aggregation took:" + aggregationDuration + "s");	
	if(dataSet.result != null && dataSet.result.length > 0) {
		print("Fetched :" + dataSet.result.length + " documents.");
		if(enablePrintResult) {
			printResult(dataSet);
		}
	}
	var aggregationAndFetchDuration = (new Date().getTime() - start.getTime())/1000;
	if(enablePrintResult) {
		print("Aggregation and fetch took:" + aggregationAndFetchDuration + "s");
	}	
	return {
		aggregationDuration : aggregationDuration,
		aggregationAndFetchDuration : aggregationAndFetchDuration
	};
}

测试新数据模型

我们将简单地重用我们先前构建的测试框架,并且对检查两个用例感兴趣:

  1. 预加载数据和索引
  2. 预加载工作集
预加载数据和索引
D:\wrk\vladmihalcea\vladmihalcea.wordpress.com\mongodb-facts\aggregator\timeseries>mongo random touch_index_data.js
MongoDB shell version: 2.4.6
connecting to: random
Touch {data: true, index: true} took 17.351s
类型 一分钟内 一小时内 一天中的几个小时
T1 0.012秒 0.044秒 0.99秒
T2 0.002秒 0.044秒 0.964秒
T3 0.001秒 0.043秒 0.947秒
T4 0.001秒 0.043秒 0.936秒
T4 0.001秒 0.043秒 0.907秒
平均 0.0034秒 0.0433秒 0.9488秒

与以前的版本相比,我们得到了更好的结果,这是可能的,因为我们现在可以预加载数据和索引,而不仅仅是数据。 整个数据和索引适合我们的8GB RAM:

mongodb_time_series_compact

预加载工作集
D:\wrk\vladmihalcea\vladmihalcea.wordpress.com\mongodb-facts\aggregator\timeseries>mongo random compacted_aggregate_year_report.js
MongoDB shell version: 2.4.6
connecting to: random
Aggregating from Sun Jan 01 2012 02:00:00 GMT+0200 (GTB Standard Time) to Tue Jan 01 2013 02:00:00 GMT+0200 (GTB Standard Time)
Aggregation took:307.84s
Fetched :366 documents.
类型 一分钟内 一小时内 一天中的几个小时
T1 0.003秒 0.037秒 0.855秒
T2 0.002秒 0.037秒 0.834秒
T3 0.001秒 0.037秒 0.835秒
T4 0.001秒 0.036秒 0.84秒
T4 0.002秒 0.036秒 0.851秒
平均 0.0018秒 0.0366秒 0.843秒

这是我们获得的最佳结果,我们可以使用这个新的数据模型,因为它已经满足了我们的虚拟项目性能要求。

结论

这是快还是慢?

这是一个您必须回答自己的问题。 性能是上下文限制的功能。 对于给定的业务案例而言,最快的事情对于另一个案例可能非常慢。

肯定有一件事。 它比现成的版本快六倍。

这些数字无意与任何其他NoSQL或SQL替代方法进行比较。 它们仅在将原型版本与优化的数据模型备选方案进行比较时才有用,因此我们可以了解数据建模如何影响整体应用程序性能。

参考: MongoDB和我们的JCG合作伙伴 Vlad Mihalcea在Vlad Mihalcea的Blog博客上提出的数据建模的精湛技巧

翻译自: https://www.javacodegeeks.com/2014/01/mongodb-and-the-fine-art-of-data-modelling.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值