Вступление
Многие из нас любят NoSQL. И MongoDB среди них является одним из топ-любимчиков. Очень часто мы выбираем нашу «Монгу» за гибкость и скорость. И это вполне логично, ведь MongoDB почти никогда не подводит... сразу. Неприхотливая, шустрая, удобная - она ведет себя как идеальный помощник: не требует лишнего, принимает любые данные, не задаёт неудобных вопросов про схему и с готовностью отвечает на каждый запрос за считанные миллисекунды.
Но потом ты начинаешь подозревать что-то неладное. И, что самое главное, происходит это не сразу, а постепенно. Сначала один запрос начинает задерживаться немного дольше обычного, потом еще один. Там, где раньше было 10-20 миллисекунд, становится 100. Ты замечаешь, что графики ведут себя странно. И начинаешь искать причину: грешишь то на версию софта, то на железо, то думаешь, что сама MongoDB какая-то не такая.
Но ответ очень часто лежит на поверхности: MongoDB не становится медленной сразу. Она лишь честно исполняет те правила, которые ей задали. И если присмотреться, почти за каждым снижением производительности стоит вполне конкретный антипаттерн.
В своей статье я предлагаю разобрать распространенные антипаттерны, которые встречаются при проектировании и работе с MongoDB. Также посмотрим на реальные известные случаи пользователей, которые в своей работе сталкивались с проблемами с MongoDB.
Итак, начнем.

1. Огромные документы (он же «over-embedding»)
Когда мы только начинаем работу с NoSQL, приходит обманчивое ощущение, что сейчас мы денормализуем тут всё и это будет работать очень быстро. И, казалось бы, MongoDB поощряет embedding, но проблема в том, что многие заходят слишком далеко.
Как примерно это может выглядеть (и как делать не стоит):
{
"_id": 24,
"name": "Roman",
"orders": [
{
"order_id": 101,
"total": 100,
"items": [
{
"product_id": 123,
"price": 100
}
]
}
]
}И представьте, что внутри orders могут быть сотни и тысячи элементов.
Нюанс заключается в том, что MongoDB читает весь документ целиком, даже если вам нужен один элемент.
И если говорить о реальных цифрах, то просто представьте:
Документ 2 KB: 2-3 мс
Документ 10 MB: уже более 120 мс
Важно помнить, что в MongoDB есть лимит - 16 MB на документ, и рано или поздно такая модель в него упрётся.
Как в такой ситуации было бы лучше:
Отдельно users
{
"_id": 24,
"name": "Roman"
}Отдельно orders
{
"_id": 101,
"user_id": 24,
"total": 100,
"items": [
{
"product_id": 123,
"price": 100
}
]
}2. Неограниченные массивы (они же «unbounded arrays»)
Частный случай вышеописанного антипаттерна - это попытка запихнуть всё в один массив. Но в отличие от первого кейса, где мы встраиваем слишком много связанных данных внутрь документа, тут речь идет именно про один массив, который растёт без лимита.
На старте это кажется удобным и быстрым. Но со временем превращается в проблему, которая убивает производительность.
Как делать не стоит:
{
"_id": 14,
"title": "Trip Tips",
"comments": [
{
"user_id": 101,
"text": "Nice vacation!"
},
{
"user_id": 122,
"text": "It is great!"
}
]
}Важно отметить, что тут мы говорим о том, что комментариев потенциально много (десятки, сотни тысяч) и они часто добавляются.
Как было бы лучше:
Отдельно posts
{
"_id": 14,
"title": "Trip Tips"
}Отдельно comments
{
"_id": 29,
"post_id": 14,
"user_id": 101,
"text": "Nice vacation!"
}3. Глубокая вложенность (она же «deep nesting»)
Ещё одна частая ошибка при проектировании в MongoDB - чрезмерная вложенность документов. Идея обычно такая: раз уж MongoDB позволяет хранить JSON - давайте вложим всё максимально глубоко.
На практике это быстро начинает мешать.
Как делать не стоит:
{
"_id": 12,
"profile": {
"personal": {
"name": "Ivan",
"contacts": {
"email": "ivan@mail.com",
"phones": [
{
"type": "mobile",
"history": [
{
"number": "+123",
"meta": {
"verified": true
}
}
]
}
]
}
}
}
}Думаю, что вы начинаете догадываться, чем это может быть чревато. И тут важно понимать, что проблема не в самой по себе вложенности (иногда умеренная вложенность имеет место быть), а в избыточной вложенности без какой-либо пользы, усложнении доступа к данным в сочетании с растущими структурами. К таким чудо-документам появляются сложные запросы, это ведет к худшей читаемости, выше шанс ошибок, сложнее поддержка. Плюс к этому всему - индексы начинают работать очень плохо.
Как можно начинать раскладывать: users (основной документ)
{
"_id": 12,
"name": "Ivan",
"email": "ivan@mail.com"
}phones (телефоны пользователя)
{
"_id": 1,
"user_id": 12,
"type": "mobile",
"number": "+123",
"verified": true
}phone_history (история изменений)
{
"_id": 2,
"phone_id": 1,
"number": "+123",
"verified": true,
"changed_at": "2026-01-01T10:00:00Z"
}Глубокая вложенность в MongoDB приводит к сложным запросам и проблемам с масштабированием. Хорошая практика - выносить сущности, которые могут расти или изменяться независимо (например, телефоны и их историю), в отдельные коллекции и связывать их через ссылки.
4. Отсутствие индексов
Один из самых распространенных примеров того, как можно перегрузить Mongo DB - это неиспользование индексов.
Предположим, мы хотим выполнить:
db.customers.find({ email: "test@mail.com" })А далее мы видим магическую надпись COLLSCAN. По итогу это означает, что MongoDB делает полное сканирование коллекции (Collection Scan), проверяет все документы подряд и фильтр применяется после чтения. И предположим, что у вас 1 000 000 документов, а совпадает только 1. Как нужно сделать:
db.customers.createIndex({ email: 1 })И после создания индекса запрос обычно выполняется через IXSCAN, а не через COLLSCAN. Важно понимать, что индекс - это B-дерево, т.е. поиск идёт по структуре, а не по всем данным. Но тут стоит учесть тот факт, что для очень маленьких коллекций (сотни документов) COLLSCAN может быть сопоставим по скорости с использованием индекса, поэтому индекс не всегда обязателен. И с другой стороны, избыточное количество индексов также может являться проблемой, так как увеличивает накладные расходы на запись.
5. Большой полиморфизм коллекции (при отсутствии схемы)
Это один из самых недооценённых антипаттернов в MongoDB и заключается он в том, что на маленьких объёмах работает ещё более менее «нормально», а потом внезапно начинает барахлить.
Суть в том, что мы начинаем хранить разные сущности в одной коллекции.
Пример документов в одной коллекции:
{
"_id": 1,
"type": "user",
"name": "Anna",
"email": "a@mail.com"
}
{
"_id": 2,
"type": "order",
"user_id": 1,
"total": 100
}
{
"_id": 3,
"type": "product",
"price": 50
}По итогу весь этот винегрет лежит у нас в одной коллекции, различается только type.
Нужно понимать, что MongoDB использует индексы на уровне коллекции. Когда в одной коллекции лежат разные структуры, поля не пересекаются (email, total, price), индексы становятся разреженными и малополезными, появляются огромные составные индексы. По итогу получается, что индексов много, вдобавок каждый используется редко, а память под них расходуется впустую.
Как лучше это было бы сделать? Разделить по разным коллекциям.
Users:
{
"_id": 1,
"name": "Anna",
"email": "a@mail.com"
}Orders:
{
"_id": 2,
"user_id": 1,
"total": 100
}Products:
{
"_id": 3,
"price": 50
}Тут стоит отметить, что умеренный полиморфизм иногда уместен, но только если структуры очень похожи. В целом допустимо, если документы имеют общий набор полей и отличаются незначительно.
Например:
{
"type": "payment",
"amount": 100,
"method": "card"
}
{
"type": "payment",
"amount": 200,
"method": "cash",
"change": 50
}6. Излишнее чтение «всего документа» (он же «overfetching»)
Нередко возникает такая ситуация, что нам необходимо получить информацию по определенному полю в конкретном документе.
Предположим, что у нас есть документ, с такой структурой:
{
"_id": 1,
"email": "olga@test.com",
"user_name": "olga123",
"first_name": "Olga",
"last_name": "Ivanova",
"phone": "+1234567890",
"gender": "female",
"country": "Russia",
"city": "Moscow",
"postal_code": "000000",
"address_line_1": "Street 1",
"address_line_2": "Apt 10",
"company": "TechCorp",
"job_title": "Software Engineer",
"language": "en-US",
"timezone": "Europe/Moscow",
"preferred_currency": "RUB",
"status": "active"
}И предположим, что нам нужно найти только "email" у этого пользователя.
Как обычно в таком случае делают запрос:
db.users.find({ _id: 1 })Но что в итоге происходит? Даже при точечном запросе по _id, MongoDB возвращает весь документ целиком, даже если нам нужно только одно поле, а документ может содержать десятки полей и внутри могут быть вложенные структуры.
Как в таком случае было бы лучше?
Используем выборку только нужных полей.
db.users.find({ _id: 1 }, { email: 1, _id: 0 })Но это был пример с одним документом. А теперь представим, что нам нужно получить "email" всех активных пользователей. Хороший вариант - это:
db.users.find(
{ status: "active" },
{ email: 1, _id: 0 }
)7. Агрегации без $match в начале
Предположим, что нам нужно посчитать статистику по пользователям и оставить только активных. Структура в коллекции, предположим, такая:
{
"_id": 1,
"user_id": 42,
"status": "active",
"amount": 100
}Частое заблуждение - сначала выполнить агрегацию, а затем фильтрацию. И получается:
[
{
$group: {
_id: {
user_id: "$user_id",
status: "$status"
},
total: { $sum: "$amount" }
}
},
{
$match: { "_id.status": "active" }
}
]Нюанс в том, что в таком варианте MongoDB вынуждена агрегировать все документы, а фильтрация происходит уже после группировки. А как стоило бы:
[
{
$match: { status: "active" }
},
{
$group: {
_id: "$user_id",
total: { $sum: "$amount" }
}
}
]То есть сначала $match, а потом $group. Тут важно уловить суть, что часто мышление происходит по логике «сначала агрегировать, потом фильтровать результат», тогда как в MongoDB во многих случаях нужно «сначала отфильтровать данные, потом агрегировать».
Пример интересных кейсов у реальных пользователей
1. «При росте данных с 30k до 100k всё стало в 30 раз медленнее.»
было: ~20 мс
стало: 600 мс и более
2. «Производительность Mongo при выполнении агрегационных запросов крайне низкая.»
5+ млн документов
запросы падают или не выполняются
3. «Всего 750 документов, а до ответа 40 секунд»
Итоги
Если говорить в целом, то конечно, антипаттернов и решений, как исправить ту или иную проблему, гораздо больше. В этой статье я осветил лишь некоторые из них, которые встречаются достаточно часто.
Резюмируя, можно сделать вывод, что практически все проблемы производительности MongoDB сводятся к трём вещам:
1. Чтение большого объема лишних данных
2. Отсутствие и неиспользование индексов (или использование их некорректно)
3. Попытка мыслить как в SQL
Ради забавы можно сформулировать данный посыл цитатой волка (или Стэйтема - кому как удобнее):
«MongoDB быстрый, пока ты помогаешь ему быть быстрым!»

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



















