架构:
{
time_utc: "milliseconds",
city: "string",
age: "integer"
}
指数:
{
time_utc: 1,
city: 1,
age: 1
}
示例查询:
col.aggregate([
{
$match: { time_utc: { $exists: true } city: "new york", age: { $gt: 18 } }
},
{
$sort: { time_utc: -1 }
}
])
我确信在这个查询中将使用复合索引,这样排序就会高效。
然而,$match
文档/过滤阶段似乎效率不高,因为我们可以假设它time_utc
在整个集合中是唯一的。
假设集合中有 100 万个文档,只返回 10 个。我们进一步假设这 10 个文档位于排序的底部time_utc
。
在这种情况下,查询必须扫描索引的整个 100 万个值才能发现 10 个文档。这相当于完整的集合扫描。
让我们假设相反, 中只有 2 个唯一值time_utc
。在这种情况下,看起来city
和age
字段的索引实际上是有效的。
我的推理正确吗?
我想说你的一些推理是正确的,思考索引结构的后果确实非常重要。像往常一样,@Wernfried Domscheit在他的回答中提出了一些要点。但我认为这里还有更多内容需要解开,所以让我们深入探讨。我们将首先直接回应一些陈述。
索引可以用,是的。但是,正如我之前提到的,这并不意味着它将被使用。在存在其他可行的索引的情况下尤其如此,我们将在稍后讨论。
正确的。不过,您所描述的行为/问题并不与扫描索引时结果所在的位置隔离。
在您所描述的场景中,所需的索引扫描非常“宽”。这使得查询服务的效率相当低。不过,结果在该索引中的放置通常与查询的行为和性能无关。如果这 10 个文档也是索引扫描中的前 10 个文档,那么您的查询将花费相同的时间并执行相同的工作量。
我们可以看到,通过在两个方向上发出排序时查看计划的执行统计信息,因为可以沿或方向
explain
扫描索引。使用问题中提供的降序排序:forward
reverse
并使用相反的(升序):
是的。正如您在这里所暗示的,这是数据分布的结果,而不是这种情况下数据库的某些不同行为。
使用上面的术语,索引扫描的边界仍然像以前一样“宽”。这里的区别在于索引的“宽度”要小得多。在这种有 2 个唯一值的情况下,我希望输出
explain
(可能)仅报告一些值seeks
,这将反映索引在这方面“更窄”的逻辑结构。让我们进一步探讨一下。
“宽”扫描
一般来说,当索引定义以排序字段开头时,对该字段的索引扫描将不受限制。在您的情况下,您确实在同一字段 (
time_utc
) 上有一个谓词条件,但它实际上并没有导致缩小扫描范围。我们可以在输出中看到explain
:无论集合中的数据如何,这些边界都是相同的。
现在,如果我们查看
IXSCAN
原始数据集(100 万个唯一值)的执行统计信息,我们可以看到数据库需要在索引中跳转以查找相关部分,并最终查看整个数据:但是,当索引定义的主键只有 2 个不同的值时,数据库会跳过其中的大部分:
建议(ESR 指南)
正如另一个答案提到的,如果结果集很小,那么排序的成本并不是特别高。
但总的来说,MongoDB 建议索引的起点是他们所说的ESR 规则。使用这种方法,谓词 on
city
是一个相等条件,因此应放在索引的第一位。time_utc
然而,谓词是一个范围条件,因为它基本上意味着“除了缺失之外的任何值”。由于该字段在两个方面都使用(排序和范围),因此指南建议您尝试的索引是:现在回到关于结果集大小的问题,通过交换第二个和第三个键并引发阻塞排序,您实际上确实有可能看到更好的性能:
这完全取决于数据选择性,并且某些谓词值可能比另一个索引更适合一个索引。您必须在自己的环境中进行测试和评估。
首先,日期/时间值应该更好地正确存储为
Date
对象,而不是数字,甚至更糟糕的是字符串。Date
对象始终是 UTC 时间,因此它应该满足您的要求。city
如果and上的条件age
仅返回 10 个文档,则不需要 上的索引time_utc
。对于 10 个文档的排序,是否使用索引并不重要,无论是否time_utc
有 1000 万个不同值或只有两个不同值。