在之前的文章中,我谈到了批处理导入和开箱即用的MongoDB性能 。 同时,MongoDB被授予年度DBMS奖 ,因此我决定对它的实际使用情况进行更全面的分析。
因为在实际环境中可以更好地理解理论,所以我将首先向您介绍我们的虚拟项目要求。
介绍
我们的虚拟项目具有以下要求:
- 它必须存储表示为v = f(t)的有价值的时间事件
- 一分钟内
- 一小时内
- 一天中的几个小时
- 一年中的天数
- 一分钟汇总中的秒数是实时计算的(因此必须非常快)
- 所有其他聚合均由批处理程序计算(因此它们必须相对较快)
资料模型
我将提供两个数据建模变体,每个变体各有利弊。
- 第一个版本使用默认的自动分配的MongoDB “ _id” ,这简化了插入操作,因为我们可以分批完成它,而不必担心任何时间戳冲突。
如果每毫秒记录10个值,那么我们最终将拥有10个不同的文档。 这篇文章将讨论这个数据模型选项。
{
"_id" : ObjectId("52cb898bed4bd6c24ae06a9e"),
"created_on" : ISODate("2012-11-02T01:23:54.010Z")
"value" : 0.19186609564349055
}
- 第二个版本使用自纪元以来的毫秒数作为“ _id”字段,并且值存储在“值”数组中。
如果每毫秒记录10个值,那么我们最终将得到一个不同的文档,其中“ values”数组中包含10个条目。 以后的文章将专门介绍这种压缩数据模型。
{
"_id" : 1348436178673,
"values" : [
0.7518879524432123,
0.0017396819312125444
]
}
插入资料
就像我以前的文章一样,我将使用5000万个文档来测试聚合逻辑。 我选择此数字是因为我正在商用PC上进行测试。 在前面提到的帖子中,我设法每秒插入超过80000个文档。 这次,我将采用更实际的方法,并在插入数据之前先创建集合和索引。
MongoDB shell version: 2.4.6
connecting to: random
> db.dropDatabase()
{ "dropped" : "random", "ok" : 1 }
> db.createCollection("randomData");
{ "ok" : 1 }
> db.randomData.ensureIndex({"created_on" : 1});
> db.randomData.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"ns" : "random.randomData",
"name" : "_id_"
},
{
"v" : 1,
"key" : {
"created_on" : 1
},
"ns" : "random.randomData",
"name" : "created_on_1"
}
]
现在是时候插入50M文档了。
mongo random --eval "var arg1=50000000;arg2=1" create_random.js
...
Job#1 inserted 49900000 documents.
Job#1 inserted 50000000 in 2852.56s
这次我们设法每秒导入17500个文档。 以这样的速度,我们每年需要550B条目,对于我们的用例而言,这绰绰有余。
压缩数据
首先,我们需要分析集合统计信息,为此,我们需要使用stats命令:
db.randomData.stats()
{
"ns" : "random.randomData",
"count" : 50000000,
"size" : 3200000096,
"avgObjSize" : 64.00000192,
"storageSize" : 5297451008,
"numExtents" : 23,
"nindexes" : 2,
"lastExtentSize" : 1378918400,
"paddingFactor" : 1,
"systemFlags" : 1,
"userFlags" : 0,
"totalIndexSize" : 3497651920,
"indexSizes" : {
"_id_" : 1623442912,
"created_on_1" : 1874209008
},
"ok" : 1
}
当前索引大小几乎是3.5GB,几乎是我可用RAM的一半。 幸运的是,MongoDB附带了一个紧凑的命令,我们可以使用该命令对数据进行碎片整理。 这会花费很多时间,特别是因为我们的索引总大小很大。
db.randomData.runCommand("compact");
Compacting took 1523.085s
让我们看看通过压缩节省了多少空间:
db.randomData.stats()
{
"ns" : "random.randomData",
"count" : 50000000,
"size" : 3200000032,
"avgObjSize" : 64.00000064,
"storageSize" : 4415811584,
"numExtents" : 24,
"nindexes" : 2,
"lastExtentSize" : 1149206528,
"paddingFactor" : 1,
"systemFlags" : 1,
"userFlags" : 0,
"totalIndexSize" : 2717890448,
"indexSizes" : {
"_id_" : 1460021024,
"created_on_1" : 1257869424
},
"ok" : 1
}
我们释放了将近800MB的数据,这对于我们的RAM密集型聚合操作非常方便。
解释聚合逻辑
所有四个汇总报告都是相似的,只是它们之间的区别在于:
- 选择时间间隔
- 按时间粒度分组
因此,我们可以从第一个报告开始,该报告按秒汇总值。 我们将使用explain方法来了解聚合的内部工作情况。
load(pwd() + "/../../util/date_util.js");
var minDate = new Date(Date.UTC(2012, 1, 10, 11, 25, 30));
var maxDate = new Date(Date.UTC(2012, 1, 10, 11, 25, 35));
var result = db.randomData.runCommand('aggregate', { pipeline:
[
{
$match: {
"created_on" : {
$gte: minDate,
$lt : maxDate
}
}
},
{
$project: {
_id : 0,
created_on : 1,
value : 1
}
},
{
$group: {
"_id": {
"year" : {
$year : "$created_on"
},
"dayOfYear" : {
$dayOfYear : "$created_on"
},
"hour" : {
$hour : "$created_on"
},
"minute" : {
$minute : "$created_on"
},
"second" : {
$second : "$created_on"
},
},
"count": {
$sum: 1
},
"avg": {
$avg: "$value"
},
"min": {
$min: "$value"
},
"max": {
$max: "$value"
}
}
},
{
$sort: {
"_id.year" : 1,
"_id.dayOfYear" : 1,
"_id.hour" : 1,
"_id.minute" : 1,
"_id.second" : 1
}
}
], explain: true});
printjson(result);
输出以下结果
{
"serverPipeline" : [
{
"query" : {
"created_on" : {
"$gte" : ISODate("2012-02-10T11:25:30Z"),
"$lt" : ISODate("2012-02-10T11:25:35Z")
}
},
"projection" : {
"created_on" : 1,
"value" : 1,
"_id" : 0
},
"cursor" : {
"cursor" : "BtreeCursor created_on_1",
"isMultiKey" : false,
"n" : 5,
"nscannedObjects" : 5,
"nscanned" : 5,
"nscannedObjectsAllPlans" : 5,
"nscannedAllPlans" : 5,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"created_on" : [
[
ISODate("2012-02-10T11:25:30Z"),
ISODate("2012-02-10T11:25:35Z")
]
]
},
"allPlans" : [
{
"cursor" : "BtreeCursor created_on_1",
"n" : 5,
"nscannedObjects" : 5,
"nscanned" : 5,
"indexBounds" : {
"created_on" : [
[
ISODate("2012-02-10T11:25:30Z"),
ISODate("2012-02-10T11:25:35Z")
]
]
}
}
],
"oldPlan" : {
"cursor" : "BtreeCursor created_on_1",
"indexBounds" : {
"created_on" : [
[
ISODate("2012-02-10T11:25:30Z"),
ISODate("2012-02-10T11:25:35Z")
]
]
}
},
"server" : "VLAD:27017"
}
},
{
"$project" : {
"_id" : false,
"created_on" : true,
"value" : true
}
},
{
"$group" : {
"_id" : {
"year" : {
"$year" : [
"$created_on"
]
},
"dayOfYear" : {
"$dayOfYear" : [
"$created_on"
]
},
"hour" : {
"$hour" : [
"$created_on"
]
},
"minute" : {
"$minute" : [
"$created_on"
]
},
"second" : {
"$second" : [
"$created_on"
]
}
},
"count" : {
"$sum" : {
"$const" : 1
}
},
"avg" : {
"$avg" : "$value"
},
"min" : {
"$min" : "$value"
},
"max" : {
"$max" : "$value"
}
}
},
{
"$sort" : {
"sortKey" : {
"_id.year" : 1,
"_id.dayOfYear" : 1,
"_id.hour" : 1,
"_id.minute" : 1,
"_id.second" : 1
}
}
}
],
"ok" : 1
}
聚合框架使用管道和过滤器设计模式,我们的管道包括以下操作:
- 匹配 :此操作与WHERE SQL子句相似,这是自使用“ created_on”索引以来我们使用的第一个子句(例如,已通过解释结果确认: “ cursor”:“ BtreeCursor created_on_1” ,) 。 我们没有使用coverage -index(例如, “ indexOnly”:false ),因为这对于我们的8GB RAM设置来说是过分的。
- 项目 :此操作类似于SELECT SQL子句,用于从工作集中删除“ _id”字段(这对我们的报告逻辑没有用)。
- Group :此操作类似于GROUP BY SQL子句,并且它在内存中进行所有计算。 这就是为什么我们在分组之前过滤工作集的原因。
- Sort :此操作类似于ORDER BY SQL子句,我们使用它按时间顺序对结果进行排序。
基本聚合脚本
由于我们的四个报告相似,因此我们可以将所有逻辑分组在一个脚本中:
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 groupBy = {
"year" : {
$year : "$created_on"
},
"dayOfYear" : {
$dayOfYear : "$created_on"
}
};
var sortBy = {
"_id.year" : 1,
"_id.dayOfYear" : 1
};
var appendSeconds = false;
var appendMinutes = false;
var appendHours = false;
switch(groupDeltaMillis) {
case ONE_SECOND_MILLIS :
appendSeconds = true;
case ONE_MINUTE_MILLIS :
appendMinutes = true;
case ONE_HOUR_MILLIS :
appendHours = true;
}
if(appendHours) {
groupBy["hour"] = {
$hour : "$created_on"
};
sortBy["_id.hour"] = 1;
}
if(appendMinutes) {
groupBy["minute"] = {
$minute : "$created_on"
};
sortBy["_id.minute"] = 1;
}
if(appendSeconds) {
groupBy["second"] = {
$second : "$created_on"
};
sortBy["_id.second"] = 1;
}
var pipeline = [
{
$match: {
"created_on" : {
$gte: fromDate,
$lt : toDate
}
}
},
{
$project: {
_id : 0,
created_on : 1,
value : 1
}
},
{
$group: {
"_id": groupBy,
"count": {
$sum: 1
},
"avg": {
$avg: "$value"
},
"min": {
$min: "$value"
},
"max": {
$max: "$value"
}
}
},
{
$sort: sortBy
}
];
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
};
}
取得成果的时间
让我们使用以下脚本测试前三个报告:
load(pwd() + "/../../util/date_util.js");
load(pwd() + "/aggregate_base_report.js");
var deltas = [
{
matchDeltaMillis: ONE_MINUTE_MILLIS,
groupDeltaMillis: ONE_SECOND_MILLIS,
description: "Aggregate all seconds in a minute"
},
{
matchDeltaMillis: ONE_HOUR_MILLIS,
groupDeltaMillis: ONE_MINUTE_MILLIS,
description: "Aggregate all minutes in an hour"
},
{
matchDeltaMillis: ONE_DAY_MILLIS,
groupDeltaMillis: ONE_HOUR_MILLIS,
description: "Aggregate all hours in a day"
}
];
var testFromDate = new Date(Date.UTC(2012, 5, 10, 11, 25, 59));
deltas.forEach(function(delta) {
print('Aggregating ' + description);
var timeInterval = calibrateTimeInterval(testFromDate, delta.matchDeltaMillis);
var fromDate = timeInterval.fromDate;
var toDate = timeInterval.toDate;
aggregateData(fromDate, toDate, delta.groupDeltaMillis, true);
});
给我们以下结果:
MongoDB shell version: 2.4.6
connecting to: random
Aggregating Aggregate all seconds in a minute
Aggregating from Sun Jun 10 2012 14:25:00 GMT+0300 (GTB Daylight Time) to Sun Jun 10 2012 14:26:00 GMT+0300 (GTB Daylight Time)
Fetched :45 documents.
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 11,
"minute" : 25,
"second" : 0
},
"count" : 1,
"avg" : 0.4924355132970959,
"min" : 0.4924355132970959,
"max" : 0.4924355132970959
}
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 11,
"minute" : 25,
"second" : 1
},
"count" : 1,
"avg" : 0.10043778014369309,
"min" : 0.10043778014369309,
"max" : 0.10043778014369309
}
...
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 11,
"minute" : 25,
"second" : 59
},
"count" : 1,
"avg" : 0.16304525500163436,
"min" : 0.16304525500163436,
"max" : 0.16304525500163436
}
Aggregating from Sun Jun 10 2012 14:00:00 GMT+0300 (GTB Daylight Time) to Sun Jun 10 2012 15:00:00 GMT+0300 (GTB Daylight Time)
Fetched :60 documents.
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 11,
"minute" : 0
},
"count" : 98,
"avg" : 0.4758610369979727,
"min" : 0.004005654249340296,
"max" : 0.9938081130385399
}
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 11,
"minute" : 1
},
"count" : 100,
"avg" : 0.5217278444720432,
"min" : 0.003654648782685399,
"max" : 0.9981840122491121
}
...
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 11,
"minute" : 59
},
"count" : 92,
"avg" : 0.5401836506308705,
"min" : 0.01764239347539842,
"max" : 0.9997266652062535
}
Aggregating Aggregate all hours in a day
Aggregating from Sun Jun 10 2012 03:00:00 GMT+0300 (GTB Daylight Time) to Mon Jun 11 2012 03:00:00 GMT+0300 (GTB Daylight Time)
Fetched :24 documents.
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 0
},
"count" : 5727,
"avg" : 0.4975644027204364,
"min" : 0.00020139524713158607,
"max" : 0.9997993060387671
}
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 1
},
"count" : 5799,
"avg" : 0.49519448930962623,
"min" : 0.00011728447861969471,
"max" : 0.9999530822969973
}
...
{
"_id" : {
"year" : 2012,
"dayOfYear" : 162,
"hour" : 23
},
"count" : 5598,
"avg" : 0.49947314951339256,
"min" : 0.00009276834316551685,
"max" : 0.9999523421283811
}
请继续关注,我的下一篇文章将向您展示如何优化这些聚合查询。
- 代码可在GitHub上获得 。
参考: MongoDB时间序列:在Vlad Mihalcea的Blog博客中,我们的JCG合作伙伴 Vlad Mihalcea 介绍了聚合框架 。
翻译自: https://www.javacodegeeks.com/2014/01/mongodb-time-series-introducing-the-aggregation-framework.html