Выбор правильной модели данных MongoDB: Вложенные документы против ссылочных документов
Выбирайте встроенные или ссылочные документы MongoDB на основе шаблонов доступа, роста, согласованности и поведения обновления.
Выбор правильной модели данных MongoDB: встроенные и ссылочные документы
Моделирование данных в MongoDB обычно сводится к одному практическому вопросу: должны ли связанные данные находиться внутри одного документа, или один документ должен ссылаться на другой? Этот выбор влияет на скорость чтения, стоимость обновления, рост документа и объем работы по обеспечению согласованности, которую должно выполнять ваше приложение.
Это руководство глубоко погружается в компромиссы между встраиванием документов в родительский документ и ссылками на связанные документы в разных коллекциях. Понимание того, когда и как применять эти методы, позволит вам проектировать эффективные, высокопроизводительные схемы MongoDB, адаптированные к конкретным шаблонам доступа вашего приложения.
Понимание стратегий моделирования данных MongoDB
MongoDB организует данные в документы (похожие на объекты JSON), хранящиеся в коллекциях. Связи между этими документами могут быть смоделированы с использованием двух основных шаблонов:
- Встраивание (денормализация): Хранение связанных данных непосредственно внутри родительского документа.
- Ссылки (нормализация): Хранение только ссылки (например,
_id) на связанный документ в другой коллекции, аналогично внешнему ключу.
1. Шаблон встраивания (денормализация)
Встраивание предполагает размещение одного документа непосредственно внутри другого. Этот метод очень популярен в MongoDB, когда связи данных являются "один к нескольким" или когда связанные данные часто извлекаются вместе с родительским документом.
Когда использовать встраивание
Используйте шаблон встраивания, когда:
- Данные извлекаются вместе: Если вам почти всегда нужны связанные данные при запросе родителя, встраивание минимизирует количество операций с базой данных, необходимых для получения полного набора информации.
- Связи "один к нескольким": Идеально подходит для отношений, где массив встроенных документов остается относительно небольшим и предсказуемым (например, последние 10 действий входа пользователя или позиции заказа).
- Согласованность данных критична: Встроенные данные по своей сути согласованы, поскольку находятся в одном документе, что упрощает гарантии атомарности, предоставляемые одно-документными ACID-транзакциями MongoDB.
Пример встраивания
Рассмотрим Product и его Reviews. Если отзывы часто извлекаются вместе с продуктом и общее количество отзывов управляемо:
// Документ коллекции Product
{
"_id": ObjectId("..."),
"name": "Высокопроизводительный SSD",
"price": 129.99,
"reviews": [
{
"user": "Алиса",
"rating": 5,
"comment": "Самый быстрый накопитель!"
},
{
"user": "Боб",
"rating": 4,
"comment": "Отличное соотношение цены и качества."
}
]
}
Недостатки встраивания
- Ограничения размера документа: Документы MongoDB имеют максимальный размер 16 МБ. Если массив встроенных документов растет без четких границ, вам может потребоваться ссылки или сегментирование.
- Накладные расходы на обновление: Большие встроенные массивы могут сделать обновления более дорогими и увеличить конкуренцию за родительский документ.
- Дублирование данных: Если встроенные данные необходимо совместно использовать или отображать независимо от родителя, вы рискуете дублированием данных и проблемами с конечной согласованностью, если обновления не синхронизированы во всех копиях.
2. Шаблон ссылок (нормализация)
Ссылки имитируют концепцию внешних ключей в реляционных базах данных. Вместо встраивания связанных данных вы сохраняете _id или другой стабильный идентификатор связанного документа. Это часто требует второго запроса, этапа агрегации $lookup или соединения на стороне приложения для получения связанных данных.
Когда использовать ссылки
Используйте шаблон ссылок, когда:
- Связи "один ко многим" или "многие ко многим": Когда одна сторона отношения может расти бесконечно (например, количество комментариев к сообщению в блоге или пользователи, принадлежащие к нескольким группам).
- Данные используются совместно несколькими родителями: Если объект связанных данных необходимо независимо обновлять и к нему должны обращаться несколько других документов (например, документ
Category, используемый многими документамиProduct). - Большие наборы данных: Когда встраивание нарушило бы ограничение размера документа в 16 МБ.
Типы ссылок
A. Ручные ссылки (соединения на стороне приложения)
Хранение _id в родительском документе:
// Коллекция Author
{
"_id": ObjectId("author123"),
"name": "Jane Doe"
}
// Коллекция Book
{
"_id": ObjectId("book456"),
"title": "Моделирование данных 101",
"author_id": ObjectId("author123") // Ссылка
}
Чтобы получить имя автора, вы выполняете два запроса или используете $lookup:
// Пример использования $lookup в агрегационном фреймворке
db.books.aggregate([
{ $match: { title: "Моделирование данных 101" } },
{
$lookup: {
from: "authors", // Коллекция для соединения
localField: "author_id", // Поле из входных документов (books)
foreignField: "_id", // Поле из документов коллекции 'from' (authors)
as: "author_details"
}
}
]);
B. Двунаправленные ссылки
Для двусторонних отношений вы также можете ссылаться на родителя в дочернем документе. Это упрощает навигацию по отношениям в обоих направлениях, хотя увеличивает накладные расходы на запись, поскольку обновления должны выполняться в двух местах.
Недостатки ссылок
- Повышенная сложность запросов: Получение полностью денормализованных данных требует соединений (либо через код приложения, либо через
$lookupMongoDB), что может быть медленнее, чем одна операция чтения встроенных данных. - Управление согласованностью: Если вы дублируете выбранные поля из ссылочного документа, например, отображаемое имя автора в записи книги, вы должны обновлять эти копии или мириться с временной устарелостью.
Резюме: Как сделать правильный выбор
Решение между встраиванием и ссылками вращается вокруг шаблонов доступа. Спросите себя: Как часто извлекаются эти связанные данные? Как часто они меняются? Они маленькие или потенциально огромные?
| Особенность / Соображение | Встраивание (денормализация) | Ссылки (нормализация) |
|---|---|---|
| Производительность чтения | Отлично (один запрос) | Хорошо до удовлетворительно (требует соединений) |
| Производительность записи | Может быть дорого для больших или "горячих" родительских документов | Часто проще для независимых документов |
| Ограничение размера данных | Ограничено лимитом документа в 16 МБ | Избегает одного огромного родительского документа, но все равно требует тщательного проектирования индексов и лимитов запросов |
| Тип отношения | Один к нескольким | Один ко многим, многие ко многим |
| Согласованность данных | Высокая (атомарные записи) | Управляется вручную (потенциальная устарелость) |
Совет по лучшим практикам: Начните со встраивания, переключайтесь позже
Распространенная и эффективная стратегия — начать со встраивания данных, которые, как вы знаете, часто читаются вместе. Это оптимизирует для типичного случая. Если позже вы столкнетесь с узкими местами производительности из-за большого роста документа или чрезмерной сложности обновления, вы можете переключить эти конкретные данные в отдельную коллекцию и перейти на ссылки.
Вывод
Схемы MongoDB работают лучше всего, когда они соответствуют вашим реальным запросам. Встраивайте данные, которые вы читаете вместе и которые можно ограничить. Ссылайтесь на данные, которые растут независимо, используются многими родителями или изменяются по своему собственному расписанию. Прежде чем остановиться на модели, запишите свои основные операции чтения и записи, а затем протестируйте эти пути с реалистичными размерами документов.