Commençons par un avertissement de base en ce que le corps principal de ce qui répond au problème a déjà été répondu ici à Rechercher dans un tableau double imbriqué MongoDB . Et "pour mémoire" le Double s'applique également à Triple ou Quadrupale ou TOUT niveau d'imbrication comme fondamentalement le même principe TOUJOURS .
L'autre point principal de toute réponse est également Ne pas imbriquer les tableaux , puisque comme cela est également expliqué dans cette réponse (et j'ai répété cela beaucoup fois ), quelle que soit la raison pour laquelle vous "pensez" vous avez pour "imbriquer" ne vous donne en fait pas les avantages que vous pensez qu'il le fera. En fait "imbriquer" rend vraiment la vie beaucoup plus difficile.
Problèmes imbriqués
La principale idée fausse de toute traduction d'une structure de données à partir d'un modèle "relationnel" est à peu près toujours interprétée comme "ajouter un niveau de tableau imbriqué" pour chaque modèle associé. Ce que vous présentez ici ne fait pas exception à cette idée fausse car il semble très bien "normalisé" afin que chaque sous-tableau contienne les éléments liés à son parent.
MongoDB est une base de données basée sur des "documents", elle vous permet donc à peu près de faire cela ou en fait tout contenu de structure de données que vous souhaitez. Cela ne signifie toutefois pas que les données sous une telle forme sont faciles à utiliser ou même pratiques pour l'objectif réel.
Remplissons le schéma avec des données réelles à démontrer :
{
"_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
}
]
}
]
}
]
}
]
}
C'est un "peu" différent de la structure de la question, mais à des fins de démonstration, il contient les éléments que nous devons examiner. Il y a principalement un tableau dans le document qui a des éléments avec un sous-tableau, qui à son tour a des éléments dans un sous-tableau et ainsi de suite. La "normalisation" ici bien sûr par les identifiants de chaque "niveau" en tant que "type d'élément" ou tout ce que vous avez réellement.
Le problème principal est que vous voulez juste "certaines" des données de ces tableaux imbriqués, et MongoDB veut vraiment juste renvoyer le "document", ce qui signifie que vous devez faire quelques manipulations pour accéder à ceux qui correspondent "sous- éléments".
Même sur la question de "correctement" la sélection du document qui correspond à tous ces "sous-critères" nécessite une utilisation intensive de $elemMatch
afin d'obtenir la bonne combinaison de conditions à chaque niveau d'éléments du tableau. Vous ne pouvez pas utiliser directement "Dot Notation"
en raison de la nécessité de ces conditions multiples
. Sans le $elemMatch
vous n'obtenez pas la "combinaison" exacte et obtenez simplement des documents où la condition était vraie sur any élément de tableau.
Quant à "filtrer le contenu du tableau" alors c'est en fait la partie de la différence supplémentaire :
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" }
}}
])
Ceci est mieux décrit comme "désordonné" et "impliqué". Non seulement notre requête initiale pour la sélection de documents avec le $elemMatch
plus qu'une bouchée, mais nous avons ensuite le $filter
et $map
traitement pour chaque niveau de tableau. Comme mentionné précédemment, c'est le modèle quel que soit le nombre de niveaux qu'il y a réellement.
Vous pouvez également faire un $unwind
et $match
combinaison au lieu de filtrer les tableaux en place, mais cela entraîne une surcharge supplémentaire pour $unwind
avant que le contenu indésirable ne soit supprimé, donc dans les versions modernes de MongoDB, il est généralement préférable de $filter
du tableau en premier.
L'endroit final ici est que vous voulez $group
par des éléments qui se trouvent réellement à l'intérieur du tableau, vous finissez donc par avoir besoin de $unwind
chaque niveau des tableaux de toute façon avant cela.
Le "regroupement" réel est alors généralement simple en utilisant le sales_date
et price
propriétés pour le premier accumulation, puis en ajoutant une étape ultérieure à $push
les différents price
les valeurs pour lesquelles vous souhaitez accumuler une moyenne pour chaque date en tant que seconde accumulation.
REMARQUE :Le traitement réel des dates peut varier dans l'utilisation pratique en fonction de la granularité avec laquelle vous les stockez. Dans cet exemple, les dates sont toutes déjà arrondies au début de chaque "jour". Si vous avez réellement besoin d'accumuler de vraies valeurs "datetime", alors vous voulez probablement vraiment une construction comme celle-ci ou similaire :
{ "$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"
}
...
}}
Utilisation de $dateFromParts
et d'autres opérateurs d'agrégation de dates
pour extraire les informations "jour" et présenter la date sous cette forme pour accumulation.
Commencer à dénormaliser
Ce qui devrait être clair à partir du "désordre" ci-dessus, c'est que travailler avec des tableaux imbriqués n'est pas exactement facile. De telles structures n'étaient généralement même pas possibles à mettre à jour de manière atomique dans les versions antérieures à MongoDB 3.6, et même si vous ne les avez même jamais mises à jour ou avez vécu avec le remplacement de l'ensemble du tableau, elles ne sont toujours pas simples à interroger. C'est ce qu'on vous montre.
Où vous devez avoir un contenu de tableau dans un document parent, il est généralement conseillé de "aplatir" et "dénormaliser" de telles structures. Cela peut sembler contraire à la pensée relationnelle, mais c'est en fait la meilleure façon de gérer ces données pour des raisons de performances :
{
"_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
}
]
}
Ce sont toutes les mêmes données que celles affichées à l'origine, mais au lieu de imbriquer en fait, nous mettons tout dans un tableau aplati singulier dans chaque document parent. Bien sûr, cela signifie duplication de divers points de données, mais la différence de complexité et de performances des requêtes devrait être évidente :
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" }
}}
])
Maintenant, au lieu d'imbriquer ces $elemMatch
appels et de même pour le $filter
expressions, tout est beaucoup plus clair et facile à lire et vraiment assez simple dans le traitement. Il y a un autre avantage dans le fait que vous pouvez même indexer les clés des éléments du tableau tels qu'ils sont utilisés dans la requête. C'était une contrainte de l'imbriquée modèle où MongoDB n'autorisera tout simplement pas une telle "indexation multiclé" sur les clés de tableaux dans des tableaux . Avec une seule baie, cela est autorisé et peut être utilisé pour améliorer les performances.
Tout après le "filtrage du contenu du tableau" reste alors exactement le même, à l'exception qu'il ne s'agit que de noms de chemin comme "data.sales_date"
par opposition à la longue "first_level.second_level.third_level.forth_level.sales_date"
de la structure précédente.
Quand NE PAS intégrer
Enfin, l'autre grande idée fausse est que TOUTES les relations doivent être traduits par intégration dans des tableaux. Cela n'a jamais vraiment été l'intention de MongoDB et vous n'étiez jamais censé conserver les données "liées" dans le même document dans un tableau dans le cas où cela signifiait faire une seule récupération de données par opposition aux "jointures".
Le modèle classique « Commande/Détails » s'applique généralement là où, dans le monde moderne, vous souhaitez afficher un « en-tête » pour une « Commande » avec des détails tels que l'adresse du client, le total de la commande, etc. dans le même « écran » que les détails de différents éléments de campagne sur la "Commande".
Au début du RDBMS, l'écran typique de 80 caractères sur 25 lignes avait simplement de telles informations "d'en-tête" sur un écran, puis les lignes de détail pour tout ce qui était acheté étaient sur un écran différent. Donc, naturellement, il y avait un certain bon sens pour les stocker dans des tables séparées. Au fur et à mesure que le monde se déplaçait vers plus de détails sur de tels "écrans", vous voulez généralement voir le tout, ou au moins "l'en-tête" et les premières lignes d'un tel "ordre".
D'où la raison pour laquelle ce type d'arrangement est logique à mettre dans un tableau, puisque MongoDB renvoie un "document" contenant toutes les données associées en une seule fois. Pas besoin de requêtes séparées pour des écrans rendus séparés et pas besoin de "jointures" sur ces données puisqu'elles sont déjà "pré-jointes" pour ainsi dire.
Déterminez si vous en avez besoin - AKA "Entièrement" Dénormaliser
Donc, dans les cas où vous savez à peu près que vous n'êtes pas réellement intéressé à traiter la plupart des données dans de tels tableaux la plupart du temps, il est généralement plus logique de simplement tout mettre dans une seule collection avec simplement une autre propriété dans afin d'identifier le "parent" si une telle "adhésion" est occasionnellement requise :
{
"_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
}
Encore une fois, ce sont les mêmes données, mais cette fois dans des documents complètement séparés avec une référence au parent au mieux dans le cas où vous pourriez en avoir besoin à d'autres fins. Notez que les agrégations ici ne sont pas du tout liées aux données parentes et il est également clair où les performances supplémentaires et la complexité supprimée entrent en jeu en les stockant simplement dans une collection séparée :
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" }
}}
])
Puisque tout est déjà un document, il n'est pas nécessaire de "filtrer les tableaux" ou avoir l'une des autres complexités. Tout ce que vous faites est de sélectionner les documents correspondants et d'agréger les résultats, avec exactement les deux mêmes étapes finales qui ont toujours existé.
Dans le but d'obtenir simplement les résultats finaux, cela fonctionne bien mieux que l'une ou l'autre des alternatives ci-dessus. La requête en question ne concerne vraiment que les données "détaillées". Par conséquent, la meilleure solution consiste à séparer complètement le détail du parent, car cela fournira toujours les meilleurs avantages en termes de performances.
Et le point global ici est où le modèle d'accès réel du reste de l'application JAMAIS doit renvoyer l'intégralité du contenu du tableau, il n'aurait probablement pas dû être intégré de toute façon. Apparemment, la plupart des opérations "d'écriture" ne devraient de toute façon jamais avoir besoin de toucher le parent associé, et c'est un autre facteur décisif où cela fonctionne ou non.
Conclusion
Le message général est à nouveau qu'en règle générale, vous ne devez jamais imbriquer des tableaux. Tout au plus, vous devriez conserver un tableau "singulier" avec des données partiellement dénormalisées dans le document parent associé, et où les modèles d'accès restants n'utilisent pas du tout le parent et l'enfant en tandem, alors les données devraient vraiment être séparées.
Le "grand" changement est que toutes les raisons pour lesquelles vous pensez que la normalisation des données est réellement bonne, s'avèrent être l'ennemi de ces systèmes de documents intégrés. Éviter les "jointures" est toujours une bonne chose, mais créer une structure imbriquée complexe pour avoir l'apparence de données "jointes" ne fonctionne jamais vraiment à votre avantage non plus.
Le coût de la gestion de ce que vous "pensez" être la normalisation finit généralement par dépasser le stockage supplémentaire et la maintenance des données dupliquées et dénormalisées dans votre stockage éventuel.
Notez également que tous les formulaires ci-dessus renvoient le même jeu de résultats. C'est assez dérivé en ce sens que les exemples de données pour la brièveté n'incluent que des articles singuliers, ou tout au plus lorsqu'il y a plusieurs niveaux de prix, la "moyenne" est toujours 1
puisque c'est ce que sont toutes les valeurs de toute façon. Mais le contenu pour expliquer cela est déjà excessivement long donc c'est vraiment juste "par exemple":
{
"_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
}