Спасибо Джулиану Моцу за любезную помощь в рецензировании этой статьи.
Одним из самых больших различий между базами данных 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.