Beginnen wir mit einem grundlegenden Haftungsausschluss, da der Hauptteil dessen, was das Problem beantwortet, bereits hier unter Find beantwortet wurde im doppelt verschachtelten Array MongoDB . Und "fürs Protokoll" das Double gilt auch für Triple oder Quadrupal oder JEDER Ebene der Verschachtelung als im Grunde das gleiche Prinzip IMMER .
Der andere Hauptpunkt jeder Antwort ist auch Don't NEST Arrays , da auch in dieser Antwort erklärt wird ( und ich habe dies viele wiederholt Mal ), aus welchem Grund auch immer Sie "denken" Sie haben für "nesting" tatsächlich gibt Ihnen nicht die Vorteile, die Sie wahrnehmen, es wird. Tatsächlich "verschachtelt" macht das Leben wirklich nur viel schwieriger.
Verschachtelte Probleme
Das größte Missverständnis bei der Übersetzung einer Datenstruktur aus einem "relationalen" Modell wird fast immer als "eine verschachtelte Array-Ebene hinzufügen" interpretiert für jedes zugehörige Modell. Was Sie hier präsentieren, ist keine Ausnahme von diesem Missverständnis, da es sehr stark "normalisiert" zu sein scheint sodass jedes Unterarray die zugehörigen Elemente zu seinem übergeordneten Element enthält.
MongoDB ist eine auf „Dokumenten“ basierende Datenbank, die es Ihnen also ziemlich genau ermöglicht, dies zu tun, oder tatsächlich jeden Datenstrukturinhalt, den Sie im Grunde möchten. Das bedeutet jedoch nicht, dass die Daten in einer solchen Form einfach zu handhaben oder für den eigentlichen Zweck tatsächlich praktikabel sind.
Füllen wir das Schema zur Veranschaulichung mit einigen tatsächlichen Daten aus:
{
"_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
}
]
}
]
}
]
}
]
}
Das unterscheidet sich ein wenig von der Struktur in der Frage, aber zu Demonstrationszwecken enthält es die Dinge, die wir uns ansehen müssen. Hauptsächlich gibt es ein Array im Dokument, das Elemente mit einem Sub-Array enthält, das wiederum Elemente in einem Sub-Array enthält und so weiter. Die "Normalisierung" hier steht natürlich durch die bezeichner auf jedem "level" als "item type" oder was auch immer du eigentlich hast.
Das Kernproblem ist, dass Sie nur „einige“ der Daten aus diesen verschachtelten Arrays haben möchten, und MongoDB wirklich nur das „Dokument“ zurückgeben möchte, was bedeutet, dass Sie einige Manipulationen vornehmen müssen, um nur zu den übereinstimmenden „Sub- Artikel".
Auch zum Thema richtig Die Auswahl des Dokuments, das all diesen „Unterkriterien“ entspricht, erfordert eine umfassende Verwendung von $elemMatch
um die richtige Kombination von Bedingungen auf jeder Ebene von Array-Elementen zu erhalten. Sie können nicht direkt die "Punktnotation"
verwenden aufgrund der Notwendigkeit dieser mehrere Bedingungen
. Ohne $elemMatch
Aussagen erhalten Sie nicht die genaue "Kombination" und erhalten nur Dokumente, bei denen die Bedingung auf beliebige zutraf Array-Element.
Was das eigentliche "Herausfiltern des Array-Inhalts" betrifft dann ist das eigentlich der Teil des zusätzlichen Unterschieds:
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" }
}}
])
Dies lässt sich am besten als „chaotisch“ und „involviert“ beschreiben. Unsere anfängliche Abfrage für die Dokumentenauswahl ist nicht nur der $elemMatch
mehr als ein Bissen, aber dann haben wir den folgenden $filter
und $map
Verarbeitung für jede Array-Ebene. Wie bereits erwähnt, ist dies das Muster, egal wie viele Ebenen es tatsächlich gibt.
Alternativ können Sie $unwind
ausführen
und $match
kombinieren, anstatt die vorhandenen Arrays zu filtern, aber dies verursacht zusätzlichen Overhead für $unwind
bevor der unerwünschte Inhalt entfernt wird, daher ist es in modernen Versionen von MongoDB im Allgemeinen besser, $filter
aus dem Array zuerst.
Die letzte Stelle hier ist, dass Sie $group
durch Elemente, die sich tatsächlich innerhalb des Arrays befinden, sodass Sie am Ende $unwind
Jede Ebene der Arrays sowieso davor.
Die eigentliche „Gruppierung“ erfolgt dann meist unkompliziert über das sales_date
und price
Eigenschaften für die erste Akkumulation und dann Hinzufügen einer nachfolgenden Stufe zu $push
der andere price
Werte, für die Sie einen Durchschnitt innerhalb jedes Datums als Sekunde akkumulieren möchten Akkumulation.
HINWEIS :Die tatsächliche Handhabung von Daten kann in der praktischen Verwendung durchaus variieren, je nachdem, wie detailliert Sie sie speichern. In diesem Beispiel sind die Daten alle nur bereits auf den Beginn eines jeden "Tages" gerundet. Wenn Sie tatsächlich echte "datetime"-Werte sammeln müssen, dann möchten Sie wahrscheinlich wirklich ein Konstrukt wie dieses oder ein ähnliches:
{ "$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"
}
...
}}
Verwenden von $dateFromParts
und andere Datumsaggregationsoperatoren
um die "Tag"-Informationen zu extrahieren und das Datum in dieser Form zur Akkumulation wiederzugeben.
Beginnt mit der Denormalisierung
Was aus dem obigen "Durcheinander" klar sein sollte, ist, dass das Arbeiten mit verschachtelten Arrays nicht gerade einfach ist. Solche Strukturen konnten in Versionen vor MongoDB 3.6 im Allgemeinen nicht einmal atomar aktualisiert werden, und selbst wenn Sie sie nie aktualisiert oder damit gelebt haben, im Grunde das gesamte Array zu ersetzen, sind sie immer noch nicht einfach abzufragen. Das wird Ihnen angezeigt.
Wo Sie müssen Array-Inhalte innerhalb eines übergeordneten Dokuments haben, wird allgemein empfohlen, "flachen" und "denormalisieren" solche Strukturen. Dies mag dem relationalen Denken zuwiderlaufen, ist aber aus Leistungsgründen tatsächlich der beste Weg, mit solchen Daten umzugehen:
{
"_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
}
]
}
Das sind alle die gleichen Daten wie ursprünglich gezeigt, jedoch statt Verschachtelung Wir haben eigentlich nur alles in ein einzelnes abgeflachtes Array in jedem übergeordneten Dokument eingefügt. Sicher, das bedeutet Duplizierung von verschiedenen Datenpunkten, aber der Unterschied in der Abfragekomplexität und -leistung sollte offensichtlich sein:
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" }
}}
])
Anstatt diese $elemMatch
zu verschachteln
Aufrufe und ähnlich für den $filter
Ausdrücken ist alles viel klarer und leichter lesbar und wirklich ganz einfach in der Verarbeitung. Ein weiterer Vorteil besteht darin, dass Sie sogar die Schlüssel der Elemente im Array so indizieren können, wie sie in der Abfrage verwendet werden. Das war eine Einschränkung der verschachtelten Modell, bei dem MongoDB eine solche "Multikey-Indizierung" auf Schlüsseln von Arrays innerhalb von Arrays . Bei einem einzelnen Array ist dies erlaubt und kann zur Leistungssteigerung verwendet werden.
Alles nach dem "Array Content Filtering" bleibt dann genau gleich, mit der Ausnahme, dass es sich nur um Pfadnamen wie "data.sales_date"
handelt im Gegensatz zum langatmigen "first_level.second_level.third_level.forth_level.sales_date"
von der vorherigen Struktur.
Wann NICHT eingebettet werden soll
Schließlich ist das andere große Missverständnis, dass ALLE Beziehungen müssen als Einbettung in Arrays übersetzt werden. Dies war wirklich nie die Absicht von MongoDB und Sie sollten immer nur "zusammengehörige" Daten innerhalb desselben Dokuments in einem Array aufbewahren, wenn dies bedeutet, dass im Gegensatz zu "Joins" ein einziger Datenabruf durchgeführt wird.
Das klassische „Bestellung/Details“-Modell findet hier typischerweise Anwendung, wenn Sie in der modernen Welt „Kopfzeilen“ für eine „Bestellung“ mit Details wie Kundenadresse, Bestellsumme usw. innerhalb desselben „Bildschirms“ wie die Details anzeigen möchten verschiedene Einzelposten auf der "Bestellung".
Vor langer Zeit, zu Beginn des RDBMS, hatte der typische Bildschirm mit 80 Zeichen und 25 Zeilen einfach solche „Kopfzeilen“-Informationen auf einem Bildschirm, dann befanden sich die Detailzeilen für alles, was gekauft wurde, auf einem anderen Bildschirm. Natürlich gab es ein gewisses Maß an gesundem Menschenverstand, um diese in separaten Tabellen zu speichern. Wenn sich die Welt auf solchen "Bildschirmen" immer mehr ins Detail bewegt, möchte man normalerweise das Ganze sehen, oder zumindest den "Header" und die ersten so vielen Zeilen einer solchen "Ordnung".
Daher ist es sinnvoll, diese Art von Anordnung in ein Array einzufügen, da MongoDB ein "Dokument" zurückgibt, das die zugehörigen Daten auf einmal enthält. Keine Notwendigkeit für separate Anfragen für separate gerenderte Bildschirme und keine Notwendigkeit für "Joins" bei solchen Daten, da sie sozusagen bereits "pre-joined" sind.
Überlegen Sie, ob Sie es brauchen - AKA "Fully" Denormalize
In Fällen, in denen Sie also ziemlich genau wissen, dass Sie die meiste Zeit nicht wirklich daran interessiert sind, mit den meisten Daten in solchen Arrays umzugehen, ist es im Allgemeinen sinnvoller, alles einfach in eine eigene Sammlung mit lediglich einer anderen Eigenschaft zu packen um den "Elternteil" zu identifizieren, sollte ein solches "Beitreten" gelegentlich erforderlich sein:
{
"_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
}
Wieder sind es die gleichen Daten, nur dieses Mal in völlig getrennten Dokumenten mit einem Hinweis auf den Elternteil, allenfalls für den Fall, dass Sie sie vielleicht tatsächlich für einen anderen Zweck benötigen. Beachten Sie, dass sich die Aggregationen hier überhaupt nicht auf die übergeordneten Daten beziehen, und es ist auch klar, wo die zusätzliche Leistung und die verringerte Komplexität durch einfaches Speichern in einer separaten Sammlung ins Spiel kommen:
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" }
}}
])
Da alles bereits ein Dokument ist, besteht keine Notwendigkeit, "Arrays herunterzufiltern" oder eine der anderen Komplexitäten haben. Alles, was Sie tun, ist die passenden Dokumente auszuwählen und die Ergebnisse zu aggregieren, mit genau denselben zwei abschließenden Schritten, die schon immer vorhanden waren.
Um nur zu den Endergebnissen zu gelangen, ist dies weitaus besser als die beiden oben genannten Alternativen. Die fragliche Abfrage betrifft wirklich nur die "Detail"-Daten, daher ist es am besten, das Detail vollständig von den übergeordneten Daten zu trennen, da dies immer den besten Leistungsvorteil bietet.
Und der allgemeine Punkt hier ist, wo das tatsächliche Zugriffsmuster des Rests der Anwendung NIE ist den gesamten Array-Inhalt zurückgeben muss, dann hätte es wahrscheinlich sowieso nicht eingebettet werden sollen. Scheinbar sollten die meisten "Schreib"-Vorgänge in ähnlicher Weise sowieso nie den zugehörigen Elternteil berühren müssen, und das ist ein weiterer entscheidender Faktor, wo dies funktioniert oder nicht.
Schlussfolgerung
Die allgemeine Botschaft lautet wiederum, dass Sie Arrays im Allgemeinen niemals verschachteln sollten. Sie sollten höchstens ein "singuläres" Array mit teilweise denormalisierten Daten innerhalb des zugehörigen übergeordneten Dokuments behalten, und wenn die verbleibenden Zugriffsmuster das übergeordnete und das untergeordnete Dokument überhaupt nicht viel verwenden, sollten die Daten wirklich getrennt werden.
Die "große" Änderung besteht darin, dass all die Gründe, warum Sie denken, dass die Normalisierung von Daten eigentlich gut ist, sich als Feind solcher eingebetteten Dokumentensysteme herausstellen. Das Vermeiden von „Verknüpfungen“ ist immer gut, aber das Erstellen einer komplexen verschachtelten Struktur, um den Eindruck von „verknüpften“ Daten zu erwecken, funktioniert auch nie wirklich zu Ihrem Vorteil.
Die Kosten für den Umgang mit dem, was Sie für Normalisierung halten, übersteigen normalerweise die zusätzliche Speicherung und Pflege duplizierter und denormalisierter Daten in Ihrem endgültigen Speicher.
Beachten Sie auch, dass alle obigen Formulare dieselbe Ergebnismenge zurückgeben. Es ist insofern ziemlich abgeleitet, als die Beispieldaten der Kürze halber nur einzelne Artikel enthalten oder höchstens dort, wo es mehrere Preispunkte gibt, der "Durchschnitt" immer noch 1
ist denn das sind sowieso alle Werte. Aber der Inhalt, um dies zu erklären, ist schon übermäßig lang, also ist es wirklich nur "am Beispiel":
{
"_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
}