大数据查询mongodb、聚合、单索引或复合索引
big data query mongodb, aggregation, single index or compound index
我正在尝试加快对包含超过 1000 万个文档的集合执行的查询。文档示例如下所示
{
nMove: 2041242,
typeMove: 'Sold',
date: "2016-05-18T16:00:00Z",
operation: 'output',
origin: {
id: '3234fds32fds42',
name: 'Main storage',
},
products: [{
id: '342fmdsff23324432',
name: 'Product 1',
price: 34,
quantity: 9
}],
}
现在我必须查询与给定的 'product.id' 或 'origin.id' 或两者匹配的所有文档,并且 $sum 总共 product.quantity 的数量。
所以我正在执行这样的查询。
movesModel.aggregate([
{
$match: {
$expr: {
$and: [
{ $in: [req.params.idProduct, '$product.id'] },
{ $eq: ['$origin.id', req.params.idOrigin }] },
]
}
}
},
{
$project: {
_id: 0,
outputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'input'] }, then: '$product.quantity', else: 0 }
}
},
inputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'output'] }, then: '$product.quantity', else: 0 }
}
}
}
},
{
$group: {
_id: '$_id',
inputs: { $sum: '$inputs' },
outputs: { $sum: '$outputs' }
}
},
]).then((result) => {
res.json(result)
})
解决此查询大约需要 1 分钟...有时此查询 $match 超过 200k 个文档...考虑到我不需要全部数据,我只需要数量的总和...我有一些问题...(我是mongodb菜鸟)
关于索引.. 我创建了一个复合索引db.moves.createIndex({ 'origin.id': 1, 'product.id':1})。这是正确的吗?我应该改变它吗?
我的查询可以吗?我可以改进它吗?
为了防止查询与 200k 文档匹配...我做了一些棘手的事情。我添加了一个名为 'date' 的字段,我想获取所有与 'origin.id'、'product.id' 和 $gte: date 匹配的文档,但它需要相同的时间.. . 即使它只匹配 1 个文档...
完成...我认为我遇到的所有问题都与索引有关...所以我尝试检查我的 indexStats...但它似乎不适用于我的聚合查询.
感谢任何帮助。谢谢
////////////完整管道////////////
在这种情况下,我还有两个集合,分别称为 'storages' 和 'inventories'
//storage examples
{
_id: '3234fds32fds42'
name: 'Main storage'
status: true
}
{
_id: '32f32f32432sda'
name: 'Other storage'
status: true
}
//invetories examples
{
_id: 'fvavcsa3a3aa3'
date: '2020-01-01'
storage: {
_id: '3234fds32fds42'
name: 'Main storage'
}
products: [{
id: '342fmdsff23324432',
name: 'Product 1',
}],
}
所以这就是我使用 $lookup 的原因,我真正需要的是获取与每个存储和产品相匹配的所有移动。
//我还添加了库存以按日期过滤并防止匹配大量文档
这是我的查询。
storagesModel.aggregation([
{
$match: { status: true }
},
{
$lookup: {
from: 'inventories',
as: 'inventory',
let: { "idStorage": "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$storage._id', { $toString: "$$idStorage" }] },
{ $in: [req.params.id, '$products._id'] }
]
}
},
},
{
$sort: { date: -1 } // TO TAKE THE LAST INVENTORY
},
{
$limit: 1
}
]
}
},
{ $unwind: { path: '$inventories', preserveNullAndEmptyArrays: true } }, //DECONSTRUCT THE ARRAY AND GET IT AS OBJECT
{
$lookup: {
from: 'moves',
as: 'moves',
let: {
"idStorage": "$_id",
'date': '$inventory.date'},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $gte: ['$date', $$date] }
{ $eq: ['$origin.id', '$$idStorage' }] },
{ $in: [req.params.idProduct, '$product.id'] },
]
}
}
},
{
$project: {
_id: 0,
outputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'input'] }, then: '$product.quantity', else: 0 }
}
},
inputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'output'] }, then: '$product.quantity', else: 0 }
}
}
}
},
{
$group: {
_id: '$_id',
inputs: { $sum: '$inputs' },
outputs: { $sum: '$outputs' }
}
},
]
}
])
总结...
我需要得到的是产品在每个存储空间中的总 'moves'。
考虑到可能有一个 'inventory' 可以给你一个日期来防止匹配很多文件。这就是为什么我使用 storageModel 并使用 $lookup 阶段来获取最后的库存,所以我有 $$date 和 $$idStorage。然后我使用 'moves' 集合中的 $lookup...我知道这是一个繁重的查询但是...我认为给出日期和适当的复合索引应该很快...但即使我尝试为只有很少 'moves' 的产品获得 'moves'... 大约需要 20 或 30 秒...
我尝试在没有 $lookup 的情况下进行单个查询,甚至匹配 400k 个文档也需要 1-2 秒...
你觉得呢?感谢您的帮助
总的来说你做的很好,我们把你的观点总结一下,分开说。
是的,这太棒了,复合索引正是这个管道所需要的。我相信您在选择创建复合索引之前已经阅读过有关复合索引的内容,所以我不会深入探讨为什么这个索引是最佳的,因为它很简单。
这个就到最后吧。
这很好,如果您不关心整个数据样本而只关心最近的数据,这是可行的方法,现在为了正确利用该字段来加快性能,您应该转储我们在第 1 部分中讨论的旧索引并创建一个新的复合索引以包含此字段,{date: -1, 'origin.id': 1, 'product.id': 1}
请注意我们 select 日期的递减索引,因为我们需要最新的数据。这将使它更有效率。
由于您一定已经阅读过复合索引字段顺序问题,因此请随意更改此字段的顺序以匹配您最常执行的查询。
但是 Mongo 生成索引树的方式不太可能是不稳定的,我的意思是,如果您很久以前就创建了索引,并且从那时起有更多的数据进来,您可能会受益于删除并重建索引。话虽如此,我不推荐这样做,因为我觉得在你的情况下任何改进都会有些微不足道。
(2) 回到你的查询,首先我想问两件事:
(a) 您在匹配查询中使用了 $and
,但根据您的措辞描述,$or
逻辑似乎更合适。这是一个快速更改,您可以在需要时进行更改。
(b) 我不确定这是否有误,但您似乎已将 input
切换为“输出”,反之亦然。如果是这种情况,您应该切换它们。
话虽如此,我将如何重写此查询(剧透,变化不大):
movesModel.aggregate([
{ // notice i'm using Mongo's dot notation, $expr is also fine. not sure if there's an efficiency difference
$match: {
$and: [
{
$or: [
{
"product.id": req.params.idProduct
},
{
"origin.id": req.params.idOrigin
}
]
},
{
date: {$gt: new Date("2020-01-01")}
}
]
}
},
{ // there's no need for the project stage as we can just nest the condition into the $group, again this should not case
// performance changes. also i switched the input to match with the inputs.
$group: {
_id: '$_id',
inputs: {$sum: {$cond: {if: {$eq: ['$operation', 'input']}, then: '$product.quantity', else: 0}}},
outputs: {$sum: {$cond: {if: {$eq: ['$operation', 'output']}, then: '$product.quantity', else: 0}}}
}
},
])
因此,回顾一下您的管道在很大程度上是最优的,您对问题与索引相关的怀疑在某种程度上是正确的。从第 3 部分构建新索引后,性能将发生很大变化。
需要考虑的是 Scale 上升,您的数据库将(希望)保持增长。您当前的解决方案目前还不错,但最终它会在规模下崩溃并且性能会再次下降。想到的 2 个简单选项是:
预处理,您所做的每次更新或插入都有一个预先计算的集合,该集合将使用这些操作进行更新并保存所需的指标。
创建一个 "current" 集合以仅包含最近的数据并查询该数据。
这两者显然都会产生一些开销,您可以选择是否以及何时实施它们。
我正在尝试加快对包含超过 1000 万个文档的集合执行的查询。文档示例如下所示
{
nMove: 2041242,
typeMove: 'Sold',
date: "2016-05-18T16:00:00Z",
operation: 'output',
origin: {
id: '3234fds32fds42',
name: 'Main storage',
},
products: [{
id: '342fmdsff23324432',
name: 'Product 1',
price: 34,
quantity: 9
}],
}
现在我必须查询与给定的 'product.id' 或 'origin.id' 或两者匹配的所有文档,并且 $sum 总共 product.quantity 的数量。
所以我正在执行这样的查询。
movesModel.aggregate([
{
$match: {
$expr: {
$and: [
{ $in: [req.params.idProduct, '$product.id'] },
{ $eq: ['$origin.id', req.params.idOrigin }] },
]
}
}
},
{
$project: {
_id: 0,
outputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'input'] }, then: '$product.quantity', else: 0 }
}
},
inputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'output'] }, then: '$product.quantity', else: 0 }
}
}
}
},
{
$group: {
_id: '$_id',
inputs: { $sum: '$inputs' },
outputs: { $sum: '$outputs' }
}
},
]).then((result) => {
res.json(result)
})
解决此查询大约需要 1 分钟...有时此查询 $match 超过 200k 个文档...考虑到我不需要全部数据,我只需要数量的总和...我有一些问题...(我是mongodb菜鸟)
关于索引.. 我创建了一个复合索引db.moves.createIndex({ 'origin.id': 1, 'product.id':1})。这是正确的吗?我应该改变它吗?
我的查询可以吗?我可以改进它吗?
为了防止查询与 200k 文档匹配...我做了一些棘手的事情。我添加了一个名为 'date' 的字段,我想获取所有与 'origin.id'、'product.id' 和 $gte: date 匹配的文档,但它需要相同的时间.. . 即使它只匹配 1 个文档...
完成...我认为我遇到的所有问题都与索引有关...所以我尝试检查我的 indexStats...但它似乎不适用于我的聚合查询.
感谢任何帮助。谢谢
////////////完整管道////////////
在这种情况下,我还有两个集合,分别称为 'storages' 和 'inventories'
//storage examples
{
_id: '3234fds32fds42'
name: 'Main storage'
status: true
}
{
_id: '32f32f32432sda'
name: 'Other storage'
status: true
}
//invetories examples
{
_id: 'fvavcsa3a3aa3'
date: '2020-01-01'
storage: {
_id: '3234fds32fds42'
name: 'Main storage'
}
products: [{
id: '342fmdsff23324432',
name: 'Product 1',
}],
}
所以这就是我使用 $lookup 的原因,我真正需要的是获取与每个存储和产品相匹配的所有移动。
//我还添加了库存以按日期过滤并防止匹配大量文档
这是我的查询。
storagesModel.aggregation([
{
$match: { status: true }
},
{
$lookup: {
from: 'inventories',
as: 'inventory',
let: { "idStorage": "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$storage._id', { $toString: "$$idStorage" }] },
{ $in: [req.params.id, '$products._id'] }
]
}
},
},
{
$sort: { date: -1 } // TO TAKE THE LAST INVENTORY
},
{
$limit: 1
}
]
}
},
{ $unwind: { path: '$inventories', preserveNullAndEmptyArrays: true } }, //DECONSTRUCT THE ARRAY AND GET IT AS OBJECT
{
$lookup: {
from: 'moves',
as: 'moves',
let: {
"idStorage": "$_id",
'date': '$inventory.date'},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $gte: ['$date', $$date] }
{ $eq: ['$origin.id', '$$idStorage' }] },
{ $in: [req.params.idProduct, '$product.id'] },
]
}
}
},
{
$project: {
_id: 0,
outputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'input'] }, then: '$product.quantity', else: 0 }
}
},
inputs: {
$sum: {
$cond: { if: { $eq: ['$operation', 'output'] }, then: '$product.quantity', else: 0 }
}
}
}
},
{
$group: {
_id: '$_id',
inputs: { $sum: '$inputs' },
outputs: { $sum: '$outputs' }
}
},
]
}
])
总结...
我需要得到的是产品在每个存储空间中的总 'moves'。 考虑到可能有一个 'inventory' 可以给你一个日期来防止匹配很多文件。这就是为什么我使用 storageModel 并使用 $lookup 阶段来获取最后的库存,所以我有 $$date 和 $$idStorage。然后我使用 'moves' 集合中的 $lookup...我知道这是一个繁重的查询但是...我认为给出日期和适当的复合索引应该很快...但即使我尝试为只有很少 'moves' 的产品获得 'moves'... 大约需要 20 或 30 秒...
我尝试在没有 $lookup 的情况下进行单个查询,甚至匹配 400k 个文档也需要 1-2 秒...
你觉得呢?感谢您的帮助
总的来说你做的很好,我们把你的观点总结一下,分开说。
是的,这太棒了,复合索引正是这个管道所需要的。我相信您在选择创建复合索引之前已经阅读过有关复合索引的内容,所以我不会深入探讨为什么这个索引是最佳的,因为它很简单。
这个就到最后吧。
这很好,如果您不关心整个数据样本而只关心最近的数据,这是可行的方法,现在为了正确利用该字段来加快性能,您应该转储我们在第 1 部分中讨论的旧索引并创建一个新的复合索引以包含此字段,
{date: -1, 'origin.id': 1, 'product.id': 1}
请注意我们 select 日期的递减索引,因为我们需要最新的数据。这将使它更有效率。
由于您一定已经阅读过复合索引字段顺序问题,因此请随意更改此字段的顺序以匹配您最常执行的查询。
但是 Mongo 生成索引树的方式不太可能是不稳定的,我的意思是,如果您很久以前就创建了索引,并且从那时起有更多的数据进来,您可能会受益于删除并重建索引。话虽如此,我不推荐这样做,因为我觉得在你的情况下任何改进都会有些微不足道。
(2) 回到你的查询,首先我想问两件事: (a) 您在匹配查询中使用了
$and
,但根据您的措辞描述,$or
逻辑似乎更合适。这是一个快速更改,您可以在需要时进行更改。 (b) 我不确定这是否有误,但您似乎已将input
切换为“输出”,反之亦然。如果是这种情况,您应该切换它们。
话虽如此,我将如何重写此查询(剧透,变化不大):
movesModel.aggregate([
{ // notice i'm using Mongo's dot notation, $expr is also fine. not sure if there's an efficiency difference
$match: {
$and: [
{
$or: [
{
"product.id": req.params.idProduct
},
{
"origin.id": req.params.idOrigin
}
]
},
{
date: {$gt: new Date("2020-01-01")}
}
]
}
},
{ // there's no need for the project stage as we can just nest the condition into the $group, again this should not case
// performance changes. also i switched the input to match with the inputs.
$group: {
_id: '$_id',
inputs: {$sum: {$cond: {if: {$eq: ['$operation', 'input']}, then: '$product.quantity', else: 0}}},
outputs: {$sum: {$cond: {if: {$eq: ['$operation', 'output']}, then: '$product.quantity', else: 0}}}
}
},
])
因此,回顾一下您的管道在很大程度上是最优的,您对问题与索引相关的怀疑在某种程度上是正确的。从第 3 部分构建新索引后,性能将发生很大变化。
需要考虑的是 Scale 上升,您的数据库将(希望)保持增长。您当前的解决方案目前还不错,但最终它会在规模下崩溃并且性能会再次下降。想到的 2 个简单选项是:
预处理,您所做的每次更新或插入都有一个预先计算的集合,该集合将使用这些操作进行更新并保存所需的指标。
创建一个 "current" 集合以仅包含最近的数据并查询该数据。
这两者显然都会产生一些开销,您可以选择是否以及何时实施它们。