Монго отказывается использовать все поля из составного индекса
У меня большая база данных и несколько составных индексов. Проблема в том, что Mongo не использует все поля из индекса для запроса, он использует только 2 поля из 4, хотя все 4 поля находятся в составном индексе. Или он использует составной индекс из 3 полей вместо 4-х полей.
Вот моя схема (есть другие поля в данных и мета, но они не используются в этом запросе):
{
store_id: {type: String},
data: {
id: {type: Number},
},
meta: {
is_published: {type: Boolean},
lowercase_sku: {type: String}
}
}
У меня есть эти индексы:
db.products.createIndex({'store_id':1,'meta.is_published':1,'data.id':1})
db.products.createIndex({'store_id':1,'meta.is_published':1,'data.id':1,'meta.lowercase_sku':1})
Вот мой запрос:
db.products.find({
'store_id': 'my_domain.com',
'meta.lowercase_sku': 'go-a83-3034/8+4',
'meta.is_published': true,
'data.id': {'$ne': 7801040323}
}).explain('executionStats')
И вот что я получаю из объяснения:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "my_database.products",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"meta.is_published" : {
"$eq" : true
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
{
"store_id" : {
"$eq" : "my_domain.com"
}
},
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1",
"isMultiKey" : false,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"[\"my_domain.com\", \"my_domain.com\"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
]
}
}
},
"rejectedPlans" : [
{
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
}
]
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"[\"my_domain.com\", \"my_domain.com\"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
}
}
}
]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 4590,
"totalKeysExamined" : 28527,
"totalDocsExamined" : 28525,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 130,
"works" : 28528,
"advanced" : 1,
"needTime" : 28525,
"needYield" : 0,
"saveState" : 5351,
"restoreState" : 5351,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 28525,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 28525,
"executionTimeMillisEstimate" : 70,
"works" : 28527,
"advanced" : 28525,
"needTime" : 1,
"needYield" : 0,
"saveState" : 5351,
"restoreState" : 5351,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1",
"isMultiKey" : false,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"[\"my_domain.com\", \"my_domain.com\"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
]
},
"keysExamined" : 28527,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"version" : "3.2.18",
"gitVersion" : "4c1bae566c0c00f996a2feb16febf84936ecaf6f"
},
"ok" : 1
}
Как видите, по какой-то причине mongo не использует индекс из 4 полей, и даже если я намекаю на индекс, он использует только два первых поля ("store_id" и "meta.is_published"). Он имеет тенденцию выбирать индекс из 3 полей, который охватывает только 3 поля ("store_id", "meta.is_published" и "data.id"), что имеет смысл, при условии, что из индекса из 4 полей используются только 2 поля. Но почему?
Может кто-нибудь объяснить мне и, возможно, намекнуть, как я могу заставить запрос работать быстрее?
База данных большая (около 11 ГБ), и она была недавно перенесена (импортирована с индексами), если это поможет. Однако указанные индексы были созданы ПОСЛЕ импорта.
UPD (2): Я также создал тестовую коллекцию документов с той же схемой и индексами. И тот же запрос действительно использует индекс правильно. Поэтому я склонен думать, что существует какая-то проблема с размерами индекса или типами значений. Но я все еще не могу определить это.
Любая помощь или понимание этого я очень ценю.
Ниже приведен запрос с подсказкой к 4-полюсному индексу и результатом для вашей ссылки.
db.products.find({
'store_id': 'my_domain.com',
'meta.lowercase_sku': 'go-a83-3034/8+4',
'meta.is_published': true,
'data.id': {'$ne': 7801040323}
}).hint('store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1').explain('executionStats')
Результат:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "sharp_production.products",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"meta.is_published" : {
"$eq" : true
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
{
"store_id" : {
"$eq" : "my_domain.com"
}
},
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
}
]
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"[\"my_domain.com\", \"my_domain.com\"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 213,
"totalKeysExamined" : 28630,
"totalDocsExamined" : 28525,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"$and" : [
{
"$not" : {
"data.id" : {
"$eq" : 7801040323
}
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
}
]
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 210,
"works" : 28630,
"advanced" : 1,
"needTime" : 28628,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 28525,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 28525,
"executionTimeMillisEstimate" : 110,
"works" : 28630,
"advanced" : 28525,
"needTime" : 104,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"data.id" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_data.id_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"[\"my_domain.com\", \"my_domain.com\"]"
],
"meta.is_published" : [
"[true, true]"
],
"data.id" : [
"[MinKey, 7801040323.0)",
"(7801040323.0, MaxKey]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
},
"keysExamined" : 28630,
"dupsTested" : 28628,
"dupsDropped" : 103,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"version" : "3.2.18",
"gitVersion" : "4c1bae566c0c00f996a2feb16febf84936ecaf6f"
},
"ok" : 1
}
ОБНОВЛЕНИЕ (1)
Хорошо, очевидно, оператор $ ne может испортить индекс, как сказал @JohnnyHK. Итак, я создал еще один такой:
db.products.createIndex({'store_id':1,'meta.is_published':1,'meta.lowercase_sku':1})
... и попытался использовать его для моего запроса после удаления "$ne" (это всего лишь один документ, на самом деле ничего не меняет):
db.products.find({
'store_id': 'my_domain.com',
'meta.is_published': true,
'meta.lowercase_sku': 'go-a83-3034/8+4',
}).hint('store_id_1_meta.is_published_1_meta.lowercase_sku_1').explain('executionStats')
Но вот что я получил на этот раз:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "sharp_production.products",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"meta.is_published" : {
"$eq" : true
}
},
{
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
{
"store_id" : {
"$eq" : "my_domain.com"
}
}
]
},
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"[\"my_domain.com\", \"my_domain.com\"]"
],
"meta.is_published" : [
"[true, true]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 136,
"totalKeysExamined" : 28629,
"totalDocsExamined" : 28526,
"executionStages" : {
"stage" : "FETCH",
"filter" : {
"meta.lowercase_sku" : {
"$eq" : "go-a83-3034/8+4"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 120,
"works" : 28630,
"advanced" : 1,
"needTime" : 28628,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 28526,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 28526,
"executionTimeMillisEstimate" : 30,
"works" : 28630,
"advanced" : 28526,
"needTime" : 103,
"needYield" : 0,
"saveState" : 223,
"restoreState" : 223,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"store_id" : 1,
"meta.is_published" : 1,
"meta.lowercase_sku" : 1
},
"indexName" : "store_id_1_meta.is_published_1_meta.lowercase_sku_1",
"isMultiKey" : true,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"store_id" : [
"[\"my_domain.com\", \"my_domain.com\"]"
],
"meta.is_published" : [
"[true, true]"
],
"meta.lowercase_sku" : [
"[MinKey, MaxKey]"
]
},
"keysExamined" : 28629,
"dupsTested" : 28629,
"dupsDropped" : 103,
"seenInvalidated" : 0
}
}
},
"serverInfo" : {
"version" : "3.2.18",
"gitVersion" : "4c1bae566c0c00f996a2feb16febf84936ecaf6f"
},
"ok" : 1
}
... Все еще сохраняет условие "meta.lowercase_sku" для этапа FETCH, даже если оно прямо в индексе. Есть еще идеи?
1 ответ
Это связано с тем, как планировщик запросов выбирает индекс для использования.
Суть проблемы в том, что у вас есть несколько индексов, которые могут удовлетворить запрос. Допустим, у вас есть два индекса с одинаковым префиксом:
db.test.createIndex({a:1, b:1})
а также
db.test.createIndex({a:1, b:1, c:1})
Теперь, если вы делаете:
db.test.find({a:1, b:1})
Как MongoDB знает, какой индекс может удовлетворить этот запрос? В зависимости от расположения на диске (среди многих вещей) один индекс может быть быстрее другого.
Итак, сначала он будет ранжировать планы по следующей формуле:
score = baseScore + productivity + tieBreakers
где в настоящее время в MongoDB 3.6.3, baseScore = 1
, productivity
зависит от того, сколько работы потребуется для того, чтобы план дал максимально возможный результат, и tieBreakers
зависит от того, нужен ли индекс FETCH
, не имея SORT
этап или пересечение индекса.
Если два плана дали одинаковые оценки (как и два плана выше), то это приводит их в гонку, чтобы получить 101 результат быстрее. Выигрышный план будет кэширован, и план будет выполнен. Как вы можете себе представить, это может быть недетерминированным, а также тратить время сервера. Лучше всего, если сервер может просто выбрать индекс, основанный на системе оценки.
Лучший способ индексировать коллекцию - это удалить несущественные индексы, чтобы MongoDB мог сразу же выбрать индекс.
Примечание: если вас интересуют мелкие детали, вы можете:
- Зарегистрируйте метод планирования запросов, используя
db.setLogLevel(5,'query')
и увидеть процесс, напечатанный вmongod
журналы. db.collection.getPlanCache().clear()
очистить кэш плана для коллекции.- Изучите исходный код ранжера плана.
- Посмотрите сообщение в блоге Оптимизация сложных индексов MongoDB.