mongodb: insérer s'il n'existe pas

146

Chaque jour, je reçois un stock de documents (une mise à jour). Ce que je veux faire, c'est insérer chaque élément qui n'existe pas déjà.

  • Je veux également garder une trace de la première fois que je les ai insérés et de la dernière fois que je les ai vus dans une mise à jour.
  • Je ne veux pas avoir de documents en double.
  • Je ne souhaite pas supprimer un document qui a déjà été enregistré, mais qui ne figure pas dans ma mise à jour.
  • 95% (estimé) des enregistrements ne sont pas modifiés au jour le jour.

J'utilise le pilote Python (pymongo).

Ce que je fais actuellement est (pseudo-code):

for each document in update:
      existing_document = collection.find_one(document)
      if not existing_document:
           document['insertion_date'] = now
      else:
           document = existing_document
      document['last_update_date'] = now
      my_collection.save(document)

Mon problème est qu'il est très lent (40 minutes pour moins de 100 000 enregistrements, et j'en ai des millions dans la mise à jour). Je suis sûr qu'il y a quelque chose de intégré pour faire cela, mais le document pour update () est mmmhhh .... un peu laconique .... ( http://www.mongodb.org/display/DOCS/Updating )

Quelqu'un peut-il conseiller comment le faire plus rapidement?

LeMiz
la source

Réponses:

153

On dirait que vous voulez faire un "upsert". MongoDB a un support intégré pour cela. Passez un paramètre supplémentaire à votre appel update (): {upsert: true}. Par exemple:

key = {'key':'value'}
data = {'key2':'value2', 'key3':'value3'};
coll.update(key, data, upsert=True); #In python upsert must be passed as a keyword argument

Cela remplace entièrement votre bloc if-find-else-update. Il s'insérera si la clé n'existe pas et se mettra à jour si c'est le cas.

Avant:

{"key":"value", "key2":"Ohai."}

Après:

{"key":"value", "key2":"value2", "key3":"value3"}

Vous pouvez également spécifier les données que vous souhaitez écrire:

data = {"$set":{"key2":"value2"}}

Maintenant, votre document sélectionné mettra à jour la valeur de "key2" uniquement et laissera tout le reste intact.

Van Nguyen
la source
5
C'est presque ce que je veux! Comment ne pas toucher le champ date_insertion si l'objet est déjà présent?
LeMiz
24
pouvez-vous s'il vous plaît donner un exemple de simplement définir un champ lors de la première insertion et ne pas le mettre à jour s'il existe? @VanNguyen
Ali Shakiba
7
La première partie de votre réponse est fausse, je pense. coll.update remplacera les données sauf si vous utilisez $ set. Donc After sera en fait: {'key2': 'value2', 'key3': 'value3'}
James Blackburn
9
-1 Cette réponse est dangereuse. Vous trouvez par la valeur de "clé" et ensuite vous effacez "clé", de sorte que par la suite vous ne pourrez plus la retrouver. C'est un cas d'utilisation très improbable.
Mark E. Haase
23
Vous devez utiliser l'opérateur $ setOnInsert! Upsert mettra même à jour le document s'il trouve la requête.
YulCheney le
64

Depuis MongoDB 2.4, vous pouvez utiliser $ setOnInsert ( http://docs.mongodb.org/manual/reference/operator/setOnInsert/ )

Définissez 'insertion_date' en utilisant $ setOnInsert et 'last_update_date' en utilisant $ set dans votre commande upsert.

Pour transformer votre pseudocode en un exemple fonctionnel:

now = datetime.utcnow()
for document in update:
    collection.update_one(
        {"_id": document["_id"]},
        {
            "$setOnInsert": {"insertion_date": now},
            "$set": {"last_update_date": now},
        },
        upsert=True,
    )
Andy
la source
3
C'est correct, vous pouvez rechercher un document correspondant à un filtre et insérer quelque chose s'il n'est pas trouvé, en utilisant $ setOnInsert. Notez cependant qu'il y avait un bogue où vous ne pouviez pas $ setOnInsert avec le champ _id - cela disait quelque chose comme "ne peut pas modifier le champ _id". C'était un bogue, corrigé dans la v2.5.4 ou à peu près. Si vous voyez ce message ou ce problème, téléchargez simplement la dernière version.
Kieren Johnstone
19

Vous pouvez toujours créer un index unique, ce qui oblige MongoDB à rejeter une sauvegarde en conflit. Considérez ce qui suit en utilisant le shell mongodb:

> db.getCollection("test").insert ({a:1, b:2, c:3})
> db.getCollection("test").find()
{ "_id" : ObjectId("50c8e35adde18a44f284e7ac"), "a" : 1, "b" : 2, "c" : 3 }
> db.getCollection("test").ensureIndex ({"a" : 1}, {unique: true})
> db.getCollection("test").insert({a:2, b:12, c:13})      # This works
> db.getCollection("test").insert({a:1, b:12, c:13})      # This fails
E11000 duplicate key error index: foo.test.$a_1  dup key: { : 1.0 }
Ram Rajamony
la source
12

Vous pouvez utiliser Upsert avec l'opérateur $ setOnInsert.

db.Table.update({noExist: true}, {"$setOnInsert": {xxxYourDocumentxxx}}, {upsert: true})
YulCheney
la source
11
Pour quiconque interroge avec pymongo, le troisième paramètre doit être true ou upsert = True, et non un dict
S ..
6

1. Utilisez Update.

En vous inspirant de la réponse de Van Nguyen ci-dessus, utilisez update au lieu de save. Cela vous donne accès à l'option upsert.

REMARQUE : cette méthode remplace le document entier lorsqu'il est trouvé (à partir de la documentation )

var conditions = { name: 'borne' }   , update = { $inc: { visits: 1 }} , options = { multi: true };

Model.update(conditions, update, options, callback);

function callback (err, numAffected) {   // numAffected is the number of updated documents })

1.a. Utiliser $ set

Si vous souhaitez mettre à jour une sélection du document, mais pas le tout, vous pouvez utiliser la méthode $ set avec update. (encore une fois, à partir de la documentation ) ... Donc, si vous voulez définir ...

var query = { name: 'borne' };  Model.update(query, ***{ name: 'jason borne' }***, options, callback)

Envoyez-le comme ...

Model.update(query, ***{ $set: { name: 'jason borne' }}***, options, callback)

Cela permet d'éviter d'écraser accidentellement tous vos documents avec { name: 'jason borne' }.

Meshach Jackson
la source
6

Résumé

  • Vous disposez d'une collection d'enregistrements existante.
  • Vous disposez d'un ensemble d'enregistrements contenant des mises à jour des enregistrements existants.
  • Certaines mises à jour ne mettent vraiment à jour rien, elles dupliquent ce que vous avez déjà.
  • Toutes les mises à jour contiennent les mêmes champs qui sont déjà là, juste peut-être des valeurs différentes.
  • Vous voulez savoir quand un enregistrement a été modifié pour la dernière fois, où une valeur a réellement changé.

Remarque, je suppose que PyMongo, changez en fonction de la langue de votre choix.

Instructions:

  1. Créez la collection avec un index avec unique = true afin de ne pas obtenir d'enregistrements en double.

  2. Parcourez vos enregistrements d'entrée, en créant des lots de 15 000 enregistrements environ. Pour chaque enregistrement du lot, créez un dict composé des données que vous souhaitez insérer, en supposant que chacun sera un nouvel enregistrement. Ajoutez-y les horodatages «créés» et «mis à jour». Émettez-le comme une commande d'insertion par lots avec le drapeau 'ContinueOnError' = true, de sorte que l'insertion de tout le reste se produit même s'il y a une clé en double (ce qui semble être le cas). CELA SE PASSE TRES RAPIDEMENT. Bulk inserts rock, j'ai obtenu des performances de 15k / seconde. Notes complémentaires sur ContinueOnError, voir http://docs.mongodb.org/manual/core/write-operations/

    Les insertions d'enregistrement se produisent TRÈS rapidement, vous en aurez donc fini avec ces insertions en un rien de temps. Il est maintenant temps de mettre à jour les enregistrements pertinents. Faites-le avec une récupération par lots, beaucoup plus rapidement qu'une à la fois.

  3. Répétez tous vos enregistrements d'entrée en créant des lots d'environ 15 Ko. Extraire les clés (mieux s'il y a une clé, mais ne peut pas être aidé s'il n'y en a pas). Récupérez ce groupe d'enregistrements de Mongo avec une requête db.collectionNameBlah.find ({field: {$ in: [1, 2,3 ...}). Pour chacun de ces enregistrements, déterminez s'il existe une mise à jour et, le cas échéant, exécutez la mise à jour, y compris la mise à jour de l'horodatage «mis à jour».

    Malheureusement, nous devons noter que MongoDB 2.4 et les versions antérieures n'incluent PAS une opération de mise à jour en masse. Ils y travaillent.

Points d'optimisation clés:

  • Les inserts accéléreront considérablement vos opérations en vrac.
  • Récupérer des enregistrements en masse accélérera également les choses.
  • Les mises à jour individuelles sont la seule voie possible actuellement, mais 10Gen y travaille. Vraisemblablement, ce sera en 2.6, même si je ne suis pas sûr que ce soit fini d'ici là, il y a beaucoup de choses à faire (j'ai suivi leur système Jira).
Kevin J. Rice
la source
5

Je ne pense pas que mongodb supporte ce type de soulèvement sélectif. J'ai le même problème que LeMiz, et l'utilisation de la mise à jour (critères, newObj, upsert, multi) ne fonctionne pas correctement lorsqu'il s'agit à la fois d'un horodatage «créé» et «mis à jour». Compte tenu de la déclaration upsert suivante:

update( { "name": "abc" }, 
        { $set: { "created": "2010-07-14 11:11:11", 
                  "updated": "2010-07-14 11:11:11" }},
        true, true ) 

Scénario n ° 1 - le document avec 'nom' ou 'abc' n'existe pas: un nouveau document est créé avec 'name' = 'abc', 'created' = 2010-07-14 11:11:11 et 'updated' = 2010-07-14 11:11:11.

Scénario n ° 2 - le document avec 'nom' de 'abc' existe déjà avec les éléments suivants: 'name' = 'abc', 'created' = 2010-07-12 09:09:09, et 'updated' = 2010-07 -13 10:10:10. Après l'upsert, le document serait désormais le même que le résultat du scénario n ° 1. Il n'y a aucun moyen de spécifier dans un upsert quels champs seront définis en cas d'insertion, et quels champs rester seuls en cas de mise à jour.

Ma solution était de créer un index unique sur les champs de critères , d'effectuer une insertion, et immédiatement après effectuer une mise à jour juste sur le champ «mis à jour».

Yonsink
la source
4

En général, l'utilisation de la mise à jour est meilleure dans MongoDB car elle créera simplement le document s'il n'existe pas encore, bien que je ne sache pas comment travailler avec votre adaptateur python.

Deuxièmement, si vous avez seulement besoin de savoir si ce document existe ou non, count () qui ne retourne qu'un nombre sera une meilleure option que find_one qui est censé transférer le document entier de votre MongoDB provoquant un trafic inutile.

Thomas R. Koll
la source