MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Somma l'array nidificato in node.js mongodb

Iniziamo con un disclaimer di base in quanto il corpo principale di ciò che risponde al problema è già stato risolto qui su Trova in Double Nested Array MongoDB . E "per la cronaca" il Doppio si applica anche a Triplo o Quadrupi o QUALSIASI livello di nidificazione come sostanzialmente lo stesso principio SEMPRE .

L'altro punto principale di qualsiasi risposta è anche Non NEST Arrays , poiché come spiegato anche in quella risposta ( e l'ho ripetuto molti volte ), qualunque sia il motivo per cui "pensa" hai per "annidamento" in realtà non ti dà i vantaggi che percepisci. Infatti "annidamento" sta davvero solo rendendo la vita molto più difficile.

Problemi nidificati

L'idea sbagliata principale di qualsiasi traduzione di una struttura dati da un modello "relazionale" è quasi sempre interpretata come "aggiungere un livello di array annidato" per ogni modello associato. Quello che stai presentando qui non fa eccezione a questo equivoco in quanto sembra essere molto "normalizzato" in modo che ogni sottoarray contenga gli elementi correlati al suo genitore.

MongoDB è un database basato su "documenti", quindi ti consente praticamente di fare questo o in effetti qualsiasi contenuto della struttura dei dati che desideri sostanzialmente. Ciò non significa tuttavia che i dati in una tale forma siano facili da utilizzare o effettivamente pratici per lo scopo effettivo.

Compiliamo lo schema con alcuni dati effettivi da dimostrare:

{
  "_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
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

È un "poco" diverso dalla struttura nella domanda, ma a scopo dimostrativo ha le cose che dobbiamo guardare. Principalmente c'è un array nel documento che ha elementi con un sottoarray, che a sua volta ha elementi in un sottoarray e così via. La "normalizzazione" ecco ovviamente gli identificatori su ogni "livello" come "tipo di oggetto" o qualunque cosa tu abbia effettivamente.

Il problema principale è che vuoi solo "alcuni" dei dati all'interno di questi array nidificati, e MongoDB vuole davvero solo restituire il "documento", il che significa che devi fare qualche manipolazione per arrivare a quelli corrispondenti "sub- articoli".

Anche sulla questione del "correttamente" la selezione del documento che soddisfa tutti questi "sottocriteri" richiede un uso estensivo di $elemMatch al fine di ottenere la corretta combinazione di condizioni su ciascun livello di elementi dell'array. Non puoi utilizzare direttamente "Dot Notation" a causa della necessità di quei condizioni multiple . Senza $elemMatch dichiarazioni non ottieni l'esatta "combinazione" e ottieni solo documenti in cui la condizione era vera su qualsiasi elemento dell'array.

Per quanto riguarda effettivamente "filtrare il contenuto dell'array" allora questa è in realtà la parte della differenza aggiuntiva:

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" }
  }}
])

Questo è meglio descritto come "disordinato" e "coinvolto". Non solo la nostra domanda iniziale per la selezione dei documenti con $elemMatch più di un boccone, ma poi abbiamo il successivo $filter e $map elaborazione per ogni livello di array. Come accennato in precedenza, questo è lo schema, non importa quanti livelli ci siano effettivamente.

In alternativa potresti eseguire un $unwind e $match combinazione invece di filtrare gli array in atto, ma ciò causa un sovraccarico aggiuntivo per $unwind prima che il contenuto indesiderato venga rimosso, quindi nelle versioni moderne di MongoDB è generalmente consigliabile $filter prima dall'array.

Il punto finale qui è che vuoi $group da elementi che sono effettivamente all'interno dell'array, quindi finisci per dover $unwind ogni livello degli array comunque prima di questo.

Il "raggruppamento" effettivo è quindi generalmente semplice utilizzando il sales_date e price proprietà per il primo accumulazione e quindi aggiungendo una fase successiva a $push il diverso price valori per i quali vuoi accumulare una media all'interno di ogni data sotto forma di secondo accumulo.

NOTA :La gestione effettiva dei datteri può variare nell'uso pratico a seconda della granularità con cui li conservi. In questo esempio le date sono già tutte arrotondate all'inizio di ogni "giorno". Se hai effettivamente bisogno di accumulare valori "datetime" reali, probabilmente vorrai davvero un costrutto come questo o simile:

{ "$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"
  }
  ...
}}

Utilizzo di $dateFromParts e altri operatori di aggregazione di date per estrarre le informazioni del "giorno" e presentare la data di ritorno in quel modulo per l'accumulo.

Inizio a denormalizzare

Ciò che dovrebbe essere chiaro dal "casino" di cui sopra è che lavorare con gli array nidificati non è esattamente facile. In genere non era nemmeno possibile aggiornare atomicamente tali strutture nelle versioni precedenti a MongoDB 3.6, e anche se non le hai nemmeno aggiornate o hai vissuto sostituendo praticamente l'intero array, non sono ancora semplici da interrogare. Questo è ciò che ti viene mostrato.

Dove devi hanno contenuto di array all'interno di un documento padre, generalmente si consiglia di "appiattire" e "denormalizzare" tali strutture. Questo può sembrare contrario al pensiero relazionale, ma in realtà è il modo migliore per gestire tali dati per motivi di prestazioni:

{
  "_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
     }
  ]
}

Sono tutti gli stessi dati mostrati originariamente, ma invece di nidificare in realtà abbiamo semplicemente inserito tutto in una singola matrice appiattita all'interno di ciascun documento padre. Certo questo significa duplicazione di vari punti dati, ma la differenza nella complessità e nelle prestazioni della query dovrebbe essere evidente:

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" }
  }}
])

Ora invece di annidare quei $elemMatch chiamate e allo stesso modo per $filter espressioni, tutto è molto più chiaro e di facile lettura e davvero abbastanza semplice nell'elaborazione. C'è un altro vantaggio nel fatto che puoi persino indicizzare le chiavi degli elementi nell'array come usate nella query. Questo era un vincolo del nidificato modello in cui MongoDB semplicemente non consentirà tale "Indicizzazione multichiave" sulle chiavi di array all'interno di array . Con un singolo array questo è consentito e può essere utilizzato per migliorare le prestazioni.

Tutto dopo il "filtro dei contenuti dell'array" quindi rimane esattamente lo stesso, con l'eccezione sono solo nomi di percorsi come "data.sales_date" al contrario del prolisso "first_level.second_level.third_level.forth_level.sales_date" dalla struttura precedente.

Quando NON incorporare

Infine, l'altro grande malinteso è che TUTTE le relazioni deve essere tradotto come incorporamento all'interno di array. Questo non è mai stato l'intento di MongoDB e dovevi solo mantenere i dati "correlati" all'interno dello stesso documento in un array nel caso in cui ciò significasse eseguire un unico recupero di dati invece di "unire".

Il classico modello "Ordine/Dettagli" qui si applica in genere laddove nel mondo moderno si desidera visualizzare "intestazione" per un "Ordine" con dettagli come indirizzo del cliente, totale dell'ordine e così via all'interno della stessa "schermata" dei dettagli di elementi pubblicitari diversi nell'"Ordine".

All'inizio dell'RDBMS, il tipico schermo da 80 caratteri per 25 righe aveva semplicemente tali informazioni di "intestazione" su uno schermo, quindi le linee di dettaglio per tutto ciò che veniva acquistato erano su uno schermo diverso. Quindi, naturalmente, c'era un certo livello di buon senso per archiviarli in tabelle separate. Man mano che il mondo si spostava più in dettaglio su tali "schermi", in genere vuoi vedere tutto, o almeno l'"intestazione" e le prime tante righe di un tale "ordine".

Ecco perché questo tipo di disposizione ha senso da inserire in un array, poiché MongoDB restituisce un "documento" contenente i dati correlati tutti in una volta. Non c'è bisogno di richieste separate per schermate renderizzate separate e non c'è bisogno di "unirsi" su tali dati poiché sono già "pre-uniti" per così dire.

Considera se ne hai bisogno - AKA "Completamente" Denormalizza

Quindi, nei casi in cui sai praticamente di non essere effettivamente interessato a gestire la maggior parte dei dati in tali array per la maggior parte del tempo, generalmente ha più senso mettere semplicemente tutto in una raccolta da solo con semplicemente un'altra proprietà in al fine di identificare il "genitore" qualora tale "unione" fosse occasionalmente richiesta:

{
  "_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
}

Anche in questo caso sono gli stessi dati, ma solo questa volta in documenti completamente separati con un riferimento al genitore nella migliore delle ipotesi nel caso in cui potresti effettivamente averne bisogno per un altro scopo. Nota che le aggregazioni qui non si riferiscono affatto ai dati principali ed è anche chiaro dove entrano in gioco le prestazioni aggiuntive e la complessità rimossa semplicemente archiviandole in una raccolta separata:

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" }
  }}
])

Poiché tutto è già un documento, non è necessario "filtrare gli array" o avere una qualsiasi delle altre complessità. Tutto quello che fai è selezionare i documenti corrispondenti e aggregare i risultati, con esattamente gli stessi due passaggi finali che sono stati sempre presenti.

Allo scopo di ottenere solo i risultati finali, questo funziona molto meglio di entrambe le alternative precedenti. La query in questione riguarda in realtà solo i dati "dettagli", quindi la migliore linea d'azione è separare completamente il dettaglio dal genitore poiché fornirà sempre il miglior vantaggio in termini di prestazioni.

E il punto generale qui è dove il modello di accesso effettivo del resto dell'applicazione MAI deve restituire l'intero contenuto dell'array, quindi probabilmente non avrebbe dovuto essere incorporato comunque. Apparentemente, la maggior parte delle operazioni di "scrittura" non dovrebbe mai avere comunque bisogno di toccare il genitore correlato, e questo è un altro fattore decisivo in cui funziona o meno.

Conclusione

Il messaggio generale è ancora una volta che come regola generale non dovresti mai annidare gli array. Al massimo dovresti mantenere un array "singolare" con dati parzialmente denormalizzati all'interno del relativo documento padre, e laddove i modelli di accesso rimanenti in realtà non usano molto il genitore e il figlio in tandem, allora i dati dovrebbero davvero essere separati.

Il "grande" cambiamento è che tutti i motivi per cui si ritiene che la normalizzazione dei dati sia effettivamente buona, si rivela essere il nemico di tali sistemi di documenti incorporati. Evitare i "join" è sempre positivo, ma creare una struttura nidificata complessa per avere l'aspetto di dati "uniti" non funziona mai a tuo vantaggio.

Il costo della gestione di ciò che "pensi" sia la normalizzazione di solito finisce per eliminare l'archiviazione aggiuntiva e il mantenimento di dati duplicati e denormalizzati all'interno del tuo eventuale spazio di archiviazione.

Si noti inoltre che tutti i moduli precedenti restituiscono lo stesso set di risultati. È piuttosto derivato in quanto i dati di esempio per brevità includono solo elementi singolari, o al massimo dove ci sono più punti di prezzo la "media" è ancora 1 poiché è quello che sono comunque tutti i valori. Ma il contenuto per spiegare questo è già estremamente lungo, quindi è davvero solo "per esempio":

{
        "_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
}