这几乎可以成为MongoDB常见问题解答,主要因为这是一个真实的例子,展示了如何从SQL处理方式转变思维,并接受像MongoDB这样的引擎所做的事情。
基本原则是“MongoDB不使用连接(join)”。无论如何,“构建SQL查询”的任何方式都需要执行“连接”操作。典型的形式是“UNION”,实际上就是一种“连接”。
那么在不同的范式下如何实现呢?首先,让我们看看如何
不做以及理解原因(即使当然它对你个别的小样本数据有效):
困难模式
db.docs.aggregate([
{ "$group": {
"_id": null,
"age": { "$push": "$age" },
"childs": { "$push": "$childs" }
}},
{ "$unwind": "$age" },
{ "$group": {
"_id": "$age",
"count": { "$sum": 1 },
"childs": { "$first": "$childs" }
}},
{ "$sort": { "_id": -1 } },
{ "$group": {
"_id": null,
"age": { "$push": {
"value": "$_id",
"count": "$count"
}},
"childs": { "$first": "$childs" }
}},
{ "$unwind": "$childs" },
{ "$group": {
"_id": "$childs",
"count": { "$sum": 1 },
"age": { "$first": "$age" }
}},
{ "$sort": { "_id": -1 } },
{ "$group": {
"_id": null,
"age": { "$first": "$age" },
"childs": { "$push": {
"value": "$_id",
"count": "$count"
}}
}}
])
这将会给你一个类似这样的结果:
{
"_id" : null,
"age" : [
{
"value" : "50",
"count" : 1
},
{
"value" : "40",
"count" : 3
}
],
"childs" : [
{
"value" : "2",
"count" : 3
},
{
"value" : "1",
"count" : 1
}
]
}
所以这为什么不好?主要问题应该在第一个管道阶段显而易见:
{ "$group": {
"_id": null,
"age": { "$push": "$age" },
"childs": { "$push": "$childs" }
}},
我们需要做的是将集合中我们想要的值分组并将这些结果用
$push
推入数组中。当数据较小时,这种方法可行,但真实世界中的大型集合会导致管道中出现“单个文档”,其大小超过所允许的16MB BSON限制,这是不好的。
其余逻辑按照每个数组的自然顺序进行操作。但是在真实世界的情况下,这几乎总是不可行的。
您可以通过执行诸如将文档“复制”为“年龄”或“子代”类型,并逐个类型分组的操作来避免这种情况。但这有点“过于复杂”,也不是一种稳定的做法。
自然的反应是“联合查询”,但由于MongoDB不执行“join”操作,那么该如何解决呢?
更好的方法(又名新的希望)
从架构和性能角度考虑,您最好的方法是通过客户端API向服务器“同时”提交“两个”查询(是的,两个)。当接收到结果后,您可以将它们“合并”成一个单独的响应,然后将其作为数据源发送回最终的“客户端”应用程序。
不同的编程语言对此有不同的处理方式,但通常情况下,您需要寻找一个“异步处理”API,以便同时执行这些操作。
在这里,我的示例目的使用
node.js
作为“异步”部分基本上是“内置的”,并且相对直观。事情的“组合”部分可以是任何类型的“哈希/映射/字典”表实现,只是简单地进行示例:
var async = require('async'),
MongoClient = require('mongodb');
MongoClient.connect('mongodb://localhost/test',function(err,db) {
var collection = db.collection('docs');
async.parallel(
[
function(callback) {
collection.aggregate(
[
{ "$group": {
"_id": "$age",
"type": { "$first": { "$literal": "age" } },
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": -1 } }
],
callback
);
},
function(callback) {
collection.aggregate(
[
{ "$group": {
"_id": "$childs",
"type": { "$first": { "$literal": "childs" } },
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": -1 } }
],
callback
);
}
],
function(err,results) {
if (err) throw err;
var response = {};
results.forEach(function(res) {
res.forEach(function(doc) {
if ( !response.hasOwnProperty(doc.type) )
response[doc.type] = [];
response[doc.type].push({
"value": doc._id,
"count": doc.count
});
});
});
console.log( JSON.stringify( response, null, 2 ) );
}
);
});
这将会得到可爱的结果:
{
"age": [
{
"value": "50",
"count": 1
},
{
"value": "40",
"count": 3
}
],
"childs": [
{
"value": "2",
"count": 3
},
{
"value": "1",
"count": 1
}
]
}
所以需要注意的关键点是,“分离”聚合语句本身其实很简单。你需要做的就是将它们结合在最终结果中。有许多方法可以“结合”,特别是处理每个查询的大结果时,但这是执行模型的基本示例。
重点如下:
- 对于大数据集,聚合管道中的数据洗牌是可能的,但不够高效。
- 使用支持“并行”和“异步”执行的语言实现和API,这样您就可以一次性“加载”所有或“大部分”操作。
- API 应支持某种“组合”方法,或者允许单独的“流式”写入将收到的每个结果集处理为一个结果。
- 忘记 SQL 的方式。NoSQL 的方式将诸如“连接”之类的处理委托给“数据逻辑层”,该层包含了此处所示的代码。它采用这种方式是因为它可扩展到非常大的数据集。在大型应用程序中,你的“数据逻辑”处理节点的工作是将其传递给最终 API。
- 这比我可能描述的任何其他形式的“操纵”都要快。 “NoSQL” 思考的一部分是“忘掉你所学的”,以不同的方式看待事物。如果这种方式表现不佳,则坚持 SQL 方法进行存储和查询。
- 这就是为什么存在替代方案的原因。