Запрос MongoDB для получения списка документов с количеством внешних связанных документов
У меня есть база данных mongodb с коллекционными документами, которые примерно таковы:
// user document
{
_id: $oid,
name: "name",
description: "description".
// ...
}
// book document
{
_id: $oid,
userId: "..."
name: "name",
description: "description"
// ...
}
// page document
{
_id: $oid,
bookId: "..."
name: "name",
description: "description"
// ...
}
У пользователя много книг, а у книги много страниц. Причина, по которой каждый объект является отдельным документом, заключается в том, что у пользователя может быть тысячи книг, а книга может иметь тысячи страниц, поэтому, если бы все было в одном документе, мы могли бы довольно легко достичь предела в 16 МБ.
Каков наилучший способ получить список книг для указанного userId
с pageCount
поле для каждой книги?
Это результат JSON, который мне нужен.
{
books: [{
_id: $oid,
name: "name1",
description: "description1",
pageCount: 8
}, {
_id: $oid,
name: "name2",
description: "description2",
pageCount: 12
},
// ...
]
}
С базой данных SQL это было бы довольно просто с количеством соединений, но с mongodb я не вижу никакого простого решения, кроме как делать отдельные запросы, чтобы получить список книг и затем получить количество страниц для каждой книги.
3 ответа
Он не дает прямого ответа на вопрос, а дает некоторые идеи относительно
делать отдельные запросы, чтобы получить список книг, а затем получить количество страниц для каждой книги
часть. Это не всегда плохо. Mongodb достаточно эффективен в простых запросах, поэтому я дам вам несколько цифр, чтобы рассмотреть производительность одного конвейера $ lookup по сравнению с несколькими запросами, и призываю вас протестировать типичные запросы в вашем наборе данных. Egpagination может иметь огромное значение, если вам не нужны все данные сразу.
настроить
Небольшая база данных на 100 пользователей X 1000 книг X 1000 страниц каждая на крошечном 1 ВЦП / 2 ГБ памяти / 50 ГБ Диск / LON1 - Ubuntu MongoDB 3.4.10 на 16.04 дроплет.
pages
Коллекция создана следующим образом:
for USERID in {1..100}; do
echo "" > pages.json;
for BOOKID in {1..1000}; do
./node_modules/.bin/mgeneratejs "{\"bookId\": \"$USERID-$BOOKID\", \"name\": {\"\$sentence\":{\"words\":3}}, \"description\": \"\$paragraph\"}" -n 1000 >> pages.json
done
cat pages.json | mongoimport -d so -c pages
done
И books
один почти такой же.
Основные характеристики:
db.books.stats(1024*1024)
"ns" : "so.books",
"size" : 50,
"count" : 100000,
"avgObjSize" : 533,
"storageSize" : 52,
"nindexes" : 2,
"totalIndexSize" : 1,
"indexSizes" : {
"_id_" : 0,
"userId_1" : 0
},
db.pages.stats(1024*1024)
"ns" : "so.pages",
"size" : 51673,
"count" : 100000000,
"avgObjSize" : 541,
"storageSize" : 28920,
"nindexes" : 2,
"totalIndexSize" : 1424,
"indexSizes" : {
"_id_" : 994,
"bookId_1" : 430
},
$ поиск
Трубопровод от ответа @chridam
db.books.aggregate([
{ "$match": { "userId": 18 } },
{ "$lookup": {
"from": "pages",
"localField": "_id",
"foreignField": "bookId",
"as": "pageCount"
}},
{ "$addFields": {
"pageCount": { "$size": "$pageCount" }
}}
])
дает быстрый ответ:
"op" : "command",
"command" : {
"aggregate" : "books"
},
"keysExamined" : 1000,
"docsExamined" : 1000,
"nreturned" : 101,
"responseLength" : 57234,
"millis" : 1028
Для первых 100 документов и пусть вы начнете обрабатывать документы в течение секунды.
Общее время для всего этого:
db.books.aggregate([
{ "$match": { "userId": 18 } },
{ "$lookup": {
"from": "pages",
"localField": "_id",
"foreignField": "bookId",
"as": "pageCount"
}},
{ "$addFields": {
"pageCount": { "$size": "$pageCount" }
}}
]).toArray()
Добавляет еще 8 секунд:
"op" : "getmore",
"query" : {
"getMore" : NumberLong("32322423895"),
"collection" : "books"
},
"keysExamined" : 0,
"docsExamined" : 0,
"nreturned" : 899,
"responseLength" : 500060,
"millis" : 8471
Общее время получения всех данных составляет более 9 секунд.
несколько запросов
получить книги:
let bookIds = []; db.books.find({userId:12}).forEach(b=>{bookIds.push(b._id);});
заполняет массив в течение 10 миллисекунд:
"op" : "query", "query" : { "find" : "books", "filter" : { "userId" : 34 } }, "keysExamined" : 101, "docsExamined" : 101, "nreturned" : 101, "responseLength" : 54710, "millis" : 3
а также
"op" : "getmore", "query" : { "getMore" : NumberLong("34224552674"), "collection" : "books" }, "keysExamined" : 899, "docsExamined" : 899, "nreturned" : 899, "responseLength" : 485698, "millis" : 7
считать страницы:
db.pages.aggregate([ { $match: { bookId: { $in: bookIds } } }, { $group: { _id: "$bookId", cnt: { $sum: 1 } } } ]).toArray()
требуется 1,5 секунды всего:
"op" : "command", "command" : { "aggregate" : "pages" }, "keysExamined" : 1000001, "docsExamined" : 0, "nreturned" : 101, "responseLength" : 3899, "millis" : 1574
а также
"op" : "getmore", "query" : { "getMore" : NumberLong("58311204806"), "collection" : "pages" }, "keysExamined" : 0, "docsExamined" : 0, "nreturned" : 899, "responseLength" : 34935, "millis" : 0
результаты слияния
Не запрос, но должно быть сделано на уровне приложения. Javascript mongoshell занимает несколько миллисекунд, что дает общее время на получение всех данных менее чем за 2 секунды.
С платформой агрегации MongoDB есть стадия конвейера, называемая $lookup
который позволяет выполнить левое внешнее соединение с другой коллекцией в той же базе данных, чтобы отфильтровать документы из "объединенной" коллекции для обработки.
Таким образом, вооружившись этим оружием, вы можете запустить совокупную конвейерную операцию, которая соединяет коллекцию книг с коллекцией страниц.
На этапах конвейера вы можете получить pageCount
запрашивая размер массива результатов из "соединения".
Чтобы получить желаемый результат, попробуйте выполнить следующую агрегированную операцию, предполагая, что версия вашего сервера MongoDB составляет не менее 3,4:
db.books.aggregate([
{ "$match": { "userId": userId } },
{ "$lookup": {
"from": "pages",
"localField": "_id",
"foreignField": "bookId",
"as": "pageCount"
}},
{ "$addFields": {
"pageCount": { "$size": "$pageCount" }
}}
])
Кроме того, вы можете запустить $lookup
трубопровод от users
коллекция как
db.user.aggregate([
{ "$match": { "_id": userId } },
{ "$lookup": {
"from": "books",
"localField": "_id",
"foreignField": "userId",
"as": "books"
}},
{ "$lookup": {
"from": "pages",
"localField": "books._id",
"foreignField": "bookId",
"as": "pages"
}},
{ "$addFields": {
"books": {
"$map": {
"input": "$books",
"as": "book",
"in": {
"name": "$$book.name",
"description": "$$book.description",
"pageCount": { "$size": "$$book.pages" }
}
}
}
}}
])
Вы можете использовать $lookup
этап из структуры агрегации:
db.Users.aggregate([
{$match: {_id: userId}},
{$lookup: {
from: "Book",
localField: "userId",
foreignField: "_id",
as: "book"
}},
{$lookup: {
from: "Page",
localField: "bookId",
foreignField: "book._id",
as: "page"
}}
])
и добавить сцену $group
рассчитать количество страниц. Но я думаю, что этот запрос будет довольно медленным. И если вы хотите удалить вашу коллекцию после, или если это уже так, вы не можете использовать $lookup