我无法让 mongo 索引与多个数组的字段一起工作。
蒙戈版本:6
我有一个包含数十亿文档的庞大数据库。出于实际原因,我使用几个文档创建了一个新的测试虚拟数据库,以了解 mongo 如何处理索引。
这是我拥有的文档列表:
[{
"_id": {
"$oid": "xxxxxxxxxxxxxxxxxx"
},
"deleted": true,
"a": 1,
"list": [
{
"l1": 1,
"l2": "123",
"l3": 456,
"l4": "value"
}
]
},
{
"_id": {
"$oid": "yyyyyyyyyyyyyyyyy"
},
"a": 1,
"list": [
{
"l1": "1",
"l2": 123,
"l3": 456,
"l4": "value"
}
]
},
{
"_id": {
"$oid": "zzzzzzzzzzzzzzzzzzzzzzz"
},
"d": "val_d",
"e": "val_e"
},
{
"_id": {
"$oid": "wwwwwwwwwwwwwwwwwwwwww"
},
"a": 1,
"list": [
{
"l1": "1",
"l2": 123,
"l3": 456,
"l4": "value"
}
]
}]
如果你看到,有一个文档与其他文档完全不同,这意味着“a”和“list”可能会丢失,还有“deleted”,而更常见的文档是带有“d”和“e”的类型”。 不管怎样,我最初要解决的问题是
query = {
"list.l1": "1",
"list.l4": "value",
"deleted": {
"$exists": false
},
"a": 1,
"list.l3": {
"$gte": 456
},
"list.l2": {
"$lte": 123
},
}
由于更改查询和索引的顺序并不能解决 docsExamined read != 0 问题,我决定从头开始,一次一个字段,从 list.l1 开始。
如果我只是查询“list.l1”:“1”并在“list.l1:1”上创建索引,它工作得很好,COUNT_SCAN作为阶段,只有keysExamined,边界完美:
indexBounds: {
startKey: {
'list.l1': '1'
},
startKeyInclusive: true,
endKey: {
'list.l1': '1'
},
endKeyInclusive: true
}
注意:在 list.l1 上使用 $elemMatch 而不是精确匹配,不会使用索引,并且 list.l1 会被 FETCHed(为什么?)
当我将 list.l4 添加到查询和索引时,问题出现了:mongo 不使用后者(list.l1:1,list.l4:1),而前者是:
indexBounds: {
'list.l1': [
'["1", "1"]'
]
},
inputStage: {
stage: 'FETCH',
filter: {
'list.l4': {
'$eq': 'value'
}
},
totalKeysExamined: 3,
totalDocsExamined: 3,
如果我尝试提示两个字段的最新索引,我会得到
indexBounds: {
'list.l1': [
'["1", "1"]'
],
'list.l4': [
'[MinKey, MaxKey]'
]
},
inputStage: {
stage: 'FETCH',
filter: {
'list.l4': {
'$eq': 'value'
}
},
totalKeysExamined: 3,
totalDocsExamined: 3,
由于我已经在 2 个字段上遇到了麻烦,因此对我的十亿文档数据库与其他字段进行调查是没有意义的,因为它在仅 2 个字段上就已经表现得很糟糕。
数组字段有什么问题吗?
我认为对于数组如何索引和查询可能存在一些误解。
考虑这 4 个文档的集合。 在 1-3 中,“arry”字段包含 2 个文档的数组,在 #4 中“arry”包含一个对象。
[
{"arry": [ // #1
{"f1": "red","f2": "left"},
{"f1": "blue","f2": "up"}
]},
{"arry": [ //#2
{"f1": "red","f2": "right"},
{"f1": "green","f2": "left"}
]},
{"arry": [ //#3
{"f1": "yellow","f2": "right"},
{"f1": "blue","f2": "up"}
]},
{"arry": {"f1": "red","f2": "left"}} //#4
]
查询
db.collection.find({"arry.f1":"red"})
将匹配包含数组中“f1”为“red”的任何元素的文档,并且还将匹配其中“arry”是具有此类字段的对象的文档,因此#1,#2,和#4 匹配。 游乐场
查询
db.collection.find({"arry":{"$elemMatch":{"f1":"red"}}})
将匹配文档,其中“arry”是一个包含具有匹配字段的元素的数组,因此#1和#2。 游乐场
查询像
db.collection.find({"arry.f1":"red","arry.f2":"left"})
这样的2个字段将匹配数组是一个对象或对象数组的文档,其中“f1”在至少一个元素中为“红色”,“f2”在至少一个元素中为“左”(不一定是相同),因此这将再次匹配 #1、#2 和 #4。 游乐场
使用elemMatch(如
db.collection.find({"arry":{"$elemMatch":{"f1":"red","f2":"left"}}})
)查询2个字段将匹配其中“arry”是对象数组的文档,并且单个对象匹配两个字段,因此仅#1。 游乐场
现在谈谈索引。 当数组字段被索引时,会为数组的每个元素创建一个单独的键,并且每个索引键仅引用一个元素。
在包含上述文档的集合中的
{"arry.f1":1, "arry.f2":1}
上创建索引将导致索引包含键(|
实际上并没有被mongod用作分隔符),我已经标记了每个引用的文档:
blue|up #1
blue|up #3
green|left #2
red|left #1
red|left #4
red|right #2
yellow|right #3
考虑与上面相同的查询:
db.collection.find({"arry.f1":"red"})
- 跳至索引中第一个“red”实例,并向前扫描最后一个,返回文档 #1、#4 和 #2
db.collection.find({"arry":{"$elemMatch":{"f1":"red"}}})
可以对初学者使用相同的计划,但随后必须使用匹配阶段来确保它只匹配数组。 根据集合中有多少文档,扫描索引然后获取文档并再次匹配可能比仅仅进行集合扫描需要更多工作。
对于
db.collection.find({"arry.f1":"red","arry.f2":"left"})
,如果查询规划器使用范围{"arry.f1":["red","red"],"arry.f2":["left","left"]}
进行朴素索引扫描,它将仅找到文档#1和#4,这是不正确的,因为#2也应该匹配。 为了找到所有结果,它扫描范围 {"arry.f1":["red","red"],"arry.f2":(MinKey,MaxKey)}
的索引,找到与第一个谓词匹配的所有文档,并使用获取/匹配将结果限制为与第二个谓词匹配的结果。
对于
db.collection.find({"arry":{"$elemMatch":{"f1":"red","f2":"left"}}})
,它可以使用索引,因为每个索引键引用数组的单个元素,并且此查询将匹配限制在同一元素中。 对范围 {"arry.f1":["red","red"],"arry.f2":["left","left"]}
的索引扫描找到文档 #1 和 #4,查询规划器使用匹配来消除非数组字段,因此只返回 #1。