it-swarm.com.de

Überprüfen Sie, ob jedes Element im Array der Bedingung entspricht

Ich habe eine Sammlung von Dokumenten:

date: Date
users: [
  { user: 1, group: 1 }
  { user: 5, group: 2 }
]

date: Date
users: [
  { user: 1, group: 1 }
  { user: 3, group: 2 }
]

Ich möchte nach dieser Sammlung fragen, um alle Dokumente zu finden, in denen sich jede Benutzer-ID in meinem Array von Benutzern in einem anderen Array befindet, [1, 5, 7]. In diesem Beispiel stimmt nur das erste Dokument überein.

Die beste Lösung, die ich finden konnte, ist:

$where: function() { 
  var ids = [1, 5, 7];
  return this.users.every(function(u) { 
    return ids.indexOf(u.user) !== -1;
  });
}

Leider scheint dies die Leistung zu beeinträchtigen. Dies wird in $ where docs angegeben:

$ where wertet JavaScript aus und kann keine Indizes nutzen.

Wie kann ich diese Abfrage verbessern?

21
Wex

Die Abfrage, die Sie möchten, lautet wie folgt:

db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})

Dies bedeutet, dass Sie alle Dokumente finden, die keine Elemente enthalten, die außerhalb der Liste 1,5,7 liegen.

31
Asya Kamsky

Ich weiß es nicht besser, aber es gibt verschiedene Möglichkeiten, dies zu erreichen, und abhängig von der verfügbaren Version von MongoDB.

Wir sind uns nicht sicher, ob dies Ihre Absicht ist oder nicht, aber die angezeigte Abfrage entspricht dem ersten Dokumentbeispiel, da Sie bei der Implementierung Ihrer Logik die Elemente im Array des Dokuments abgleichen, die im Beispielarray enthalten sein müssen.

Wenn Sie also tatsächlich wollten, dass das Dokumentalledieser Elemente enthält, dann ist der $all Operator wäre die naheliegende Wahl:

db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } })

Wenn Sie jedoch davon ausgehen, dass Ihre Logik tatsächlich beabsichtigt ist, können Sie diese Ergebnisse zumindest laut Vorschlag durch Kombination mit $in"filtern". Operator, damit es weniger Dokumente gibt, die Ihrer $where ** Bedingung in ausgewertetem JavaScript unterliegen:

db.collection.find({
    "users.user": { "$in": [ 1, 5, 7 ] },
    "$where": function() { 
        var ids = [1, 5, 7];
        return this.users.every(function(u) { 
            return ids.indexOf(u.user) !== -1;
        });
    }
})

Und Sie erhalten einen Index, obwohl der tatsächlich gescannte Wert mit der Anzahl der Elemente in den Arrays aus den übereinstimmenden Dokumenten multipliziert wird, aber immer noch besser als ohne den zusätzlichen Filter.

Oder vielleicht überlegen Sie sich die logische Abstraktion des $and -Operators in Kombination mit $or und möglicherweise der Operator $size abhängig von Ihren tatsächlichen Array-Bedingungen:

db.collection.find({
    "$or": [
        { "users.user": { "$all": [ 1, 5, 7 ] } },
        { "users.user": { "$all": [ 1, 5 ] } },
        { "users.user": { "$all": [ 1, 7 ] } },
        { "users": { "$size": 1 }, "users.user": 1 },
        { "users": { "$size": 1 }, "users.user": 5 },
        { "users": { "$size": 1 }, "users.user": 7 }
    ]
})

Dies ist also eine Generierung aller möglichen Permutationen Ihrer Übereinstimmungsbedingung, aber auch hier wird die Leistung wahrscheinlich abhängig von Ihrer verfügbaren installierten Version variieren.

HINWEIS:Tatsächlich ist in diesem Fall ein vollständiger Fehler zu verzeichnen, da dies etwas völlig anderes tut und in der Tat zu einem logischen$in


Alternativen sind das Aggregations-Framework, Ihre Laufleistung kann variieren, was am effizientesten ist, aufgrund der Anzahl der Dokumente in Ihrer Sammlung, eines Ansatzes mit MongoDB 2.6 und höher:

db.problem.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Just keeping the "user" element value
    { "$group": {
        "_id": "$_id",
        "users": { "$Push": "$users.user" }
    }},

    // Compare to see if all elements are a member of the desired match
    { "$project": {
        "match": { "$setEquals": [
            { "$setIntersection": [ "$users", [ 1, 5, 7 ] ] },
            "$users"
        ]}
    }},

    // Filter out any documents that did not match
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

Bei diesem Ansatz werden einige neu eingeführte set-Operatoren verwendet, um den Inhalt zu vergleichen. Natürlich müssen Sie das Array umstrukturieren, um den Vergleich durchzuführen.

Wie bereits erwähnt, gibt es dafür einen direkten Operator in $setIsSubset , der das Äquivalent der oben genannten kombinierten Operatoren in einem einzelnen Operator ausführt :

db.collection.aggregate([
    { "$match": { 
        "users.user": { "$in": [ 1,5,7 ] } 
    }},
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},
    { "$unwind": "$users" },
    { "$group": {
        "_id": "$_id",
        "users": { "$Push": "$users.user" }
    }},
    { "$project": {
        "match": { "$setIsSubset": [ "$users", [ 1, 5, 7 ] ] }
    }},
    { "$match": { "match": true } },
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

Oder mit einem anderen Ansatz, während Sie immer noch den $size Operator von MongoDB 2.6 ausnutzen:

db.collection.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    // and a note of it's current size
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
        "size": { "$size": "$users" }
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Filter array contents that do not match
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Count the array elements that did match
    { "$group": {
        "_id": "$_id",
        "size": { "$first": "$size" },
        "count": { "$sum": 1 }
    }},

    // Compare the original size to the matched count
    { "$project": { 
        "match": { "$eq": [ "$size", "$count" ] } 
    }},

    // Filter out documents that were not the same
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

Was natürlich immer noch möglich ist, obwohl es in Versionen vor 2.6 etwas langwieriger ist:

db.collection.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Group it back to get it's original size
    { "$group": { 
        "_id": "$_id",
        "users": { "$Push": "$users" },
        "size": { "$sum": 1 }
    }},

    // Unwind the array copy again
    { "$unwind": "$users" },

    // Filter array contents that do not match
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Count the array elements that did match
    { "$group": {
        "_id": "$_id",
        "size": { "$first": "$size" },
        "count": { "$sum": 1 }
    }},

    // Compare the original size to the matched count
    { "$project": { 
        "match": { "$eq": [ "$size", "$count" ] } 
    }},

    // Filter out documents that were not the same
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

Das rundet im Allgemeinen die verschiedenen Möglichkeiten ab, probieren Sie sie aus und finden Sie heraus, was für Sie am besten funktioniert. Aller Wahrscheinlichkeit nach wird die einfache Kombination von $in mit Ihrem vorhandenen Formular wahrscheinlich die beste sein. Stellen Sie jedoch in jedem Fall sicher, dass Sie einen Index haben, der ausgewählt werden kann:

db.collection.ensureIndex({ "users.user": 1 })

Welches wird Ihnen die beste Leistung geben, solange Sie auf diese Weise zugreifen, wie dies in allen Beispielen hier der Fall ist.


Urteil

Ich war fasziniert davon und habe mir letztendlich einen Testfall ausgedacht, um zu sehen, was die beste Leistung bringt. Also erstmal ein paar Testdaten generieren:

var batch = [];
for ( var n = 1; n <= 10000; n++ ) {
    var elements = Math.floor(Math.random(10)*10)+1;

    var obj = { date: new Date(), users: [] };
    for ( var x = 0; x < elements; x++ ) {
        var user = Math.floor(Math.random(10)*10)+1,
            group = Math.floor(Math.random(10)*10)+1;

        obj.users.Push({ user: user, group: group });
    }

    batch.Push( obj );

    if ( n % 500 == 0 ) {
        db.problem.insert( batch );
        batch = [];
    }

} 

Mit 10000 Dokumenten in einer Sammlung mit zufälligen Arrays mit einer Länge von 1 bis 10 und zufälligen Werten von 1 bis 0 kam ich zu einer Übereinstimmungszahl von 430 Dokumenten (von 7749 ausgehend von$inmatch) mit folgenden Ergebnissen (Durchschnitt):

  • JavaScript mit$inKlausel: 420ms
  • Aggregieren Sie mit$size: 395ms
  • Aggregat mit Gruppenarrayanzahl: 650ms
  • Aggregat mit zwei Mengenoperatoren: 275 ms
  • Aggregieren Sie mit$setIsSubset:250ms

Es ist zu beachten, dass alle Proben mit Ausnahme der letzten beiden eine Varianz von peak von ungefähr 100 ms schneller aufwiesen und die letzten beiden eine Antwort von 220 ms zeigten. Die größten Abweichungen gab es bei der JavaScript-Abfrage, bei der die Ergebnisse ebenfalls 100 ms langsamer waren.

Aber der Punkt hier ist relativ zur Hardware, die auf meinem Laptop unter einem VM ist nicht besonders toll, gibt aber eine Idee.

Daher gewinnt das Aggregat und insbesondere die MongoDB 2.6.1-Version mit festgelegten Operatoren deutlich an Leistung, da$setIsSubsetals einzelner Operator einen zusätzlichen leichten Gewinn bringt .

Dies ist besonders interessant, da (wie durch die 2.4-kompatible Methode angezeigt) die größten Kosten in diesem Prozess die Anweisung$unwind(über 100 ms im Durchschnitt) sind. Bei einer Auswahl von$inmit einem Mittelwert um 32 ms dauert der Rest der Pipeline-Stufen im Durchschnitt weniger als 100 ms. Das gibt also eine relative Vorstellung von der Aggregation im Vergleich zur JavaScript-Leistung.

12
Neil Lunn

Ich habe gerade einen wesentlichen Teil meines Tages damit verbracht, die oben beschriebene Asya-Lösung mit Objektvergleichen anstatt mit strikter Gleichheit zu implementieren. Ich dachte mir, ich würde es hier teilen.

Angenommen, Sie haben Ihre Frage von userIds zu vollständigen Benutzern erweitert .. Sie möchten alle Dokumente finden, in denen sich alle Elemente in ihrem Array users in einem anderen Benutzerarray befinden: [{user: 1, group: 3}, {user: 2, group: 5},...]

Das funktioniert nicht: db.collection.find({"users":{"$not":{"$elemMatch":{"$nin":[{user: 1, group: 3},{user: 2, group: 5},...]}}}}}), da $ nin nur für strikte Gleichheit funktioniert. Daher müssen wir für Arrays von Objekten einen anderen Weg finden, "Not in array" auszudrücken. Die Verwendung von $where würde die Abfrage zu sehr verlangsamen.

Lösung:

db.collection.find({
 "users": {
   "$not": {
     "$elemMatch": {
       // if all of the OR-blocks are true, element is not in array
       "$and": [{
         // each OR-block == true if element != that user
         "$or": [
           "user": { "ne": 1 },
           "group": { "ne": 3 }
         ]
       }, {
         "$or": [
           "user": { "ne": 2 },
           "group": { "ne": 5 }
         ]
       }, {
         // more users...
       }]
     }
   }
 }
})

Um die Logik abzurunden: $ elemMatch gleicht alle Dokumente ab, bei denen sich ein Benutzer nicht im Array befindet. $ Not wird also mit allen Dokumenten übereinstimmen, die alle Benutzer im Array enthalten.

0
Mark Bryk