使用 mongodb 时要考虑的最重要的事情之一是在决定数据库设计时必须考虑应用程序的访问模式。我们将尝试通过使用示例来解决您的问题,并查看它是如何工作的。
假设您的集合中有以下文档,让我们在下面看看如何使这种简单的数据格式发挥作用:
> db.performant.find()
{ "_id" : ObjectId("522bf7166094a4e72db22827"), "name" : "abc", "tags" : [ "chest", "bicep", "tricep" ] }
{ "_id" : ObjectId("522bf7406094a4e72db22828"), "name" : "def", "tags" : [ "routine", "trufala", "tricep" ] }
{ "_id" : ObjectId("522bf75f6094a4e72db22829"), "name" : "xyz", "tags" : [ "routine", "myTag", "tricep", "myTag2" ] }
{ "_id" : ObjectId("522bf7876094a4e72db2282a"), "name" : "mno", "tags" : [ "exercise", "myTag", "tricep", "myTag2", "biceps" ] }
首先,您绝对必须在标签上创建索引。(如果需要,您可以创建一个复合索引)
> db.performant.ensureIndex({tags:1})
> db.performant.getIndexes()
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"ns" : "test.performant",
"name" : "_id_"
},
{
"v" : 1,
"key" : {
"tags" : 1
},
"ns" : "test.performant",
"name" : "tags_1"
}
]
要从上述集合中查询标签数据,通常会使用 db.performant.find({tags:{$in:["bicep"]}}),但这不是一个好主意。让我告诉你为什么:
> db.performant.find({tags:{$in:["bicep","chest","trufala"]}}).explain()
{
"cursor" : "BtreeCursor tags_1 multi",
"isMultiKey" : true,
"n" : 2,
"nscannedObjects" : 3,
"nscanned" : 5,
"nscannedObjectsAllPlans" : 3,
"nscannedAllPlans" : 5,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"tags" : [
[
"bicep",
"bicep"
],
[
"chest",
"chest"
],
[
"trufala",
"trufala"
]
]
},
"server" : "none-6674b8f4f2:27017"
}
您可能已经注意到,此查询正在执行整个集合扫描。这可能会让你想知道,为什么我们添加了那个索引,如果它没有被使用,我也想知道。但不幸的是,这是一个 mongoDB 尚未解决的问题(至少据我所知)
不过幸运的是,我们可以解决这个问题,并且仍然使用我们在标签集合上创建的索引。方法如下:
> db.performant.find({$or:[{tags:"bicep"},{tags:"chest"},{tags:"trufala"}]}).explain()
{
"clauses" : [
{
"cursor" : "BtreeCursor tags_1",
"isMultiKey" : true,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 10,
"indexBounds" : {
"tags" : [
[
"bicep",
"bicep"
]
]
}
},
{
"cursor" : "BtreeCursor tags_1",
"isMultiKey" : true,
"n" : 0,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"tags" : [
[
"chest",
"chest"
]
]
}
},
{
"cursor" : "BtreeCursor tags_1",
"isMultiKey" : true,
"n" : 1,
"nscannedObjects" : 1,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 1,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"tags" : [
[
"trufala",
"trufala"
]
]
}
}
],
"n" : 2,
"nscannedObjects" : 3,
"nscanned" : 3,
"nscannedObjectsAllPlans" : 3,
"nscannedAllPlans" : 3,
"millis" : 10,
"server" : "none-6674b8f4f2:27017"
}
如您所见,n 非常接近 nscanned。扫描了三个记录,每个对应于“bicep”、“chest”、“trufala”。由于“bicep”和“chest”属于同一个文档,因此只返回1个对应的结果。一般来说,count() 和 find() 都会进行有限的扫描,并且非常有效。此外,您永远不必为用户提供过时的数据。您也可以完全避免运行任何类型的批处理作业!!!
因此,通过使用这种方法,我们可以得出以下结论:如果您搜索 n 个标签,并且每个标签出现 m 次,则扫描的文档总数将是 n * m。现在考虑到您有大量的标签和大量的文档,并且您扫描了几个标签(这些标签又对应于少数文档 - 尽管不是 1:1),结果总是非常快,因为发生了 1 个文档扫描每个标签和文档组合。
注意:这种方法永远无法覆盖索引,因为数组上有一个索引,即“isMultiKey”:true。您可以在此处阅读有关涵盖索引的更多信息
局限性:每种方法都有局限性,这个也有!!对结果进行排序将产生极差的性能,因为它将扫描整个集合的次数等于传递给此查询的标签的次数,加上它扫描与 $or 的每个参数对应的其他记录。
> db.performant.find({$or:[{tags:"bicep"},{tags:"chest"},{tags:"trufala"}]}).sort({tags:1}).explain()
{
"cursor" : "BtreeCursor tags_1",
"isMultiKey" : true,
"n" : 2,
"nscannedObjects" : 15,
"nscanned" : 15,
"nscannedObjectsAllPlans" : 15,
"nscannedAllPlans" : 15,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"tags" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
]
},
"server" : "none-6674b8f4f2:27017"
}
在这种情况下,它扫描 15 次,这等于 3 次完整集合扫描,每次 4 条记录加上每个参数扫描的 3 条记录 $or
最终结论:如果您对未排序的结果表示满意,或者愿意在前端付出额外的努力来自己对它们进行排序,请使用这种方法来获得非常有效的结果。