Статьи

Использование JOIN в базах данных MongoDB NoSQL

Использование $ lookUp с NoSQL

Спасибо Джулиану Моцу за любезную помощь в рецензировании этой статьи.


Одним из самых больших различий между базами данных SQL и NoSQL является JOIN. В реляционных базах данных предложение SQL JOIN позволяет объединять строки из двух или более таблиц, используя общее поле между ними. Например, если у вас есть таблицы books и publishers , вы можете написать команды SQL, такие как:

 SELECT book.title, publisher.name FROM book LEFT JOIN book.publisher_id ON publisher.id; 

Другими словами, таблица book имеет поле publisher_id которое ссылается на поле id в таблице publisher .

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

Базы данных SQL предлагают ряд функций нормализации и ограничения для обеспечения поддержания отношений.

NoSQL == Нет JOIN?

Не всегда …

Документно-ориентированные базы данных, такие как MongoDB , предназначены для хранения денормализованных данных. В идеале не должно быть никаких связей между коллекциями. Если в двух или более документах требуются одни и те же данные, их необходимо повторить.

Это может быть неприятно, поскольку есть несколько ситуаций, когда вам никогда не нужны реляционные данные. К счастью, MongoDB 3.2 представляет новый оператор $lookup который может выполнять LEFT-OUTER-JOIN-подобную операцию над двумя или более коллекциями. Но есть подвох …

MongoDB Aggregation

$lookup разрешен только в операциях агрегации . Думайте о них как о конвейере операторов, которые запрашивают, фильтруют и группируют результат. Выход одного оператора используется как вход для следующего.

Агрегирование сложнее понять, чем более простые запросы find и обычно оно работает медленнее. Тем не менее, они являются мощным и бесценным вариантом для сложных поисковых операций.

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

 { "_id": ObjectID("45b83bda421238c76f5c1969"), "name": "User One", "email: "userone@email.com", "country": "UK", "dob": ISODate("1999-09-13T00:00:00.000Z") } 

Мы можем добавить столько полей, сколько необходимо, но для всех документов MongoDB требуется поле _id которое имеет уникальное значение. _id похож на первичный ключ SQL и будет вставлен автоматически при необходимости.

Наша социальная сеть теперь требует коллекции сообщений, в которой хранятся многочисленные полезные обновления от пользователей. Документы хранят текст, дату, рейтинг и ссылку на пользователя, который написал его в поле user_id :

 { "_id": ObjectID("17c9812acff9ac0bba018cc1"), "user_id": ObjectID("45b83bda421238c76f5c1969"), "date: ISODate("2016-09-05T03:05:00.123Z"), "text": "My life story so far", "rating": "important" } 

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

Сводный запрос MongoDB передается массивом операторов конвейера, которые определяют каждую операцию по порядку. Во-первых, нам нужно извлечь все документы из коллекции сообщений, которые имеют правильную оценку, с помощью фильтра $match :

 { "$match": { "rating": "important" } } 

Теперь мы должны отсортировать совпадающие элементы в обратном порядке дат, используя оператор $sort :

 { "$sort": { "date": -1 } } 

Поскольку нам требуется всего двадцать сообщений, мы можем применить стадию $limit поэтому MongoDB нужно только обрабатывать нужные нам данные:

 { "$limit": 20 } 

Теперь мы можем объединить данные из user коллекции, используя новый оператор $lookup . Требуется объект с четырьмя параметрами:

  • localField : поле поиска во входном документе
  • from : коллекция, чтобы присоединиться
  • foreignField : поле для поиска в коллекции from
  • as : имя выходного поля.

Поэтому наш оператор:

 { "$lookup": { "localField": "user_id", "from": "user", "foreignField": "_id", "as": "userinfo" } } 

Это создаст новое поле в нашем выводе с именем userinfo . Он содержит массив, в котором каждому значению соответствует user документ:

 "userinfo": [ { "name": "User One", ... } ] 

У нас есть отношение один к одному между post.user_id и user._id , поскольку у публикации может быть только один автор. Следовательно, наш массив userinfo будет содержать только один элемент. Мы можем использовать оператор $unwind чтобы преобразовать его в поддокумент:

 { "$unwind": "$userinfo" } 

Вывод теперь будет преобразован в более практичный формат, в котором могут применяться дополнительные операторы:

 "userinfo": { "name": "User One", "email: "userone@email.com",} 

Наконец, мы можем вернуть текст, время публикации, имя пользователя и страну, используя этап $project в конвейере:

 { "$project": { "text": 1, "date": 1, "userinfo.name": 1, "userinfo.country": 1 } } 

Собираем все вместе

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

 db.post.aggregate([ { "$match": { "rating": "important" } }, { "$sort": { "date": -1 } }, { "$limit": 20 }, { "$lookup": { "localField": "user_id", "from": "user", "foreignField": "_id", "as": "userinfo" } }, { "$unwind": "$userinfo" }, { "$project": { "text": 1, "date": 1, "userinfo.name": 1, "userinfo.country": 1 } } ]); 

Результатом является сборник до двадцати документов. Например:

 [ { "text": "The latest post", "date: ISODate("2016-09-27T00:00:00.000Z"), "userinfo": { "name": "User One", "country": "UK" } }, { "text": "Another post", "date: ISODate("2016-09-26T00:00:00.000Z"), "userinfo": { "name": "User One", "country": "UK" } } ... ] 

Большой! Наконец-то я могу перейти на NoSQL!

MongoDB $lookup полезен и мощен, но даже этот базовый пример требует сложного агрегированного запроса. Это не замена более мощного предложения JOIN, предлагаемого в SQL. MongoDB также не предлагает ограничений; если user документ будет удален, оставшиеся без post документы останутся.

В идеале оператор $lookup должен требоваться нечасто. Если вам это нужно, вы, возможно, используете неправильное хранилище данных …

Если у вас есть реляционные данные, используйте реляционную (SQL) базу данных!

Тем не менее, $lookup является долгожданным дополнением к MongoDB 3.2. Он может преодолеть некоторые из наиболее неприятных проблем при использовании небольших объемов реляционных данных в базе данных NoSQL.