DevGang
Авторизоваться

Основы индексов 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 для правильного определения порядка индексов или как правильно управлять индексами в продакшен среде.

#MongoDB
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

В подарок 100$ на счет при регистрации

Получить