В прошлом уроке мы увидели, как MongoDB держит данные целыми и надёжными. Самая сильная защита там была не транзакция, а удачная модель: то, что меняется вместе, лежит в одном документе. Сегодня практика. Спроектируем с нуля две схемы, блог и магазин, и пройдём весь путь: от вопросов приложения к документам.
Сначала меню, потом кухня
Ресторан не строят так: сначала кухню, а потом думают, что подавать. Сначала пишут меню, а под него ставят плиты и холодильники. Если в меню пицца, нужна печь, а не фритюрница.
В реляционной базе обычно начинают с сущностей и нормальных форм, а запросы пишут потом. В MongoDB порядок обратный. Сначала выписывают шаблоны доступа: какие запросы приложение делает чаще всего, что читается вместе и что меняется. Схема подстраивается под этот список, как кухня под меню.
План на оба примера один:
- Выписать главные запросы и прикинуть частоту.
- Для каждой связи ответить на вопросы из урока про встраивание: читают вместе? сколько штук? меняется отдельно?
- Решить, что встроить, на что сослаться и что скопировать.
- Проверить схему запросами.
Блог
Запросы
Блог о книгах. Что он делает чаще всего:
| Запрос | Частота |
|---|---|
| лента: последние посты с названием, автором и числом комментариев | на каждом открытии главной |
| страница поста с текстом и последними комментариями | очень часто |
| все комментарии к посту по порядку | когда читатель нажал «показать все» |
| посты по метке | часто |
| новый комментарий | редко по сравнению с чтением |
Решения
Автор поста. В ленте нужно имя автора, а не вся его анкета. Значит, в пост кладём сводку { _id, name }: это копия-сводка из урока про денормализацию. Имя меняется редко, а читается на каждой главной.
Метки. Их у поста несколько штук, и вне поста они не живут. Массив строк внутри поста.
Комментарии. Здесь главный выбор. Встроить их в пост соблазнительно: пост и комментарии читаются вместе. Но под популярным постом комментариев могут быть тысячи, а это массив без верхней границы, антипаттерн, который рано или поздно упрётся в 16 МБ. Поэтому все комментарии живут в своей коллекции со ссылкой post_id.
Последние комментарии и счётчик. Странице поста нужны три свежих комментария и общее число. Ходить за ними во вторую коллекцию на каждом открытии дорого. Держим в посте их копию: массив из трёх последних и счётчик. Такой приём называют шаблоном подмножества: в документе часть, полный список отдельно.
Схема
db.posts.insertMany([
{
_id: 1, title: 'Почему я перечитываю Булгакова', author: { _id: 1, name: 'Ольга' },
tags: ['книги', 'классика'], created_at: ISODate('2026-09-01'),
comments_count: 0, recent_comments: []
},
{
_id: 2, title: 'Пять книг об алгоритмах', author: { _id: 2, name: 'Павел' },
tags: ['книги', 'программирование'], created_at: ISODate('2026-09-20'),
comments_count: 0, recent_comments: []
}
])
Новый комментарий это две записи: документ в comments и обновление поста. Счётчик и подмножество меняются одной командой, а $slice: -3 оставляет в массиве только три последних:
db.comments.insertOne({ post_id: 1, user: 'Роман', text: 'Согласен, каждый раз новое', created_at: ISODate('2026-09-02') })
db.posts.updateOne(
{ _id: 1 },
{
$inc: { comments_count: 1 },
$push: { recent_comments: { $each: [{ user: 'Роман', text: 'Согласен, каждый раз новое' }], $slice: -3 } }
}
)
Если между этими двумя командами сервер упадёт, комментарий будет, а копия в посте отстанет. Для блога это допустимо: копию можно пересчитать фоновой задачей. Для денег такой подход не годится, там нужна транзакция.
Проверка запросами
Лента, одним запросом и без похода в другие коллекции:
db.posts.find({}, { title: 1, 'author.name': 1, comments_count: 1 }).sort({ created_at: -1 })
[
{
_id: 2,
title: 'Пять книг об алгоритмах',
author: { name: 'Павел' },
comments_count: 0
},
{
_id: 1,
title: 'Почему я перечитываю Булгакова',
author: { name: 'Ольга' },
comments_count: 1
}
]
Посты по метке находит обычный фильтр по массиву:
db.posts.find({ tags: 'классика' }, { _id: 0, title: 1 })
[ { title: 'Почему я перечитываю Булгакова' } ]
Все комментарии к посту, в порядке написания:
db.comments.find({ post_id: 1 }, { _id: 0, user: 1, text: 1 }).sort({ created_at: 1 })
[ { user: 'Роман', text: 'Согласен, каждый раз новое' } ]
Каждый запрос из таблицы закрыт одним обращением к одной коллекции. Это и есть признак удачной схемы.
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.