Не надо лезть в MongoDB с реляционным подходом. Этот тезис вроде бы очевиден, но когда дело доходит до реализации, возникает множество вопросов. И стопроцентно одним из них будет: «Как делать join?». Правильный ответ: никак.

Да, в версии 3.2 появился $lookup в агрегациях. Отчасти это замена классическому JOIN из реляционного мира. Но в целом агрегации — не самый быстрый в MongoDB инструмент. Лучше, когда запрос идёт к одной коллекции и это обычный find.

Стоит отметить, что использование NoSQL не обходится бесплатно. К примеру, обеспечение консистентности данных встроено в реляционные базы по умолчанию. При использовании MongoDB задача обеспечить консистентность ложится на программу‑клиент.

Будет лучше посмотреть, как это происходит, на примере с постом в блог. Рассмотрим сначала, как его представить в обычной реляционной БД в нормализованном виде, а потом — разные варианты представления постов в MongoDB.

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

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

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

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

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

Вариант 1. Хранить данные в одной коллекции

posts

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

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

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

Вариант 2. Вынести данные тегов и категории в отдельные коллекции

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

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

categories

  • _id
  • title
  • slug

tags

  • _id
  • name
  • slug

posts

  • _id
  • title
  • body
  • author
  • category_id
  • tags
    • [tag_id]
  • slug

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

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

Вариант 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

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

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

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