Основы индексов MongoDB
Индексы базы данных необходимы для повышения производительности нашей системы. Поэтому сегодня мы хотим начать серию постов, связанных с этой темой. В частности, мы поговорим о том, как управлять индексами в MongoDB, самой популярной базе данных NoSQL. В этом первом посте мы обсудим основные концепции. В будущем, возможно, мы будем публиковать более продвинутые материалы :) Начнем!
Прежде всего, мы запускаем контейнер Docker с Mongo 5, открываем оболочку Mongo и создаем коллекцию пользователей в тестовой базе данных.
docker run --name mongo5 -p 27017:27017 -d mongo:5.0.2
docker exec -it mongo5 mongo
use test
db.createCollection("users")
Создадим данные
Чтобы поиграть во что-то знакомое, мы собрали сборник спортивной тематики.
{
"_id" : ObjectId("6148fddb88c2dc877518437e"),
"user_id" : 158021798,
"phone" : 627210141,
"favourite_sports" : [ "ping-pong", "padel" ],
"stats" : {
"matches_won" : 907,
"matches_lost" : 566
}
}
По сути, мы создали простую коллекцию, в которой мы храним различную информацию о пользователе, такую как номер телефона, любимые виды спорта и некоторую статистику матчей, сыгранных пользователем. Не обращайте на это особого внимания, глупый пример.
Чтобы было интересно, давайте создадим 1 миллион документов, следуя этой структуре, но со случайными данными. Следующий скрипт выполнит эту функцию. Конечно, на это потребуется время. Мы ставим сценарий только потому, что ... почему бы и нет? Но особо не нужно обращать на это внимание.
function random(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
const sports = ["padel", "tenis", "ping-pong", "badminton"];
const nDocs = 1000000;
documents = [];
for (let i = 0; i < nDocs; i++) {
const shuffled = sports.sort(() => 0.5 - Math.random());
const random_sports = shuffled.slice(0,
random(1, sports.length));
documents.push({
"user_id": random(100000000, 199999999),
"phone": random(600000000, 699999999),
"favourite_sports": random_sports,
"stats": {
"matches_won": random(0, 1000),
"matches_lost": random(0, 1000)
}
});
}
db.users.insert(documents)
Итак, теперь у нас есть полная коллекция данных! Давайте запустим несколько запросов, чтобы увидеть, как Mongo выполняет поиск по 1 миллиону документов. Обратите внимание: чтобы получить реальный пример коллекции, мы можем запустить db.users.find().limit(1)
и использовать значения из этого конкретного документа.
Индекс ID
Пойдем с первого раунда. Поиск документа по идентификатору.
db.users.find({"_id": ObjectId("6148fddb88c2dc877518437e")})
{
"_id" : ObjectId("6148fddb88c2dc877518437e"),
"user_id" : 158021798,
"phone" : 627210141,
"favourite_sports" : [ "ping-pong", "padel" ],
"stats" : {
"matches_won" : 907,
"matches_lost" : 566
}
}
Что ж, он вернул документ, но это не то, что нас интересует. Чтобы увидеть, что происходит за этим запросом, мы должны выполнить следующее:
db.users.find({"_id": ObjectId("6148fddb88c2dc877518437e")})
.explain("executionStats")
Это возвращает данные о том, что Mongo использовало для получения результатов. Вдобавок, если мы укажем опцию «executionStats», мы увидим статистику того, как выполнялся запрос производительности. В результате мы можем увидеть много интересной информации. Но в этом посте мы сосредоточимся только на некоторой статистике. Обратите внимание, что эту операцию объяснения запроса можно выполнить с помощью различных инструментов, таких как MongoDB Compass.
{
...
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
"stage" : "IDHACK",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"keysExamined" : 1,
"docsExamined" : 1
}
},
...
}
Здесь мы видим, что время выполнения в миллисекундах было 0, потому что оно было выполнено очень быстро. Но самое интересное, что это этап IDHACK, что означает, что был использован индекс по _id, который Mongo создает по умолчанию. Как мы все знаем, индекс - это не что иное, как структура данных, которая позволяет быстро ссылаться на соответствующие документы. В частности, Mongo хранит индексы в дереве B+. Причина, по которой дерево B+ используется вместо B- дерева, или двоичного дерева, или даже хеш-таблицы ... тема для другого поста. Может в следующий раз.
Также обратите внимание, что 1 документ был рассмотрен и 1 возвращен. Таким образом, можно сказать, что соотношение рассмотренных документов / возвращенных документов равно 1, что является оптимальным. Это означает, что остальные 999 999 документов в нашей коллекции не исследованы. И это то, что мы хотим достичь в нашей системе, чтобы все запросы имели низкий коэффициент, чтобы избежать просмотра ненужных документов, что ухудшает производительность.
Индекс одного поля
Что, если нам придется искать по идентификатору пользователя? Давайте посмотрим.
db.users.find({"user_id": 158021798}).explain("executionStats")
{
...
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 523,
"totalKeysExamined" : 0,
"totalDocsExamined" : 1000000,
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"user_id" : {
"$eq" : 158021798
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 11,
"works" : 1000002,
"advanced" : 1,
"needTime" : 1000000,
"needYield" : 0,
"saveState" : 1000,
"restoreState" : 1000,
"isEOF" : 1,
"direction" : "forward",
"docsExamined" : 1000000
}
},
...
}
Здесь все выглядит не очень хорошо. Время выполнения уже составляет полсекунды (523 миллисекунды), а коэффициент равен 1000000. Все документы были проверены, а это значит, что Mongo должен прочитать каждый документ, как правило, с диска. Но это уже слишком, учитывая, что был возвращен только один документ. И это то, что означает этап COLLSCAN. Вся коллекция просканирована. Это, конечно, наихудший сценарий. Если поиск по идентификатору пользователя является обязательным, мы должны создать индекс по полю user_id:
db.users.createIndex({"user_id": 1})
1 указывает возрастающий порядок, в котором хранится индекс. Когда индексы охватывают только одно поле, не имеет значения, в каком порядке мы размещаем его: по возрастанию (1) или по убыванию (-1), потому что Mongo может читать их в любом направлении. Однако, когда мы помещаем более одного атрибута в индекс, а запрос требует сортировки, направление индексов важно, хотя мы не собираемся обсуждать его в этом посте, поэтому мы создадим их все с восходящим направлением.
Итак, давайте снова запустим запрос:
db.users.find({"user_id": 158021798}).explain("executionStats")
{
...
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 2,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 1,
...
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 1,
...
"indexName" : "user_id_1",
...
}
Здесь мы видим два этапа. Сначала мы просканировали индексы (IXSCAN), используя индекс с именем user_id_1, который является именем по умолчанию только что созданного индекса. Из отсканированных индексов был взят 1 документ (FETCH). Таким образом, благодаря использованию индекса мы изучили только тот документ, который нам был нужен, а не миллион, который мы изучали раньше. Благодаря этому нам удалось сделать запрос за 2 миллисекунды, в отличие от предыдущих 523.
Составной индекс
А что, если мы хотим извлечь документы, в которых пользователь любит пинг-понг и выиграл более 900 матчей?
db.users.find({
"favourite_sports":"ping-pong",
"stats.matches_won": {$gt: 900}
})
Мы можем создавать составные индексы, и не только это, их также можно применять к спискам (например, любимые виды спорта) или встроенным документам (например, статистике):
db.users.createIndex({
"favourite_sports": 1,
"stats.matches_won": 1
})
Резюме
Следовательно, для улучшения вашей системы вы должны определить, какие шаблоны запросов требуются вашей системе, и на их основе создать соответствующие индексы. Имейте в виду, что они не бесплатны. Каждый индекс занимает место, и планировщику Mongo потребуется больше времени, чтобы решить, какой индекс использовать для каждого запроса, чем больше индексов будет. Следовательно, у нас должны быть только необходимые индексы.
И все. В более продвинутом посте мы обязательно обсудим более сложные концепции, такие как индексы TTL, правило ESR для правильного определения порядка индексов или как правильно управлять индексами в продакшен среде.