使用案例
我有一个拥有几百万份文件的mongodb集合。这里的文件
集合有时必须更新。因此,我设置了monitorFrequency
字段,用于定义特定文档必须每6,12,24或720小时更新一次。另外,我设置了一个名为lastRefreshAt
的字段,它是上次实际更新的时间戳。
问题:
如何从我的集合profiles
中选择需要再次刷新的所有文档(因为monitorFrequency早于lastRefreshAt)。
我应该在单个查询上运行它,它只会返回那些需要再次刷新的文档,或者我是否应该使用游标迭代所有文档并检查我的节点应用程序是否需要刷新文档或不?
我知道如何做方法#2,但我不确定选择哪种方法以及#1的查询结果如何。
答案 0 :(得分:1)
根据可用的架构和选择,有几种方法。有些是好的选择,有些是坏的,但我们不妨解释一下。
作为第一个需要检查的选项,您可以使用$where
来计算选择的差异,并直接提供给.update()
或.updateMany()
:
db.profiles.update(
{
"$where": function() {
return (Date.now() - this.lastRefreshAt.valueOf())
> ( this.monitorFrequency * 1000 * 60 * 60 );
}
},
{ "$currentDate": { "lastRefreshAt": true } },
{ "multi": true }
)
这可以简单地计算当前"lastRefreshAt"
值与当前Date
值之间的毫秒差异,并将其与存储的"monitorFrequency"
进行比较,转换为毫秒数。
$currentDate
是适用的,因为它是"multi"
更新并应用于所有匹配的文档,因此这可确保"服务器时间戳"在文档更新的实际时间应用于文档。
它并不太棒,因为它需要完整的收集扫描才能通过计算选择文档,因此无法使用索引。加上它的JavaScript评估,它不是本机代码会增加一些开销。
因此,当其他选项适用时,JavaScript通常不是一个很好的选择选项。而是尝试使用聚合框架进行计算并循环游标结果:
var ops = [];
db.profiles.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$gt": [
{ "$subtract": [new Date(), "$lastRefreshAt"] },
{ "$multiply": ["$monitorFrequency", 1000 * 60 * 60] }
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
]).forEach(doc => {
ops.push({
"updateOne": {
"filter": { "_id": doc._id },
"update": { "$currentDate": { "lastRefreshAt": true } }
}
});
if ( ops.length > 1000 ) {
db.profiles.bulkWrite(ops);
ops = [];
}
})
if ( ops.length > 0 ) {
db.profiles.bulkWrite(ops);
ops = [];
}
因此,由于计算的原因,这是一次集合扫描,但它是由本机运算符完成的,所以至少部分应该更快一些。同样从技术角度来看,它有点不同,因为new Date()
实际上是在请求时建立的,而不是每个文档迭代,因为它将使用$where
。缺乏操作员来生成"当前日期"在内部,聚合框架无法在每次迭代时执行此操作。
当然,而不仅仅是应用我们的"更新"表达式匹配文档时,我们循环结果游标并应用函数。所以虽然有一些"一些"收益,还有额外的开销。里程可能会因性能和实用性而有所不同。
就我个人而言,我不会做上述任何一项,只需运行一个查询,选择每个标记"monitorFrequency"
并查找超出允许差异的边界之间的日期。
作为使用NodeJS为并行调用实现Promise.all()
的简单示例:
const MongoClient = require('mongodb').MongoClient;
const onHour = 1000 * 60 * 60;
(async function() {
let db;
try {
db = await MongoClient.connect('mongodb://localhost/test');
let collection = db.collection('profiles');
let intervals = [6, 12, 24, 720];
let snapDate = new Date();
await Promise.all(
intervals.map( (monitorFrequency,i) =>
collection.updateMany(
{
monitorFrequency,
"lastRefreshAt": Object.assign(
{ "$lt": new Date(snapDate.valueOf() - intervals[i] * oneHour) },
(i < intervals.length) ?
{ "$gt": new Date(snapDate.valueOf() - intervals[i+1] * oneHour) }
: {}
)
},
{ "$currentDate": { "lastRefreshAt": true } },
)
)
);
} catch(e) {
console.error(e);
} finally {
db.close();
}
})();
这将允许您在两个字段上编制索引并允许最佳选择,并且因为&#34;日期范围&#34;与"monitorFrequency"
的计算差异配对,然后那些需要刷新的文件&#34;是唯一被选中进行更新的人。
Gievn有限数量的可能间隔这是我怀疑是最佳解决方案。但结构以及每个选择的实际"update"
部分保持一致这一事实导致了另一种选择。
与上面的逻辑大致相同,但是应用于为&#34;查询&#34;构建$or
条件。 &#34;单身&#34;的一部分更新。这是一系列标准&#34; afterall,它基本上与&#34;查询数组相同&#34;这就是我们上面所做的。所以只需稍微转一下:
let intervals = [6, 12, 24, 720];
let snapDate = new Date();
db.profiles.updateMany(
{
"$or": intervals.map( (monitorFrequency,i) =>
({
monitorFrequency,
"lastRefreshAt": Object.assign(
{ "$lt": new Date(snapDate.valueOf() - intervals[i] * oneHour) },
(i < intervals.length) ?
{ "$gt": new Date(snapDate.valueOf() - intervals[i+1] * oneHour) }
: {}
)
})
)
},
{ "$currentDate": { "lastRefreshAt": true } }
)
然后这变成一个简单的语句,当然可以在可用的情况下使用索引。通常这是你应该做的,虽然我已经建议我的直觉告诉我,只有最慢的4个执行线程才能让工作完成得更快。再一次,里程可能会有所不同,但逻辑表明情况确实如此。
所以这里的基本教训是#34;而你可能会想到&#34;逻辑方法是计算值并在数据库本身内进行比较,它实际上是您可以为查询性能做的最糟糕的事情。
采取的简单方法是制定应该选择所需文件的标准&#34;之前&#34;您将查询语句发送到服务器。这意味着您正在关注具体的价值观。而不是&#34;计算结果&#34;相比下。并且&#34;具体的价值观&#34;实际上可以编制索引,这通常是您想要的数据库查询。