介绍
这是我们的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
};
}
测试新数据模型
我们将简单地重用我们先前构建的测试框架,并且对检查两个用例感兴趣:
- 预加载数据和索引
- 预加载工作集
预加载数据和索引
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:
预加载工作集
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替代方法进行比较。 它们仅在将原型版本与优化的数据模型备选方案进行比较时才有用,因此我们可以了解数据建模如何影响整体应用程序性能。
- 代码可在GitHub上获得 。
参考: MongoDB和我们的JCG合作伙伴 Vlad Mihalcea在Vlad Mihalcea的Blog博客上提出的数据建模的精湛技巧 。
翻译自: https://www.javacodegeeks.com/2014/01/mongodb-and-the-fine-art-of-data-modelling.html