mongodb: insertar si no existe

146

Todos los días, recibo un stock de documentos (una actualización). Lo que quiero hacer es insertar cada elemento que aún no existe.

  • También quiero hacer un seguimiento de la primera vez que los inserté y la última vez que los vi en una actualización.
  • No quiero tener documentos duplicados.
  • No quiero eliminar un documento que se ha guardado anteriormente, pero que no está en mi actualización.
  • El 95% (estimado) de los registros no se modifican día a día.

Estoy usando el controlador Python (pymongo).

Lo que hago actualmente es (pseudocódigo):

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)

Mi problema es que es muy lento (40 minutos para menos de 100 000 registros, y tengo millones de ellos en la actualización). Estoy bastante seguro de que hay algo incorporado para hacer esto, pero el documento para la actualización () es mmmhhh ... un poco conciso ... ( http://www.mongodb.org/display/DOCS/Updating )

¿Alguien puede aconsejar cómo hacerlo más rápido?

LeMiz
fuente

Respuestas:

153

Parece que quieres hacer un "upsert". MongoDB tiene soporte incorporado para esto. Pase un parámetro adicional a su llamada a update (): {upsert: true}. Por ejemplo:

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

Esto reemplaza su bloque if-find-else-update por completo. Se insertará si la clave no existe y se actualizará si es así.

Antes de:

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

Después:

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

También puede especificar qué datos desea escribir:

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

Ahora su documento seleccionado actualizará el valor de "clave2" solamente y dejará todo lo demás intacto.

Van Nguyen
fuente
55
¡Esto es casi lo que quiero! ¿Cómo puedo no tocar el campo insertion_date si el objeto ya está presente?
LeMiz
24
¿puede dar un ejemplo de solo establecer un campo en la primera inserción y no actualizarlo si existe? @VanNguyen
Ali Shakiba
77
La primera parte de su respuesta es incorrecta, creo. coll.update reemplazará los datos a menos que use $ set. Entonces After será: {'key2': 'value2', 'key3': 'value3'}
James Blackburn el
9
-1 Esta respuesta es peligrosa. Lo encuentra por el valor de "clave" y luego borra "clave", de modo que posteriormente no podrá encontrarlo nuevamente. Este es un caso de uso muy poco probable.
Mark E. Haase
23
¡Debe usar el operador $ setOnInsert! Upsert incluso actualizará el documento si encuentra la consulta.
YulCheney
64

A partir de MongoDB 2.4, puede usar $ setOnInsert ( http://docs.mongodb.org/manual/reference/operator/setOnInsert/ )

Establezca 'insertion_date' usando $ setOnInsert y 'last_update_date' usando $ set en su comando upsert.

Para convertir su pseudocódigo en un ejemplo de trabajo:

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
fuente
3
Esto es correcto, puede verificar si hay un documento que coincida con un filtro e insertar algo si no se encuentra, usando $ setOnInsert. Sin embargo, tenga en cuenta que hubo un error en el que no se pudo $ setOnInsert con el campo _id: diría algo como "no se puede modificar el campo _id". Este fue un error, corregido en v2.5.4 o sus alrededores. Si ve este mensaje o problema, solo obtenga la última versión.
Kieren Johnstone
19

Siempre puede crear un índice único, lo que hace que MongoDB rechace un guardado conflictivo. Considere lo siguiente hecho usando el 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
fuente
12

Puede usar Upsert con el operador $ setOnInsert.

db.Table.update({noExist: true}, {"$setOnInsert": {xxxYourDocumentxxx}}, {upsert: true})
YulCheney
fuente
11
Para cualquiera que consulte con pymongo, el tercer parámetro debería ser verdadero o upsert = True, y no un dict
S ..
6

1. Use Actualizar.

Basándose en la respuesta de Van Nguyen anterior, use la actualización en lugar de guardar. Esto le da acceso a la opción upsert.

NOTA : Este método anula todo el documento cuando se encuentra ( desde los documentos )

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. Use $ set

Si desea actualizar una selección del documento, pero no todo, puede usar el método $ set con la actualización. (de nuevo, desde los documentos ) ... Entonces, si quieres configurar ...

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

Envíalo como ...

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

Esto ayuda a evitar sobrescribir accidentalmente todos sus documentos { name: 'jason borne' }.

Meshach Jackson
fuente
6

Resumen

  • Tiene una colección de registros existente.
  • Tiene un conjunto de registros que contienen actualizaciones de los registros existentes.
  • Algunas de las actualizaciones realmente no actualizan nada, duplican lo que ya tienes.
  • Todas las actualizaciones contienen los mismos campos que ya existen, posiblemente solo valores diferentes.
  • Desea realizar un seguimiento de cuándo se modificó por última vez un registro, donde realmente cambió un valor.

Tenga en cuenta que supongo que PyMongo cambia para adaptarse al idioma que elija.

Instrucciones:

  1. Cree la colección con un índice con unique = true para que no obtenga registros duplicados.

  2. Itere sobre sus registros de entrada, creando lotes de ellos de 15,000 registros más o menos. Para cada registro en el lote, cree un archivo compuesto por los datos que desea insertar, suponiendo que cada uno será un nuevo registro. Agregue las marcas de tiempo 'creado' y 'actualizado' a estos. Publique esto como un comando de inserción por lotes con el indicador 'ContinueOnError' = verdadero, por lo que la inserción de todo lo demás ocurre incluso si hay una clave duplicada allí (lo que parece que habrá). ESTO OCURRIRÁ MUY RÁPIDO. Las inserciones masivas de rock, obtuve niveles de rendimiento de 15k / segundo. Más notas sobre ContinueOnError, consulte http://docs.mongodb.org/manual/core/write-operations/

    Las inserciones de grabación suceden MUY rápido, por lo que terminará con esas inserciones en muy poco tiempo. Ahora es el momento de actualizar los registros relevantes. Haga esto con una recuperación por lotes, mucho más rápido que uno a la vez.

  3. Itere sobre todos sus registros de entrada nuevamente, creando lotes de 15K más o menos. Extraiga las claves (mejor si hay una clave, pero no se puede evitar si no la hay). Recupere este grupo de registros de Mongo con una consulta db.collectionNameBlah.find ({field: {$ in: [1, 2,3 ...}). Para cada uno de estos registros, determine si hay una actualización y, de ser así, emita la actualización, incluida la actualización de la marca de tiempo 'actualizada'.

    Desafortunadamente, debemos tener en cuenta que MongoDB 2.4 y posteriores NO incluyen una operación de actualización masiva. Están trabajando en eso.

Puntos clave de optimización:

  • Los insertos acelerarán enormemente sus operaciones a granel.
  • Recuperar registros en masa también acelerará las cosas.
  • Las actualizaciones individuales son la única ruta posible ahora, pero 10Gen está trabajando en ello. Presumiblemente, esto será en 2.6, aunque no estoy seguro de si estará terminado para entonces, hay muchas cosas que hacer (he estado siguiendo su sistema Jira).
Kevin J. Rice
fuente
5

No creo que mongodb admita este tipo de inserción selectiva. Tengo el mismo problema que LeMiz, y el uso de la actualización (criterios, newObj, upsert, multi) no funciona bien cuando se trata de una marca de tiempo 'creada' y 'actualizada'. Dada la siguiente declaración upsert:

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

Escenario n. ° 1: el documento con 'nombre' de 'abc' no existe: el nuevo documento se crea con 'nombre' = 'abc', 'creado' = 14/07/2010 11:11:11 y 'actualizado' = 2010-07-14 11:11:11.

Escenario # 2: el documento con 'nombre' de 'abc' ya existe con lo siguiente: 'nombre' = 'abc', 'creado' = 2010-07-12 09:09:09 y 'actualizado' = 2010-07 -13 10:10:10. Después del upsert, el documento ahora sería el mismo que el resultado en el escenario # 1. No hay forma de especificar en un upsert qué campos se establecerán si se insertan, y qué campos se dejarán solos si se actualizan.

Mi solución fue crear un índice único en los campos de criterios , realizar una inserción e inmediatamente después realizar una actualización solo en el campo 'actualizado'.

Yonsink
fuente
4

En general, usar la actualización es mejor en MongoDB, ya que solo creará el documento si aún no existe, aunque no estoy seguro de cómo hacerlo con su adaptador de Python.

En segundo lugar, si solo necesita saber si ese documento existe o no, count () que devuelve solo un número será una mejor opción que find_one, que supuestamente transfiere todo el documento desde su MongoDB causando tráfico innecesario.

Thomas R. Koll
fuente