Название звучит немного предвзято, но теперь, когда я привлек ваше внимание, позвольте мне объяснить мою точку зрения…

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

Когда в этих ситуациях примитивных операций монго (например, обычной операции findCollection с применением фильтров) недостаточно, возможно, возникает необходимость получения данных более сложным способом, и это где начинает появляться использование агрегаций в монго. Агрегации — это очень мощный инструмент монго, который позволяет нам применять гораздо более глубокий синтаксис к данным, например, добавлять поля по запросу, вычислять средние значения, максимумы, минимумы, комбинированные фильтры, группировать по полям, объединять с другими коллекциями. И именно в этом последнем случае соединений мы начинаем страдать от того, для чего монго не создан, то есть выполнять операции над несколькими коллекциями для одного результата. В случае агрегации представлен этапом $lookup, который также часто встречается в мангусте или других формах как заполнение полей (обычно для ассоциаций n.1 ).

Операция $lookup выполняет левое внешнее соединение с неразделенной коллекцией в той же базе данных для фильтрации документов из соединенной коллекции для обработки. Технический директор MongoDB также подчеркивает свои мысли.

Сначала $lookup или операция заполнения могут показаться невинными, в основном в случаях, когда мы определяем «id» в коллекции, которая ссылается на один элемент из другой коллекции. В некоторых других случаях мы могли бы захотеть заполнить массивы данных, в конечном счете, по ограниченному списку, в одном документе. Но есть также очень распространенные сценарии, в которых мы моделируем отношения 1.n (будучи n неограниченными) и хотим запрашивать результаты, применяя фильтры, принадлежащие свойству «стороны n» отношения.

В этом примере у нас есть «книги» и «авторы», и мы хотим сгруппировать книги по авторам и упорядочить их по имени их автора.

// Book schema
{
   bookId: uuid,
   name: string,
   yearOfPublication: date,
   author: uuid // Ref to Author collection
}   
// Author schema
{
   authorId: uuid,
   name: string,
   lastName: string
}

Поэтому в упрощенном сценарии мы будем стремиться решить этот запрос, выполнив запрос, который включает:

  1. Получить книги
  2. Для каждой книги свяжите соответствующего автора.
  3. Отсортируйте результат по автору.имя
  4. Возьмите первую страницу результатов

Эти шаги с использованием этой ассоциации могут быть тривиальными и на самом деле не вызовут серьезных проблем при небольшом объеме данных. Однако представьте себе случай, когда у нас зарегистрированы миллионы книг, тогда наша производительность быстро пострадает, и для определения ожидаемого результата потребуется порядка нескольких секунд. Это связано с тем, что каждый элемент в коллекции книг должен быть заполнен своей парой авторов, прежде чем можно будет применить фильтр, что естественно для SQL-запроса, но не для NOSQL. В этой ситуации, используя Mongo, мы дорого заплатим за то, что не смоделировали схему по-другому.

В качестве дополнения к этому посту, я думаю, стоит упомянуть, что я лично столкнулся со сценарием, подобным этому, но усиленным гораздо большей сложностью модели сущности. К тому времени команда проекта решила направить запрос о совете непосредственно команде MongoAtlas, которая удобно предлагает персональные консультации со своими инженерами, что чрезвычайно полезно для иллюстрации информации людям, которые работают с Mongo каждый день. На этом «семинаре» вся команда смогла присутствовать и задать различные вопросы, представить проблемы с точки зрения нашей собственной системы, результатом чего стал документ рекомендаций и анализа, адаптированный к нашей системе.

Самая настоятельная рекомендация, которую вы, возможно, неоднократно встречали в Интернете, заключается в моделировании системы на основе шаблонов доступа к запросам, упомянутых в начале статьи. Это потенциально потребует наличия полностью денормализованной модели, то есть с обилием избыточности, что подразумевает наличие всей информации, которую мы хотим получить и к которой мы хотим применить фильтры и сортировку, в одной и той же коллекции. В этом смысле важно установить шаблоны доступа к запросам в двух направлениях, как для записи, так и для чтения.

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

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

Хотя этот пост специально основан на предотвращении злоупотребления $lookups, этот семинар Mongo оставляет другие ключевые рекомендации, такие как использование индексов в качестве фундаментального фактора производительности, лучшие практики для операций текстового поиска, использование транзакций, обработка разбиения на страницы. Об этих функциях я сделаю дополнительные сообщения об этом в будущих публикациях.

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

{
   bookId: uuid,
   name: string,
   author: { 
     authorId: uuid,
     name: string
   }
}

Таким образом, мы можем решить исходный запрос, просто просматривая коллекцию книг и применяя фильтры к author.name без необходимости выполнять $lookup или сканировать общее количество документов.

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

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

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

Весьма вероятно, что эти типы изменений схемы происходят с системой, которая уже находится в полной разработке и, возможно, с производственной версией. Моя рекомендация в этом отношении — подготовить надежные сценарии, которые переносят данные из одного места в другое и, в лучшем случае, операции, выполняемые в фоновом режиме, чтобы обеспечить их согласованность в случае, если что-то не на месте или произошел сбой в процессе обновления. К счастью, mongo предлагает возможность назначать и удалять атрибуты из схемы.

В заключение, $lookup по существу применяет соединение к другой таблице, для чего mongo не обещает приемлемой производительности при большом масштабе данных. Поэтому важно избежать его через определение паттернов доступа к запросам, которые нужны нашему приложению, и только потом определять, какие данные будет содержать наша коллекция таким образом, чтобы она не зависела от других при возврате результатов. По сути, это денормализация нашей схемы данных для повышения производительности чтения и снижения производительности записи. Отказ от $lookups — это большой шаг к повышению производительности нашей системы, пользователи и заинтересованные стороны это оценят.

Спасибо за прочтение! 📖

Вы можете подписаться на меня в Твиттере, чтобы узнать больше постов и интересных вещей.

Ваше здоровье! 🤘