Czy klauzula $ in w MongoDB gwarantuje kolejność


Odpowiedzi:


80

Jak zauważono, kolejność argumentów w tablicy klauzuli $ in nie odzwierciedla kolejności, w jakiej dokumenty są pobierane. Będzie to oczywiście naturalna kolejność lub wybrana kolejność indeksu, jak pokazano.

Jeśli chcesz zachować tę kolejność, zasadniczo masz dwie opcje.

Powiedzmy więc, że dopasowujesz wartości _idw swoich dokumentach z tablicą, która zostanie przekazana do $inas [ 4, 2, 8 ].

Podejdź za pomocą Aggregate


var list = [ 4, 2, 8 ];

db.collection.aggregate([

    // Match the selected documents by "_id"
    { "$match": {
        "_id": { "$in": [ 4, 2, 8 ] },
    },

    // Project a "weight" to each document
    { "$project": {
        "weight": { "$cond": [
            { "$eq": [ "$_id", 4  ] },
            1,
            { "$cond": [
                { "$eq": [ "$_id", 2 ] },
                2,
                3
            ]}
        ]}
    }},

    // Sort the results
    { "$sort": { "weight": 1 } }

])

Więc to byłaby rozszerzona forma. Zasadniczo dzieje się tutaj to, że tak jak tablica wartości jest przekazywana do $inCiebie, również konstruujesz „zagnieżdżony”$cond instrukcję, aby przetestować wartości i przypisać odpowiednią wagę. Ponieważ ta wartość „wagi” odzwierciedla kolejność elementów w tablicy, możesz następnie przekazać tę wartość do etapu sortowania, aby uzyskać wyniki w wymaganej kolejności.

Oczywiście faktycznie „budujesz” instrukcję potoku w kodzie, podobnie jak to:

var list = [ 4, 2, 8 ];

var stack = [];

for (var i = list.length - 1; i > 0; i--) {

    var rec = {
        "$cond": [
            { "$eq": [ "$_id", list[i-1] ] },
            i
        ]
    };

    if ( stack.length == 0 ) {
        rec["$cond"].push( i+1 );
    } else {
        var lval = stack.pop();
        rec["$cond"].push( lval );
    }

    stack.push( rec );

}

var pipeline = [
    { "$match": { "_id": { "$in": list } }},
    { "$project": { "weight": stack[0] }},
    { "$sort": { "weight": 1 } }
];

db.collection.aggregate( pipeline );

Podejdź za pomocą mapReduce


Oczywiście, jeśli to wszystko wydaje się mocne dla twojej wrażliwości, możesz zrobić to samo za pomocą mapReduce, które wygląda na prostsze, ale prawdopodobnie będzie działać nieco wolniej.

var list = [ 4, 2, 8 ];

db.collection.mapReduce(
    function () {
        var order = inputs.indexOf(this._id);
        emit( order, { doc: this } );
    },
    function() {},
    { 
        "out": { "inline": 1 },
        "query": { "_id": { "$in": list } },
        "scope": { "inputs": list } ,
        "finalize": function (key, value) {
            return value.doc;
        }
    }
)

Zasadniczo polega to na tym, że emitowane wartości „klucza” są w „kolejności indeksów”, w jakiej występują w tablicy wejściowej.


Więc to są w istocie sposoby na utrzymanie kolejności listy wejściowej do $instanu, w którym masz już tę listę w określonej kolejności.


2
Świetna odpowiedź. Dla tych, którzy tego potrzebują, wersja skryptu kawowego tutaj
Lawrence Jones

1
@NeilLunn Wypróbowałem podejście z wykorzystaniem agregatu, ale otrzymałem identyfikator i wagę. Czy wiesz, jak odzyskać posty (obiekt)?
Juanjo Lainez Reche

1
@NeilLunn Właściwie to zrobiłem (jest tutaj stackoverflow.com/questions/27525235/… ) Ale jedyny komentarz dotyczył tutaj, mimo że sprawdziłem to przed wysłaniem mojego pytania. Możesz mi tam pomóc? Dziękuję Ci!
Juanjo Lainez Reche

1
wiem, że to jest stare, ale zmarnowałem dużo czasu na debugowanie, dlaczego inputs.indexOf () nie pasuje do this._id. Jeśli zwracasz tylko wartość identyfikatora obiektu, być może będziesz musiał wybrać następującą składnię: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (input [i])) {var order = i; }} emitować (order, {doc: this}); };
NoobSter

1
możesz użyć „$ addFields” zamiast „$ project”, jeśli chcesz mieć również wszystkie oryginalne pola
Jodo,

40

Inny sposób użycia kwerendy agregacji ma zastosowanie tylko do wersji MongoDB> = 3.4 -

To zasługa tego miłego wpisu na blogu .

Przykładowe dokumenty do pobrania w tej kolejności -

var order = [ "David", "Charlie", "Tess" ];

Zapytanie -

var query = [
             {$match: {name: {$in: order}}},
             {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
             {$sort: {"__order": 1}}
            ];

var result = db.users.aggregate(query);

Kolejny cytat z postu wyjaśniający zastosowane operatory agregacji -

Etap „$ addFields” jest nowością w wersji 3.4 i umożliwia „projektowanie” nowych pól w istniejących dokumentach bez znajomości wszystkich innych istniejących pól. Nowe wyrażenie "$ indexOfArray" zwraca pozycję konkretnego elementu w danej tablicy.

Zasadniczo addFieldsoperator dołącza nowe orderpole do każdego dokumentu, gdy je znajdzie, a to orderpole reprezentuje oryginalną kolejność podanej przez nas tablicy. Następnie po prostu sortujemy dokumenty na podstawie tego pola.


czy istnieje sposób na przechowywanie tablicy zamówień jako zmiennej w zapytaniu, abyśmy nie otrzymywali tego masowego zapytania o tę samą tablicę dwa razy, jeśli tablica jest duża?
Ethan SK,

27

Jeśli nie chcesz używać aggregate, innym rozwiązaniem jest użycie, finda następnie posortowanie wyników dokumentu po stronie klienta za pomocą array#sort:

Jeśli $inwartości są typami pierwotnymi, takimi jak liczby, możesz użyć podejścia takiego:

var ids = [4, 2, 8, 1, 9, 3, 5, 6];
MyModel.find({ _id: { $in: ids } }).exec(function(err, docs) {
    docs.sort(function(a, b) {
        // Sort docs by the order of their _id values in ids.
        return ids.indexOf(a._id) - ids.indexOf(b._id);
    });
});

Jeśli $inwartości nie są typami pierwotnymi, takimi jak ObjectIds, wymagane jest inne podejście, ponieważ indexOfw tym przypadku porównuje się przez odniesienie.

Jeśli używasz Node.js 4.x +, możesz użyć Array#findIndexi, ObjectID#equalsaby sobie z tym poradzić, zmieniając sortfunkcję na:

docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - 
                    ids.findIndex(id => b._id.equals(id)));

Lub z dowolną wersją Node.js, z podkreśleniem / lodash findIndex:

docs.sort(function (a, b) {
    return _.findIndex(ids, function (id) { return a._id.equals(id); }) -
           _.findIndex(ids, function (id) { return b._id.equals(id); });
});

skąd funkcja equal porównuje właściwość id z id 'return a.equals (id);', ponieważ a przechowuje wszystkie właściwości zwrócone dla tego modelu?
lboyel

1
@lboyel Nie chciałem, żeby był taki sprytny :-), ale to zadziałało, ponieważ używał Mongoose Document#equalsdo porównania z _idpolem doc . Zaktualizowano, aby _idporównanie było wyraźne. Dzięki, że pytasz.
JohnnyHK

6

Podobnie jak w rozwiązaniu JonnyHK , możesz zmienić kolejność dokumentów zwróconych findw swoim kliencie (jeśli twój klient jest w JavaScript) za pomocą kombinacji mapi Array.prototype.findfunkcji w EcmaScript 2015:

Collection.find({ _id: { $in: idArray } }).toArray(function(err, res) {

    var orderedResults = idArray.map(function(id) {
        return res.find(function(document) {
            return document._id.equals(id);
        });
    });

});

Kilka uwag:

  • Powyższy kod używa sterownika Mongo Node, a nie Mongoose
  • idArrayJest tablicąObjectId
  • Nie testowałem wydajności tej metody w porównaniu z sortowaniem, ale jeśli potrzebujesz manipulować każdym zwróconym elementem (co jest dość powszechne), możesz to zrobić w mapwywołaniu zwrotnym, aby uprościć swój kod.

Czas działania wynosi O (n * n), ponieważ wewnętrzna strona findprzechodzi przez tablicę dla każdego elementu tablicy (od zewnętrznej map). Jest to strasznie nieefektywne, ponieważ istnieje rozwiązanie O (n) korzystające z tabeli przeglądowej.
curran

5

Wiem, że to pytanie jest związane z frameworkiem Mongoose JS, ale zduplikowany jest ogólny, więc mam nadzieję, że opublikowanie tutaj rozwiązania w języku Python (PyMongo) jest w porządku.

things = list(db.things.find({'_id': {'$in': id_array}}))
things.sort(key=lambda thing: id_array.index(thing['_id']))
# things are now sorted according to id_array order

5

Łatwym sposobem uporządkowania wyniku po zwróceniu tablicy przez mongo jest utworzenie obiektu o identyfikatorze jako kluczy, a następnie odwzorowanie podanych _id w celu zwrócenia tablicy, która jest poprawnie uporządkowana.

async function batchUsers(Users, keys) {
  const unorderedUsers = await Users.find({_id: {$in: keys}}).toArray()
  let obj = {}
  unorderedUsers.forEach(x => obj[x._id]=x)
  const ordered = keys.map(key => obj[key])
  return ordered
}

1
Robi dokładnie to, czego potrzebuję i jest znacznie prostszy niż górny komentarz.
dyarbrough

@dyarbrough to rozwiązanie działa tylko w przypadku zapytań, które pobierają wszystkie dokumenty (bez ograniczeń lub pomijania). Górny komentarz jest bardziej złożony, ale sprawdza się w każdym scenariuszu.
marian2js

3

Zawsze? Nigdy. Kolejność jest zawsze taka sama: nieokreślona (prawdopodobnie fizyczna kolejność, w jakiej przechowywane są dokumenty). Chyba że to posortujesz.


$naturalporządek normalny, który jest raczej logiczny niż fizyczny
Sammaye

1

Wiem, że to stary wątek, ale jeśli po prostu zwracasz wartość identyfikatora w tablicy, być może będziesz musiał wybrać tę składnię. Ponieważ nie mogłem uzyskać wartości indexOf pasującej do formatu mongo ObjectId.

  obj.map = function() {
    for(var i = 0; i < inputs.length; i++){
      if(this._id.equals(inputs[i])) {
        var order = i;
      }
    }
    emit(order, {doc: this});
  };

Jak przekonwertować mongo ObjectId .toString bez dołączania opakowania „ObjectId ()” - tylko wartość?



0

Jest to rozwiązanie kodowe po pobraniu wyników z Mongo. Używanie mapy do przechowywania indeksu, a następnie zamiana wartości.

catDetails := make([]CategoryDetail, 0)
err = sess.DB(mdb).C("category").
    Find(bson.M{
    "_id":       bson.M{"$in": path},
    "is_active": 1,
    "name":      bson.M{"$ne": ""},
    "url.path":  bson.M{"$exists": true, "$ne": ""},
}).
    Select(
    bson.M{
        "is_active": 1,
        "name":      1,
        "url.path":  1,
    }).All(&catDetails)

if err != nil{
    return 
}
categoryOrderMap := make(map[int]int)

for index, v := range catDetails {
    categoryOrderMap[v.Id] = index
}

counter := 0
for i := 0; counter < len(categoryOrderMap); i++ {
    if catId := int(path[i].(float64)); catId > 0 {
        fmt.Println("cat", catId)
        if swapIndex, exists := categoryOrderMap[catId]; exists {
            if counter != swapIndex {
                catDetails[swapIndex], catDetails[counter] = catDetails[counter], catDetails[swapIndex]
                categoryOrderMap[catId] = counter
                categoryOrderMap[catDetails[swapIndex].Id] = swapIndex
            }
            counter++
        }
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.