Вступление
Многие из нас любят 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 быстрый, пока ты помогаешь ему быть быстрым!»

Полезные ссылки про антипаттерны
Комментарии (3)

SkillMax999
27.04.2026 08:30Подборка топ, материал прям полезный, я как раз хотел сам в своё время про это написать. Но тут и так всё круто расписано)

PopovPS
27.04.2026 08:30Убираем вложенность, добавляем правильные индексы, не храним большие документы... Как будто если после этого логичный шаг перейти на реляционную БД и тогда вообще огонь будет...
WillyIBig
Классная статья, спасибо за примеры и пояснения. Несколько раз тоже приходилось сталкиваться со снижением производительности.