Давайте начнем с основного заявления об отказе от ответственности: основная часть ответов на проблему уже дана здесь: Найти в массиве с двойной вложенностью MongoDB . И «для записи» Double также применяется к Triple или Quadrupal или ANY уровню вложенности, так как в основном тот же принцип ВСЕГДА .
Другим основным пунктом любого ответа также является Не NEST Arrays , поскольку, как объяснено и в этом ответе (и я повторял это много раз), по любой причине, которую вы "думаете" у вас есть для "вложения" , на самом деле не дает вам тех преимуществ, которые вы себе представляете. На самом деле «вложение» действительно просто делает жизнь намного сложнее.
Вложенные задачи
Основное заблуждение любого перевода структуры данных из «реляционной» модели почти всегда интерпретируется как «добавить уровень вложенного массива» для каждой связанной модели. То, что вы здесь представляете, не является исключением из этого заблуждения, поскольку очень похоже, что оно "нормализовано" , так что каждый подмассив содержит связанные элементы со своим родителем.
MongoDB - это база данных на основе документов, так что она в значительной степени позволяет вам делать это или фактически любой контент структуры данных, который вы в основном хотите. Это, однако, не означает, что с данными в такой форме легко работать или они действительно полезны для реальных целей.
Давайте заполним схему некоторыми фактическими данными для демонстрации:
{
"_id": 1,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "A",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
]
},
{
"third_item": "B",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
]
}
]
},
{
"second_item": "A",
"third_level": [
{
"third_item": "B",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
}
]
}
]
},
{
"first_item": "A",
"second_level": [
{
"second_item": "B",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
}
]
}
]
}
]
},
{
"_id": 2,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "A",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
]
}
]
}
]
},
{
"_id": 3,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "B",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
]
}
]
}
]
}
Это "немного" отличается от структуры в вопросе, но для демонстрационных целей в ней есть то, на что нам нужно обратить внимание. В основном в документе есть массив, в котором есть элементы с вложенным массивом, который, в свою очередь, содержит элементы в вложенном массиве и так далее. "нормализующий" здесь, конечно, идентификаторы на каждом "уровне" как "тип элемента" или что-то, что у вас есть на самом деле.
Основная проблема заключается в том, что вы просто хотите получить «некоторые» данные из этих вложенных массивов, а MongoDB действительно просто хочет вернуть «документ», что означает, что вам нужно выполнить некоторые манипуляции, чтобы просто добраться до соответствующих "подпункты".
Даже по вопросу «правильно» выбор документа, который соответствует всем этим «подкритериям», требует широкого использования $elemMatch
для получения правильной комбинации условий на каждом уровне элементов массива. Вы не можете использовать прямую «Точечную нотацию» из-за необходимости выполнения этих множественных условий . Без операторов $elemMatch
вы не получите точную «комбинацию» и просто получите документы, в которых условие было выполнено для любого элемента массива.
Что касается на самом деле "фильтрации содержимого массива" , то это фактически является частью дополнительной разницы:
db.collection.aggregate([
{ "$match": {
"first_level": {
"$elemMatch": {
"first_item": "A",
"second_level": {
"$elemMatch": {
"second_item": "A",
"third_level": {
"$elemMatch": {
"third_item": "A",
"forth_level": {
"$elemMatch": {
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}
}
}
}
}
}
}
}
}},
{ "$addFields": {
"first_level": {
"$filter": {
"input": {
"$map": {
"input": "$first_level",
"in": {
"first_item": "$$this.first_item",
"second_level": {
"$filter": {
"input": {
"$map": {
"input": "$$this.second_level",
"in": {
"second_item": "$$this.second_item",
"third_level": {
"$filter": {
"input": {
"$map": {
"input": "$$this.third_level",
"in": {
"third_item": "$$this.third_item",
"forth_level": {
"$filter": {
"input": "$$this.forth_level",
"cond": {
"$and": [
{ "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
{ "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.third_item", "A" ] },
{ "$gt": [ { "$size": "$$this.forth_level" }, 0 ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.second_item", "A" ] },
{ "$gt": [ { "$size": "$$this.third_level" }, 0 ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.first_item", "A" ] },
{ "$gt": [ { "$size": "$$this.second_level" }, 0 ] }
]
}
}
}
}},
{ "$unwind": "$first_level" },
{ "$unwind": "$first_level.second_level" },
{ "$unwind": "$first_level.second_level.third_level" },
{ "$unwind": "$first_level.second_level.third_level.forth_level" },
{ "$group": {
"_id": {
"date": "$first_level.second_level.third_level.forth_level.sales_date",
"price": "$first_level.second_level.third_level.forth_level.price",
},
"quantity_sold": {
"$avg": "$first_level.second_level.third_level.forth_level.quantity"
}
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quanity_sold": "$quantity_sold"
}
},
"quanity_sold": { "$avg": "$quantity_sold" }
}}
])
Это лучше всего описать как "грязный" и "вовлеченный". Не только наш начальный запрос для выбора документа с $elemMatch
больше, чем глоток, но затем мы имеем следующие $filter
и $map
обработка для каждого уровня массива. Как упоминалось ранее, это шаблон независимо от того, сколько уровней на самом деле.
Вы можете поочередно сделать комбинацию $unwind
и $match
вместо фильтрации массивов на месте, но это приведет к дополнительным расходам до $unwind
до удаления нежелательного контента, поэтому в современных выпусках MongoDB обычно лучше сначала $filter
из массива.
Конечным местом здесь является то, что вы хотите $group
по элементам, которые на самом деле находятся внутри массива, так что вам в конечном итоге понадобится $unwind
каждый уровень массивов в любом случае до этого.
Фактическая «группировка» тогда обычно проста, используя свойства sales_date
и price
для первого накопления, а затем добавляя последующий этап к $push
различные price
значения, для которых вы хотите накапливать среднее значение для каждой даты в виде секунды накопления.
ПРИМЕЧАНИЕ : Фактическая обработка дат может значительно различаться при практическом использовании в зависимости от степени детализации, с которой вы их храните. В этом примере все даты уже округлены до начала каждого «дня». Если вам действительно нужно накапливать реальные значения даты и времени, то вам, вероятно, действительно нужна такая конструкция или подобная:
{ "$group": {
"_id": {
"date": {
"$dateFromParts": {
"year": { "$year": "$first_level.second_level.third_level.forth_level.sales_date" },
"month": { "$month": "$first_level.second_level.third_level.forth_level.sales_date" },
"day": { "$dayOfMonth": "$first_level.second_level.third_level.forth_level.sales_date" }
}
}.
"price": "$first_level.second_level.third_level.forth_level.price"
}
...
}}
Использование $dateFromParts
и других операторов агрегирования даты для извлечения информации "дня" и представления даты обратно в этой форме для накопления.
Начинаем денормализовать
Что должно быть ясно из "беспорядка" выше, так это то, что работать с вложенными массивами не совсем просто. Такие структуры, как правило, даже невозможно было атомарно обновить в выпусках, предшествующих MongoDB 3.6, и даже если вы никогда не обновляли их или не заменяли в основном весь массив, они все равно не просты для запроса. Это то, что вам показывают.
Если вы должны иметь содержимое массива в родительском документе, обычно рекомендуется "сплющить" и "денормализовать" такие структуры. Это может показаться противоречащим реляционному мышлению, но на самом деле это лучший способ обработки таких данных по соображениям производительности:
{
"_id": 1,
"data": [
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
},
{
"_id": 2,
"data": [
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
},
{
"_id": 3,
"data": [
{
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
Это все те же данные, что и первоначально показанные, но вместо вложенности мы фактически просто помещаем все в единый плоский массив в каждом родительском документе. Конечно, это означает дублирование различных точек данных, но разница в сложности запросов и производительности должна быть очевидна:
db.collection.aggregate([
{ "$match": {
"data": {
"$elemMatch": {
"first_item": "A",
"second_item": "A",
"third_item": "A",
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}
}
}},
{ "$addFields": {
"data": {
"$filter": {
"input": "$data",
"cond": {
"$and": [
{ "$eq": [ "$$this.first_item", "A" ] },
{ "$eq": [ "$$this.second_item", "A" ] },
{ "$eq": [ "$$this.third_item", "A" ] },
{ "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
{ "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
]
}
}
}
}},
{ "$unwind": "$data" },
{ "$group": {
"_id": {
"date": "$data.sales_date",
"price": "$data.price",
},
"quantity_sold": { "$avg": "$data.quantity" }
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quantity_sold": "$quantity_sold"
}
},
"quantity_sold": { "$avg": "$quantity_sold" }
}}
])
Теперь вместо того, чтобы вкладывать эти вызовы $elemMatch
и аналогичным образом для выражений $filter
, все гораздо яснее и проще для чтения, а на самом деле довольно просто в обработке. Есть еще одно преимущество в том, что вы даже можете индексировать ключи элементов в массиве, которые используются в запросе. Это было ограничением для вложенной модели, в которой MongoDB просто не допускает такое «индексирование нескольких ключей» для ключей массивов в массивах . Для одного массива это разрешено и может быть использовано для повышения производительности.
Все, что происходит после «фильтрации содержимого массива» , остается точно таким же, за исключением того, что это просто имена путей, такие как "data.sales_date"
, в отличие от длинных "first_level.second_level.third_level.forth_level.sales_date"
из предыдущей структуры.
Когда НЕ вставлять
Наконец, еще одно большое заблуждение заключается в том, что ВСЕ Отношения необходимо перевести как встраивание в массивы. Это действительно никогда не было целью MongoDB, и вам когда-либо предназначалось хранить «связанные» данные в одном и том же документе в массиве в том случае, если это означало выполнение одного извлечения данных, а не «объединений».
Классическая модель «Заказ / Детали» здесь обычно применяется, когда в современном мире вы хотите отобразить «заголовок» для «Заказа» с такими деталями, как адрес клиента, общая сумма заказа и т. Д. В том же «экране», что и детали различных позиций в разделе "Заказ".
На заре создания СУБД типичный экран размером 80 на 25 строк просто имел такую информацию «заголовка» на одном экране, тогда подробные строки для всего приобретенного были на другом экране. Поэтому, естественно, был какой-то уровень здравого смысла, чтобы хранить их в отдельных таблицах. По мере того, как мир стал более детализированным на таких «экранах», вы, как правило, хотите увидеть все целиком или, по крайней мере, «заголовок» и первые столько строк такого «порядка».
Следовательно, почему такого рода расположение имеет смысл помещать в массив, так как MongoDB возвращает «документ», содержащий все связанные данные одновременно. Нет необходимости в отдельных запросах для отдельных отображаемых экранов и нет необходимости в «соединениях» с такими данными, поскольку они уже «предварительно объединены».
Подумайте, нужно ли вам это - АКА "Полностью" денормализовать
Так что в тех случаях, когда вы в значительной степени знаете, что на самом деле вы не заинтересованы в работе с большей частью данных в таких массивах большую часть времени, обычно имеет смысл просто поместить все это в одну коллекцию самостоятельно, просто другое свойство для идентификации «родителя», если иногда требуется такое «присоединение»:
{
"_id": 1,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 2,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"_id": 3,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
{
"_id": 4,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 5,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 6,
"parent_id": 1,
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 7,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 8,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 9,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 10,
"parent_id": 3,
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
Опять же, это те же данные, но только на этот раз в совершенно отдельных документах, в лучшем случае со ссылкой на родителя в том случае, если вам это может действительно понадобиться для другой цели. Обратите внимание, что агрегации здесь вообще не связаны с родительскими данными, и также ясно, откуда возникает дополнительная производительность и устраненная сложность, просто храня в отдельной коллекции:
db.collection.aggregate([
{ "$match": {
"first_item": "A",
"second_item": "A",
"third_item": "A",
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}},
{ "$group": {
"_id": {
"date": "$sales_date",
"price": "$price"
},
"quantity_sold": { "$avg": "$quantity" }
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quantity_sold": "$quantity_sold"
}
},
"quantity_sold": { "$avg": "$quantity_sold" }
}}
])
Поскольку все уже является документом, нет необходимости «фильтровать массивы» или иметь любую другую сложность. Все, что вы делаете, - это выбираете подходящие документы и агрегируете результаты с двумя точно такими же последними шагами, которые присутствовали все время.
С целью просто достижения окончательных результатов, это работает намного лучше, чем любая из вышеуказанных альтернатив. Вопрос, о котором идет речь, на самом деле касается только «подробных» данных, поэтому лучший способ - полностью отделить детали от родительского элемента, поскольку он всегда обеспечивает наилучшее повышение производительности.
И общая точка здесь - это то, где фактическая схема доступа к остальной части приложения НИКОГДА не должна возвращать весь контент массива, тогда он, вероятно, не должен был быть встроен в любом случае. По-видимому, большинству операций «записи» в любом случае никогда не нужно касаться родителя, и это еще один решающий фактор, где это работает или нет.
Заключение
Общее сообщение снова гласит, что по общему правилу вы никогда не должны вкладывать массивы. Самое большее, вы должны хранить «единственный» массив с частично денормализованными данными в связанном родительском документе, и если в оставшихся шаблонах доступа действительно совсем не используются родитель и потомок в тандеме, тогда данные действительно должны быть разделены.
«Большое» изменение заключается в том, что все причины, по которым вы считаете нормализацию данных на самом деле хорошей, оказываются врагами таких встроенных систем документов. Избегать «объединений» - это всегда хорошо, но создание сложной вложенной структуры, которая будет выглядеть как «объединенные» данные, также не поможет вам.
Затраты на то, что вы «считаете» нормализацией, обычно заканчиваются потерей дополнительного хранения и обслуживания дублированных и денормализованных данных в вашем конечном хранилище.
Также обратите внимание, что все формы выше возвращают один и тот же набор результатов. Это довольно производно в том смысле, что для краткости выборочные данные включают только единичные товары, или, самое большее, там, где есть несколько ценовых точек, «среднее» все равно равно 1
, поскольку в любом случае это все значения. Но содержание, объясняющее это, уже очень длинное, поэтому на самом деле это просто «на примере»:
{
"_id" : ISODate("2018-11-01T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}
{
"_id" : ISODate("2018-11-02T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}
{
"_id" : ISODate("2018-11-03T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
},
{
"price" : 2,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}