这确实是其中一种情况,您必须表明立场并做出改变,而不是试图“跳过箍”来处理其他人糟糕的设计决策。这里要扩展的“橄榄枝”是最初的设计可能没有考虑到数据将如何被使用。
您在不更改数据存储方式的情况下提出的查询需要不小的努力。我将这里的所有内容都保留为带有 JSON 表示法或其他原始 JavaScript 的“shell”形式。与其他语言一样,JSON 易于解析或转换为可用于为 Java 驱动程序构造 BSON 对象的方法。
所以,继续前进,让我们看看这里的所有案例以及如何解决,以及限制以及最终在此处进行更改的好处。
考虑我们“即将到期”的集合中的以下示例:
{ "contractDate" : ISODate("2014-04-23T00:00:00Z"), "term" : 10 }
{ "contractDate" : ISODate("2014-04-23T00:00:00Z"), "term" : 7 }
{ "contractDate" : ISODate("2014-11-30T00:00:00Z"), "term" : 1 }
MongoDB 有一个 $where 运算符,它将在服务器上运行任意 JavaScript 代码(作为 JavaDriver 的字符串提供)。定义的函数必须返回true/false,以确定是否满足查询条件。基本上将“contractDate”+“term”评估为当前日期,或者由允许您将变量“范围”到评估的 JavaScript 的变体提供的日期:
db.expiring.count({
"$where": function () {
var now = new Date(),
today = new Date(
now.valueOf() -
( now.valueOf() % ( 1000 * 60 * 60 * 24 ) )
);
var adjustedMonth =
this.contractDate.getMonth() + 1 + this.term;
var year = ( adjustedMonth > 12 ) ?
this.contractDate.getFullYear() + 1
: this.contractDate.getFullYear();
var month = ( adjustedMonth > 12 ) ?
adjustedMonth - 12 : adjustedMonth;
var day = this.contractDate.getDate();
var expiring = new Date( year + "-" + month + "-" + day );
return expiring > today;
}
})
这太可怕了,因为您既要强行针对集合中的每个文档评估条件,又要针对集合中的每个项目强制服务器端评估和执行 JavaScript 代码。由于它计算评估,因此您不能使用索引来改进任何东西。
您还可以通过聚合框架计算日期并进行比较。为了一点可读性(也让我自己动手),这里的示例分两个阶段给出,但可以在单个 $group 阶段完成:
db.expiring.aggregate([
{ "$project": {
"contractDate": 1,
"term": 1,
"expires": {
"year": {
"$cond": [
{ "$gt": [
{ "$add": [{ "$month": "$contractDate" }, "$term" ] },
12
]},
{ "$add": [{ "$year": "$contractDate" }, 1 ] },
{ "$year": "$contractDate" }
]
},
"month": {
"$cond": [
{ "$gt": [
{ "$add": [{ "$month": "$contractDate" }, "$term" ] },
12
]},
{ "$subtract": [
{ "$add": [{ "$month": "$contractDate" }, "$term" ] },
12
]},
{ "$add": [{ "$month": "$contractDate" }, "$term" ] }
]
},
"day": { "$dayOfMonth": "$contractDate" }
}
}},
{ "$group": {
"_id": null,
"count": {
"$sum": {
"$cond": [
{ "$or": [
{ "$gt": [ "$expires.year", thisYear ] },
{ "$and": [
{ "$eq": [ "$expires.year", thisYear ] },
{ "$gt": [ "$expires.month", thisMonth ] },
]},
{ "$and": [
{ "$eq": [ "$expires.year", thisYear ] },
{ "$eq": [ "$expires.month", thisMonth ] },
{ "$gt": [ "$expires.day", thisDay ] }
]}
]},
1,
0
]
}
}
}}
])
当然,在构建表示当前日期时会输入外部变量。在这里,它们被分解为thisYear、thisMonth 和thisDay 以匹配显示的模式。您还可以使用类似于 JavaScript 代码的“日期数学”方法。
再一次,这太可怕了。即使在单个管道阶段,这仍然需要贯穿整个集合。本地操作符会加快速度,但不会快很多,当然你仍然不能使用索引。
这就是您应该更改数据存储方式的原因。考虑一下何时文档看起来像这样:
{
"contractDate" : ISODate("2014-04-23T00:00:00Z"),
"term" : 10,
"expiry": ISODate("2015-02-23T00:00:00Z")
}
{
"contractDate" : ISODate("2014-04-23T00:00:00Z"),
"term" : 7,
"expiry" : ISODate("2014-11-23T00:00:00Z"),
}
{
"contractDate" : ISODate("2014-11-30T00:00:00Z"),
"term" : 1,
"expiry": ISODate("2014-12-30T00:00:00Z")
}
现在还要考虑新的expiry 字段也被索引了,现在获取计数的非常有效的方法是非常基本的:
db.expiring.count({ "expiry": { "$gt": new Date("2014-12-30") } })
就是这样!只有那些大于指定索引范围的项目被触及,您只需计算仍处于活动状态的项目的数量,而无需计算或评估任何内容。
因此,我认为需要更改维护此数据的代码,以在文档中保留此附加字段,并在任何更改时保留“contractDate”和“term”字段中的两个相关字段。
操作很简单,应该不难,并且应该谈论维护此代码的“非常小的”更改,以及对现有数据的“一次性”更新以使其如此。所以平衡要么是“小变化”,要么是实施“大混乱”,只是为了报告不存在的东西。
我强烈建议您将其展示给能够做出更改决定的人。这将节省您和其他所有人的时间。没有人想要缓慢而长时间运行的查询。这也是要花钱的。