Большой туториал по MongoDB

Индексация

Merrick
7 min readDec 22, 2017

Вступление

Благодаря индексам мы можем делать более эффективные запросы в базу. Без индексирования MongoDB при запросе должен выполнить сканирование всей коллекции, то есть обойти каждый документ, чтобы выбрать документ, который подходит под ваш запрос. Если для запроса существует индекс, то он может ограничить количество документов, которое должен обойти MongoDB. Индексы — это специальный тип данных, которые хранят небольшую часть набора данных коллекций в форме удобной для обхода. Индекс хранит значение определенного поля или набора полей, упорядоченных по значению поля. Упорядочение записей индекса поддерживает эффективное сопоставление и запросы на основе диапазоне. Также индексы могут возвращать отсортированный результат, используя порядок в индексе.

Фундаментально индексы в MongoDB аналогичны индексам в других базах данных. MongoDB определяет индексы на уровне коллекций и поддерживает индексы в любом поле документов в коллекции MongoDB.

Придеставление индекса

Чтобы разобраться с индексами нужно провести аналогию. Представьте, что у вас есть адресная справка с 5000 записей, у которых есть такие пункты как имя, фамилия, улица, пол, возраст и пункт о наличии судимости. Но есть два огромных недостатка, во первых все записи расположены в случайном порядке, а во вторых нет алфавитного указателя ни по фамилиям, ни по именам. Отсюда встает вопрос, если нет указателя как найти Иванова Ивана? Только обойти все записи.В худшем случае Ваня будет самым последним.

Если мы расположим все записи в алфавитном порядке, то это позволит нам ускорить поиск. Также мы можем с помощью индексов разбить всех людей, по улицам и значительно сократить выборку, зная улицу. Например мы разобьем всех людей по улицам и на улице Блюхера у нас будет 10 записей, это гораздо меньше чем 5000, поэтому зная, что у нас запись с улицы блюхера мы можем обойти только эти 10 записей.

Default индекс

Самый простой пример индекса — _id. MongoDB создает уникальный индекс для поля _id во время создания документа. Индекс _id не позволяет клиентам вставлять два документа с тем же значением для поля _id. Вы не сможете удалить поле _id из документа. Поскольку это поле проиндексировано, то идентификатор каждого документа хранится также в индексе, что позволяет быстро искать документы по _id.

В шардируемых кластерах если вы не используете _id как шард ключ, то ваше приложение должно обеспечить уникальность _id, чтобы предотвратить ошибки.

Создание индексов

Для создания индекса нужно использовать метод или похожий метод из драйвера:

db.collection.createIndex(
<key and index type specification>,
<options>
)

Метод createIndex создает индекс если его еще нет. MongoDB использует B-tree структуру данных для хранения деревьев.

Типы индексов

MongoDB предоставляет ряд различных типов индексов для поддержки конкретных типов данных и запросов.

Одно поле — MongoDB поддерживает создание пользовательских возрастающих и уменьшающихся индексов на одно поле документа.

Для однополевого индекса и операций сортировки. Порядок не имеет значения поскольку MongoDB может обойти индексы в любом порядке.

db.users.createIndex(
{ email: 1 }
)

Составной индекс — MongoDB также позволяет определить составные индексы, те индексы которые состоят из нескольких полей. Порядок полей в составном индексе имеет значение. Например если составной индекс состоит из { city: 1, email: -1 } индекс сортируется сначала по city и потом в пределах city по email.

db.users.createIndex(
{ city: 1, email: -1 }
)

Multikey индексы

MongoDB использует multikey для индексации содержимого, хранящегося в массивах. В таких индексах допускается несколько записей в массиве указывающих на один и тот же документ. К примеру есть документ с несколькими тегами:

{
name: 'Merrick'
tags: [ 'tools', 'Mongodb', 'express' ]
}

Если создать индекс по полю тег то в нем будут перечислены все значения из tags следовательно для поиска требуется указать любой из его тегов. Именно в этом и состоит идея многоключевого индекса. Несколько ключей указывают на одну запись.

Свойства индексов

Уникальные индексы — Уникальное свойство для индекса заставляет MongoDB отклонять повторяющиеся значения для проиндексированного поля. Помимо уникального ограничения, уникальные индексы функционально взаимозаменяемы с другими индексами MongoDB.

db.users.createIndex(
{ email: 1 },
{ unique: true }
)

При попытке вставить документ с проиндексированным полем которое уже есть, будет следующее исключение:

E11000 duplicate key error index:

Если используется драйвер, то ошибка будет перехвачена только в том случае если вставка производится в безопасном режиме.

Разреженные индексы — По умолчанию индексы плотные, это значит, что каждому документу в индексированной коллекции соответствует запись в индексе, даже если в документе нет индексируемого ключа. К примеру возьмем коллекцию товаров из интернет магазина у которых есть разделение по категориям. Тем не менее у нас могут иметься товары которые не относятся ни к одной категории. Несмотря на это, в индексе могут присутствовать пустые записи для них. Найти все такие пустые записи поможет:

db.product.find({ category_ids: null })

В таком случае mongoDB может оптимизировать запрос по индексу, но есть два случая когда нужно избегать плотных индексов. Когда требуется уникальный индекс в некоторых документах поле может отсутствовать В таком случае первый элемент будет вставлен, а все последующие будут с ошибкой, потому что уникальный индекс с значение null уже существует. В такой ситуации плотный индекс не годиться, а требуется разреженный. В разряженном индексе представляются документы у которых поле имеет какое то значение.

db.users.createIndex(
{ email: 1 },
{ sparse: true }
)

TTL Indexes — это специальные индексы, которые MongoDB может использовать для автоматического удаления документов из коллекции через определенное время. Это идеально подходит для определенных типов информации, таких как машинные события, данные журнала и информация о сеансе, которые должны сохраняться в базе данных в течение конечного времени.

// Удалить документ через 2 минуты
db.users.createIndex(
{ email: 1 },
{ expireAfterSeconds: 120 }
)

Имя индекса — индексу также можно задать имя, для его дальнейшего удаления или поиска.

db.users.createIndex(
{ email: 1 },
{ name: 'catIdx' }
)

Эффективность индексов

Хотя индексы и позволяют повысить поиск, но с каждым индексом связаны дополнительные накладные расходы. Каждый раз когда в коллекцию добавляется документ, его также нужно добавить во все индексы связанные с этой коллекцией. Получается если над коллекцией построено 10 индексов, то при добавлении документа, нужно изменить 10 разных структур данных. И это относится к любой операции записи, добавления, удаления, обновления полей документа, которые проиндексированы. Для приложений ориентированных на чтение, затраты на индексы почти всегда оправданы. Следите за тем, чтобы индексы которые есть использовались, а если не используются то удаляйте их.

Чтобы удалить индекс пользуйтесь командой:

db.collection.dropIndex("catIdx");

Где catIdx это название индекса. Чтобы посмотреть все индексы которые относятся к коллекции, нужно воспользоваться командой:

db.collection.getIndexes();
// В ответе мы получим следующее
[
{ "v" : 1,
"key" : { "_id" : 1 },
"ns" : "test.pets",
"name" : "_id_"
},
{
"v" : 1,
"key" : { "cat" : -1 },
"ns" : "test.pets",
"name" : "catIdx"
},
{
"v" : 1,
"key" : { "cat" : 1, "dog" : -1 },
"ns" : "test.pets",
"name" : "cat_1_dog_-1"
}
]

Также мы можем удалить все индексы сразу:

db.collection.dropIndexes()

Сопоставление в индексах

Вы можете думать об этом как о способе сортировать и сравнивать строки относительно языковых особенностей. Представьте, что мы добавили два документа в коллекцию:

db.users.insertMany([{ name: 'Merrick' }, { name: 'Mérrick' }])

И в какой то параллельной вселенной это пишется и так и так. И чтобы выполнить поиск по данным поля максимально просто в MongoDB есть collation который позволяет по специфики языка искать в строках относительно некоторого количества символов, к примеру создадим следующий запрос:

db.cities.find(
{ name: 'San Jose' }
).collation({ locale: 'en_US', strength: 1 })

strength: 1 будет игнорировать диакритические знаки. Мы там же можешь отсортировать документ по чувствительности к регистру.

db.words.find({})
.sort({ v: 1 })
.collation({ locale: 'en_US', caseLevel: true })

Поэкспериментируйте со своими данными сами и посмотрите как они меняются.

db.files.find()
.sort({ name: 1 })
.collation({ locale: 'en_US', numericOrdering: true })

Также в порядке возрастания строк которые имею цифры в значении

Например, коллекция myColl имеет индекс поля category строки с локализацией сортировки fr.

db.myColl.createIndex(
{ name: 1 },
{ collation: {
locale: "fr"
}
}
)

Следующая операция запроса, которая указывает ту же строку, что и индекс, может использовать индекс:

db.myColl.find( { category: "cafe" } ).collation( { locale: "fr" } )

Однако следующая операция запроса, которая по умолчанию использует «простой» двоичное сравнение, не может использовать индекс:

db.myColl.find( { category: "cafe" } )

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

Например, коллекция myColl имеет составной индекс для числовых полей score price и строковое поле category; индекс создается с локализацией сортировки fr для сравнения строк:

db.myColl.createIndex(
{ score: 1, price: 1, category: 1 },
{ collation: { locale: "fr" } } )

Следующие операции, которые используют ‘простое’ двоичное сравнение для строк, могут использовать индекс:

db.myColl.find( { score: 5 } ).sort( { price: 1 } )
db.myColl.find( { score: 5, price: { $gt: NumberDecimal( "10" ) } } ).sort( { price: 1 } )

Следующая операция которая использует ‘простое’ бинарное сравнение для строк на индексируемом поле category. Мы можем использовать индекс, чтобы выполнить только score: 5 запрос:

db.myColl.find( { score: 5, category: "cafe" } )

Покрытие запросов

Когда критерии запроса включают только индексированные поля, MongoDB будет возвращать результаты непосредственно из индекса без сканирования каких-либо документов или переноса документов в память. Эти охватываемые запросы могут быть очень эффективными.

Оптимизация запросов

Как очень просто выявить медленный запрос и вообще, что такое медленный запрос? Ну попробуйте выполнить find в коллекции где один документ и в коллекции где их миллион. Но с использованием индексов можно добиться скорости как в однодокументной из миллионной. Для мониторинга того, сколько потребовалось запросу, чтобы выполниться воспользуемся методом explain:

db.users.find({})
.sort({ nickname: 1 })
.limit(1)
.explain('allPlansExecution')

Такой безобидный запрос может занять у нас в миллионной коллекции около 3 секунд. А вернет он примерно следующее:

{
...
millis: 14538
...
}

Это значит, что у нас запрос длится более 14 секунд и его следует оптимизировать. Так же для обнаружения медленных запросов мы можем использовать профилирование:

db.setProfilingLevel(1, 50)

В коллекцию system.profile будут попадать все запросы которые выполняются дольше 50 мc, но стоит учесть, что у системных коллекций ограничена память и на профайлер выделяется всего 1мб. Отключить лог можно следующим образом.

db.setProfilingLevel(0)

Мы можем анализировать коллекцию профайлера, так же как и остальные:

db.system.profile.find({ millis: { $gt: 50 } })

Найдет все запросы более 50 мс. Но запросы выполняются довольно долго и лучше всего выявлять долгие запросы еще на этапе разработки через команду explain. В проблеме которая была выше, у нас все довольно просто, достаточно построить индекс:

db.users.createIndex({ nickname: 1 })

Теперь попробуем выполнить тот же поиск:

db.users.find({})
.sort({ nickname: 1 })
.limit(1)
.explain('allPlansExecution')

И о чудо запрос выполняется меньше чем за одну миллисекунду!

Старайтесь делать индексы универсальными и в тоже время специально под запросы.

--

--

Merrick

Переодически пишу про веб разработку. Любая поддержка сюда — https://www.tinkoff.ru/sl/3tsMbIw4Mqo