web-dev-qa-db-fra.com

Vérifier si chaque élément du tableau correspond à la condition

J'ai une collection de documents:

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

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

J'aimerais interroger cette collection pour trouver tous les documents où chaque identifiant d'utilisateur de mon tableau d'utilisateurs se trouve dans un autre tableau, [1, 5, 7]. Dans cet exemple, seul le premier document correspond.

La meilleure solution que j'ai pu trouver est de faire:

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

Malheureusement, cela semble nuire aux performances, indique le $ where docs:

$ où évalue JavaScript et ne peut pas tirer parti des index. 

Comment puis-je améliorer cette requête?

21
Wex

La requête que vous voulez est la suivante:

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

Cela dit me trouver tous les documents qui n'ont pas d'éléments qui sont en dehors de la liste 1,5,7.

31
Asya Kamsky

Je ne sais pas si c'est mieux, mais il y a différentes façons de l'aborder, et selon la version de MongoDB disponible.

Vous n'êtes pas certain de savoir si c'est votre intention ou non, mais la requête, telle qu'elle est illustrée, correspondra au premier exemple de document car, à mesure que votre logique est implémentée, vous mettez en correspondance les éléments du tableau de ce document qui doivent être contenus dans le tableau exemple.

Donc, si vous vouliez réellement que le document contienne tous de ces éléments, alors l'opérateur $all serait le choix évident:

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

Mais si vous présumez que votre logique est bien destinée, vous pouvez au moins "filtrer" ces résultats en les associant à l'opérateur $in afin de réduire le nombre de documents soumis à votre $where ** condition en JavaScript évalué:

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;
        });
    }
})

Et vous obtenez un index bien que le nombre réel analysé soit multiplié par le nombre d'éléments dans les tableaux des documents correspondants, mais reste meilleur que sans le filtre supplémentaire.

Ou même éventuellement vous considérez l'abstraction logique de l'opérateur $and utilisé en combinaison avec $or et éventuellement de l'opérateur $size en fonction de vos données réelles conditions de la matrice:

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

Il s'agit donc d'une génération de toutes les permutations possibles de votre condition correspondante, mais là encore, les performances varieront probablement en fonction de la version installée disponible.

NOTE: En fait, un échec complet dans ce cas, car cela fait quelque chose de complètement différent et aboutit en fait à un $in logique.


Les alternatives sont avec le cadre d'agrégation, votre kilométrage peut varier, ce qui est le plus efficace en raison du nombre de documents de votre collection, une approche avec MongoDB 2.6 ou ultérieure:

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

Donc, cette approche utilise des opérateurs set nouvellement introduits afin de comparer le contenu, bien que vous deviez bien sûr restructurer le tableau afin de faire la comparaison.

Comme indiqué, il y a un opérateur direct pour faire cela dans $setIsSubset qui fait l'équivalent des opérateurs combinés ci-dessus dans un seul opérateur:

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

Ou avec une approche différente tout en tirant parti de l'opérateur $size de MongoDB 2.6:

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

Ce qui bien sûr peut encore être fait, bien qu'un peu plus long dans les versions antérieures à 2.6:

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

Cela complète généralement les différentes manières, essayez-les et voyez ce qui vous convient le mieux. Selon toute vraisemblance, la combinaison simple de $in avec votre formulaire existant sera probablement la meilleure. Mais dans tous les cas, assurez-vous de disposer d’un index pouvant être sélectionné:

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

Ce qui vous donnera les meilleures performances tant que vous y accédez, comme tous les exemples ici.


Verdict

Cela m'a intrigué et j'ai donc finalement inventé un scénario de test afin de voir ce qui avait la meilleure performance. Alors tout d'abord quelques données de test de génération:

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 = [];
    }

} 

Avec 10000 documents dans une collection avec des tableaux aléatoires de 1..10 de long avec des valeurs aléatoires de 1..0, je suis arrivé à un nombre de correspondances de 430 documents (réduit de 7749 à partir de la correspondance $in) avec le texte suivant: résultats (avg):

  • JavaScript avec la clause $in: 420ms
  • Agrégation avec $size: 395ms
  • Agrégat avec le nombre de tableaux de groupe: 650ms
  • Agrégat avec deux opérateurs de jeu: 275ms
  • Agréger avec $setIsSubset: 250ms

Notant que sur les échantillons effectués, tous les modèles sauf les deux derniers présentaient une variance de pic environ 100 ms plus rapidement, et que les deux derniers présentaient une réponse de 220 ms. Les variations les plus importantes se trouvaient dans la requête JavaScript, qui affichait également des résultats 100 ms plus lentement.

Mais le point ici concerne le matériel, qui sur mon ordinateur portable sous un VM n’est pas particulièrement intéressant, mais donne une idée.

Ainsi, la version agrégée, et plus particulièrement la version MongoDB 2.6.1 avec les opérateurs définis, gagne clairement sur les performances avec le léger gain supplémentaire provenant de $setIsSubset en tant qu'opérateur unique.

Ceci est particulièrement intéressant étant donné (comme indiqué par la méthode compatible 2.4) que le coût le plus élevé dans ce processus sera l'instruction $unwind (plus de 100 ms en moyenne), donc avec la sélection $in ayant une moyenne autour de 32 ms les étapes restantes du pipeline s’exécutent en moins de 100 ms en moyenne. Cela donne donc une idée relative de l’agrégation par rapport aux performances JavaScript.

11
Neil Lunn

Je viens de passer une bonne partie de ma journée à essayer de mettre en œuvre la solution d'Asya ci-dessus avec des comparaisons d'objet plutôt qu'une stricte égalité. Alors je me suis dit que je le partagerais ici.

Supposons que vous ayez étendu votre question de userIds à des utilisateurs complets . Vous souhaitez rechercher tous les documents contenant chaque élément de son tableau users dans un autre tableau d'utilisateurs: [{user: 1, group: 3}, {user: 2, group: 5},...]

Cela ne fonctionnera pas: db.collection.find({"users":{"$not":{"$elemMatch":{"$nin":[{user: 1, group: 3},{user: 2, group: 5},...]}}}}}) car $ nin ne fonctionne que pour une égalité stricte. Nous devons donc trouver une manière différente d’exprimer «Pas dans un tableau» pour les tableaux d’objets. Et utiliser $where ralentirait trop la requête.

Solution:

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

Pour compléter la logique: $ elemMatch correspond à tous les documents dont l'utilisateur ne fait pas partie du tableau. Donc, $ not correspondra à tous les documents contenant tous les utilisateurs du tableau.

0
Mark Bryk