Деление данных по коллекциям в MongoDB

Опубликовано 21 December 2015 в Разное

Не надо лезть в MongoDB с реляционным подходом. Этот тезис вроде бы очевиден, но когда дело доходит до реализации, то возникает множество вопросов. И 100% одним из них будет "как делать join?" Правильный ответ: никак. Да, в 3.2 появился $lookup в агрегациях. Отчасти это замена классическому join из реляционного мира. Но в целом агрегации не самый быстрый в монге инструмент. Лучше когда запрос идет к одной коллекции и это обычный find.

Стоит отметить, что использование NoSQL не обходится бесплатно. К примеру, обеспечение консистентности данных встроено в реляционные базы по умолчанию. При использовании MongoDB, задача обеспечить консистентность ложится на программу-клиент. Будет лучше посмотреть как это происходит на примере с постом в блог. Рассмотрим сначала как его представить в обычной реляционной БД в нормализованном виде, а потом разные варианты представления постов в MondoDB.

Предположим, что нам нужно решить следующие задачи:

  • выводить список постов с заголовками, категорией и тегами
  • выводить отдельный пост
  • выводить список тегов
  • выводить список категорий
  • редактировать имена тегов и категорий

В реляционной базе, скорее всего, структура БД будет примерно такой:

  • posts
    • id
    • title
    • body
    • author_id
    • category_id
    • slug
  • authors
    • id
    • name
  • categories
    • id
    • title
    • slug
  • tags
    • id
    • name
    • slug
  • posts_tags
    • post_id
    • tag_id

Есть ряд вариантов как организовать документы с постами в монге.

Можно хранить данные в одной коллекции:

  • posts
    • _id
    • title
    • body
    • author
    • category
      • title
      • slug
    • tags
      • [
        • name
        • slug
      • ]
    • slug

Такая структура хороша тем, что список постов и вывод отдельного поста решается одним запросом. Мы сразу получим все необходимы данные. Список тегов и категорий придется строить с помощью агрегаций. Если их количество не велико, то можно результаты агрегаций просто хранить в памяти или кэшировать. Редактировать имена тегов и категорий можно достаточно простыми апдейтами. Что при наличии индексов сделает эту операцию относительно быстрой.

Такой подход подойдет для относительно небольшой коллекции. На большом количестве документов операции обновления начнут заметно тормозить. К тому же, хоть в данном примере это и маловероятно, если документы у вас сложные и содержат много данных, то можно упереться в предельный размер документа монги. На "живой" базе разрывать документы по отдельным коллекциям весьма сомнительное удовольствие.

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

Коллекции в этом случае буду выглядеть так:

  • categories
    • _id
    • title
    • slug
  • tags
    • _id
    • name
    • slug
  • posts
    • _id
    • title
    • body
    • author
    • category_id
    • tags
      • [tag_id]
    • slug

Отлично. Проблему с редактированием категорий и тегов решили. Теперь эта операция занимает константное время. Построить список тегов и категорий не проблема. Но бесплатного ничего не бывает. Теперь для построения поста нам надо сделать 3 запроса: получить сам пост, получить категорию, получить теги. Аналогичные проблемы придется решать при построении списка постов.

Этот способ подойдет тогда, когда имена категорий и тегов меняются часто, но их количество не очень велико. При этом нет жестких требований к скорости отдачи постов на чтение.

Можно построить еще такую гибридную схему:

  • categories
    • _id
    • title
    • slug
  • tags
    • _id
    • name
    • slug
  • posts
    • _id
    • title
    • body
    • author
    • category
      • category_id
      • title
      • slug
    • tags
      • [
        • tag_id
        • name
        • slug
      • ]
    • slug

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

Пример с блогом очень искусственный (я бы вообще 10 раз подумал прежде, чем делать это не на реляционной базе), но даже на нем видно, что все подходы к разделению данных по коллекциям требуют написания какого-то количества дополнительного кода. И правильно оценив свою предметную область и объемы данных, можно прикинуть где кода будет меньше, он будет проще и не будет провалов по производительности.

PS Понимаю, что я не то что Капитан Очевидность, а даже адмирал Ясен Пень. Но подобные вопросы с MongoDB (да и в целом с NoSQL) возникают с завидной регулярностью.

---
Возник вопрос? Мне всегда можно написать в Twitter: avkorablev