2014-04-29 3 views
6

voglio ottenere l'ultimo documento per ogni stazione con tutti gli altri campi:MongoDB: Aggregazione quadro: Ottenere ultimo documento datato per ID raggruppamento

{ 
     "_id" : ObjectId("535f5d074f075c37fff4cc74"), 
     "station" : "OR", 
     "t" : 86, 
     "dt" : ISODate("2014-04-29T08:02:57.165Z") 
} 
{ 
     "_id" : ObjectId("535f5d114f075c37fff4cc75"), 
     "station" : "OR", 
     "t" : 82, 
     "dt" : ISODate("2014-04-29T08:02:57.165Z") 
} 
{ 
     "_id" : ObjectId("535f5d364f075c37fff4cc76"), 
     "station" : "WA", 
     "t" : 79, 
     "dt" : ISODate("2014-04-29T08:02:57.165Z") 
} 

Ho bisogno di avere t e la stazione per l'ultima dt per stazione . Con il quadro di aggregazione:

db.temperature.aggregate([{$sort:{"dt":1}},{$group:{"_id":"$station", result:{$last:"$dt"}, t:{$last:"$t"}}}]) 

rendimenti

{ 
     "result" : [ 
       { 
         "_id" : "WA", 
         "result" : ISODate("2014-04-29T08:02:57.165Z"), 
         "t" : 79 
       }, 
       { 
         "_id" : "OR", 
         "result" : ISODate("2014-04-29T08:02:57.165Z"), 
         "t" : 82 
       } 
     ], 
     "ok" : 1 
} 

E 'questo il modo più efficace per farlo?

Grazie

+3

la risposta che hai accettato da NeilLunn è effettivamente errata. Non è garantito che l'ordine naturale sia un ordine di inserzione (ad eccezione delle collezioni con limite) e _id è garantito che aumenti in modo monotono se * tutti * delle macchine client sono sincronizzati nel tempo. –

risposta

5

Per rispondere direttamente alla tua domanda, sì, è il modo più efficiente. Ma penso che dobbiamo chiarire perché sia ​​così.

come suggerito nelle alternative, l'unica cosa che la gente sta guardando è "l'ordinamento" i risultati prima di passare ad una fase $group e che cosa stanno guardando è il valore "timestamp", in modo che ci si vuole fare assicurarsi che tutto sia in ordine "timestamp", in modo da qui il modulo:

db.temperature.aggregate([ 
    { "$sort": { "station": 1, "dt": -1 } }, 
    { "$group": { 
     "_id": "$station", 
     "result": { "$first":"$dt"}, "t": {"$first":"$t"} 
    }} 
]) 

E come detto si sarà ovviamente vuole un indice per riflettere che, al fine di rendere il tipo efficiente:

Tuttavia e thi s è il vero punto.Ciò che sembra essere stato trascurato da altri (se non è così per te) è che probabilmente tutti questi dati sono stati inseriti già in ordine cronologico, in quanto ogni lettura è registrata come aggiunta.

Così la bellezza di questo è il campo _id (il valore predefinito è ObjectId) è già in ordine "timestamp", così come si fa in realtà contiene un valore di tempo e questo rende la dichiarazione possibile:

db.temperature.aggregate([ 
    { "$group": { 
     "_id": "$station", 
     "result": { "$last":"$dt"}, "t": {"$last":"$t"} 
    }} 
]) 

E lo è più veloce. Perché? Bene, non è necessario selezionare un indice (codice aggiuntivo da richiamare), inoltre non è necessario "caricare" l'indice oltre al documento.

Abbiamo già sanno i documenti siano in ordine (da _id) in modo che i $last confini sono perfettamente valide. Si sta eseguendo comunque la scansione di tutto e si potrebbe anche eseguire una query "intervallo" sui valori _id come ugualmente validi per due date.

L'unica cosa vera da dire qui, è che nel "mondo reale" utilizzo, potrebbe essere solo più pratico per voi di $match tra intervalli di date quando si fa questo tipo di accumulo invece di ottenere la "prima "e" last "_id valori per definire un" intervallo "o qualcosa di simile nell'utilizzo effettivo.

Quindi, dov'è la prova di questo? Beh, è ​​abbastanza facile da riprodurre, quindi ho solo fatto in modo da generare un po 'di dati di esempio:

var stations = [ 
    "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", 
    "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", 
    "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", 
    "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", 
    "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", 
    "VA", "WA", "WV", "WI", "WY" 
]; 


for (i=0; i<200000; i++) { 

    var station = stations[Math.floor(Math.random()*stations.length)]; 
    var t = Math.floor(Math.random() * (96 - 50 + 1)) +50; 
    dt = new Date(); 

    db.temperatures.insert({ 
     station: station, 
     t: t, 
     dt: dt 
    }); 

} 

Il mio hardware (computer portatile da 8 GB con disco spinny, che non è stellare, ma certamente adeguato) in esecuzione ogni forma di la dichiarazione mostra chiaramente una pausa notevole con la versione che utilizza un indice e un ordinamento (stesse chiavi sull'indice dell'istruzione sort). È solo una piccola pausa, ma la differenza è abbastanza significativa da notare.

Anche guardando in uscita spiegare (versione 2.6 in su, o in realtà c'è in 2.4.9 anche se non documentato) si può vedere la differenza in quanto, anche se il $sort è ottimizzato fuori a causa della presenza di un indice, il tempo impiegato sembra essere con la selezione dell'indice e quindi il caricamento delle voci indicizzate. L'inclusione di tutti i campi per una query dell'indice "coperta" "coperta" non fa alcuna differenza.

Anche per la cronologia, indicizzazione puramente della data e solo l'ordinamento sui valori di data dà lo stesso risultato. Forse leggermente più veloce, ma ancora più lento del modulo indice naturale senza l'ordinamento.

Quindi, fintanto che si può tranquillamente "range" sulle prima e ultimi_id valori, allora è vero che utilizzando l'indice naturale sul ordine di inserimento è in realtà il modo più efficace per farlo. Il tuo chilometraggio nel mondo reale può variare a seconda che questo sia pratico o meno per te e potrebbe semplicemente risultare più conveniente implementare l'indice e l'ordinamento alla data.

Ma se eri felice con l'utilizzo di _id gamme o superiori al "ultima" _id nella query, allora forse un Tweak per ottenere i valori insieme con i vostri risultati in modo da poter in serbo fatto e utilizzare le informazioni in query successive:

db.temperature.aggregate([ 
    // Get documents "greater than" the "highest" _id value found last time 
    { "$match": { 
     "_id": { "$gt": ObjectId("536076603e70a99790b7845d") } 
    }}, 

    // Do the grouping with addition of the returned field 
    { "$group": { 
     "_id": "$station", 
     "result": { "$last":"$dt"}, 
     "t": {"$last":"$t"}, 
     "lastDoc": { "$last": "$_id" } 
    }} 
]) 

e se si fosse in realtà "in seguito" i risultati del genere, allora è possibile determinare il valore massimo di ObjectId dai risultati e usarlo nella prossima interrogazione.

In ogni caso, divertiti a giocare con quello, ma di nuovo Sì, in questo caso quella query è il modo più veloce.

2

Un indice è tutto ciò che ha realmente bisogno:

db.temperature.ensureIndex({ 'station': 1, 'dt': 1 }) 
for s in db.temperature.distinct('station'): 
    db.temperature.find({ station: s }).sort({ dt : -1 }).limit(1) 

naturalmente utilizzando qualsiasi sintassi è in realtà valida per la propria lingua.

Modifica: è corretto che un loop come questo comporta un viaggio di andata e ritorno per stazione, ed è ottimo per alcune stazioni, e non così buono per 1000. Si desidera comunque l'indice composto sulla stazione + dt, anche se , e per approfittare di un ordinamento decrescente:

db.temperature.aggregate([ 
    { $sort: { station: 1, dt: -1 } }, 
    { $group: { _id: "$station", result: {$first:"$dt"}, t: {$first:"$t"} } } 
]) 
+1

Farai n trovare con il tuo codice. Ho migliaia di stazioni ... Ecco perché voglio usare il framework di aggregazione per avere una sola richiesta. Grazie per il suggerimento di indice – hotips

+0

Quindi, per la cronologia, in questo caso la definizione di questo tipo verrà effettivamente eseguita più lentamente. La cosa da considerare qui è che i documenti sono in realtà già in ordine di inserimento. Con questo caso, ho scritto questo con un esempio di caso di prova per dimostrare perché è così. –

1

per quanto riguarda la query di aggregazione che hai postato, mi piacerebbe fare certi che si dispone di un indice su dt:

db.temperature.ensureIndex({'dt': 1 }) 

Questa volontà accertarsi che l'ordinamento $ all'inizio della pipeline di aggregazione sia il più efficiente possibile e.

Se questo è il modo più efficace per ottenere questi dati, rispetto a una query in un ciclo, sarà probabilmente una funzione di quanti punti di dati si dispone. All'inizio, con "migliaia di stazioni" e forse centinaia di migliaia di punti dati, penserei che l'approccio di aggregazione sarà più veloce.

Tuttavia, man mano che si aggiungono un numero sempre maggiore di dati, la query di aggregazione continuerà a toccare tutti i documenti. Ciò diventerà sempre più costoso man mano che aumenterai di milioni o più documenti. Un approccio per quel caso sarebbe quello di aggiungere un limite $ subito dopo l'ordinamento $ per limitare il numero totale di documenti considerati. È un po 'hacky e inesatto, ma potrebbe aiutare a limitare il numero totale di documenti a cui è necessario accedere.

+1

Posso usare _id per l'ordinamento, è più veloce di IsoDate, penso. – hotips

+0

E in realtà non è così. I valori di '_id' sono già nell'ordine richiesto, e un caso di test (come mostrato) dimostra quando questo è il caso, definendo un indice e un ordinamento verrà effettivamente eseguito più lentamente. –

+0

@NeilLunn errato, i valori _id non sono già nell'ordine richiesto, a meno che non li stia leggendo da un indice (che è ciò che accade quando si ordina per _id). –