mongodb: insira se não existir

146

Todos os dias, recebo um estoque de documentos (uma atualização). O que eu quero fazer é inserir cada item que ainda não existe.

  • Também quero acompanhar a primeira vez que os inseri e a última vez que os vi em uma atualização.
  • Não quero documentos duplicados.
  • Não quero remover um documento que foi salvo anteriormente, mas não está na minha atualização.
  • 95% (estimado) dos registros não são modificados diariamente.

Estou usando o driver Python (pymongo).

O que atualmente faço é (pseudo-có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)

Meu problema é que é muito lento (40 minutos para menos de 100.000 registros e tenho milhões deles na atualização). Tenho certeza de que há algo embutido para fazer isso, mas o documento para update () é mmmhhh .... um pouco mais conciso ... ( http://www.mongodb.org/display/DOCS/Updating )

Alguém pode aconselhar como fazê-lo mais rápido?

LeMiz
fonte

Respostas:

153

Parece que você quer fazer um "upsert". O MongoDB possui suporte interno para isso. Passe um parâmetro extra para sua chamada de update (): {upsert: true}. Por exemplo:

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

Isso substitui totalmente o seu bloco if-find-else-update. Ele será inserido se a chave não existir e será atualizado se existir.

Antes:

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

Depois de:

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

Você também pode especificar quais dados você deseja gravar:

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

Agora, o documento selecionado atualizará apenas o valor de "chave2" e deixará todo o resto intocado.

Van Nguyen
fonte
5
Isso é quase o que eu quero! Como não posso tocar no campo inserttion_date se o objeto já está presente?
LeMiz 27/05
24
você pode dar um exemplo de apenas definir um campo na primeira inserção e não atualizá-lo, se existir? @VanNguyen
Ali Shakiba
7
A primeira parte da sua resposta está errada, eu acho. coll.update substituirá os dados, a menos que você use $ set. Portanto, After será na verdade: {'key2': 'value2', 'key3': 'value3'}
James Blackburn
9
-1 Esta resposta é perigosa. Você encontra pelo valor de "chave" e depois apaga "chave", para que posteriormente não seja possível encontrá-lo novamente. Este é um caso de uso muito improvável.
Mark E. Haase
23
Você deve usar o operador $ setOnInsert! Upsert até atualizará o documento se a consulta for encontrada.
precisa saber é o seguinte
64

No MongoDB 2.4, você pode usar $ setOnInsert ( http://docs.mongodb.org/manual/reference/operator/setOnInsert/ )

Defina 'inserttion_date' usando $ setOnInsert e 'last_update_date' usando $ set no seu comando upsert.

Para transformar seu pseudocódigo em um exemplo de trabalho:

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
fonte
3
Isso está correto, você pode verificar se há um documento correspondente a um filtro e inserir algo, se não encontrado, usando $ setOnInsert. Observe, porém, que houve um erro no qual você não podia $ setOnInsert com o campo _id - ele dizia algo como "não é possível modificar o campo _id". Este foi um erro, corrigido na v2.5.4 ou mais. Se você vir essa mensagem ou problema, obtenha a versão mais recente.
Kieren Johnstone
19

Você sempre pode criar um índice exclusivo, o que faz com que o MongoDB rejeite um salvamento conflitante. Considere o seguinte feito usando o 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
fonte
12

Você pode usar Upsert com o operador $ setOnInsert.

db.Table.update({noExist: true}, {"$setOnInsert": {xxxYourDocumentxxx}}, {upsert: true})
YulCheney
fonte
11
Para quem consulta com pymongo, o terceiro parâmetro deve ser true ou upsert = True, e não um dict
S ..
6

1. Use Atualização.

Com base na resposta de Van Nguyen acima, use update em vez de save. Isso lhe dá acesso à opção de upsert.

NOTA : Este método substitui o documento inteiro quando encontrado ( Dos 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

Se você deseja atualizar uma seleção do documento, mas não a coisa toda, pode usar o método $ set com a atualização. (novamente, nos documentos ) ... Então, se você deseja definir ...

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

Enviar como ...

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

Isso ajuda a evitar a substituição acidental de todos os seus documentos { name: 'jason borne' }.

Meshach Jackson
fonte
6

Resumo

  • Você tem uma coleção de registros existente.
  • Você tem um conjunto de registros que contém atualizações para os registros existentes.
  • Algumas das atualizações não atualizam nada, elas duplicam o que você já possui.
  • Todas as atualizações contêm os mesmos campos que já existem, apenas valores possivelmente diferentes.
  • Você deseja rastrear quando um registro foi alterado pela última vez, onde um valor realmente foi alterado.

Note, presumo que o PyMongo mude para se adequar ao seu idioma de escolha.

Instruções:

  1. Crie a coleção com um índice com unique = true para não obter registros duplicados.

  2. Faça uma iteração sobre seus registros de entrada, criando lotes deles com cerca de 15.000 registros. Para cada registro no lote, crie um ditado que consiste nos dados que você deseja inserir, presumindo que cada um será um novo registro. Adicione os carimbos de data / hora 'criados' e 'atualizados' a estes. Emita isso como um comando de inserção em lote com o sinalizador 'ContinueOnError' = true, para que a inserção de todo o resto aconteça mesmo se houver uma chave duplicada (o que parece haver). Isso vai acontecer muito rápido. Inserções em massa de rochas, obtive níveis de desempenho de 15k / segundo. Para obter mais notas sobre o ContinueOnError, consulte http://docs.mongodb.org/manual/core/write-operations/

    As inserções de registro acontecem MUITO rápido, então você será feito com essas inserções em pouco tempo. Agora, é hora de atualizar os registros relevantes. Faça isso com uma recuperação em lote, muito mais rápido que um de cada vez.

  3. Itere novamente todos os seus registros de entrada, criando lotes de 15 mil ou mais. Extraia as chaves (melhor se houver uma chave, mas não puderem ser ajudadas se não houver). Recupere esse monte de registros do Mongo com uma consulta db.collectionNameBlah.find ({field: {$ in: [1, 2,3 ...}). Para cada um desses registros, determine se há uma atualização e, se houver, emita a atualização, incluindo a atualização do carimbo de data / hora 'atualizado'.

    Infelizmente, devemos observar que o MongoDB 2.4 e abaixo NÃO incluem uma operação de atualização em massa. Eles estão trabalhando nisso.

Principais pontos de otimização:

  • As pastilhas irão acelerar muito suas operações em massa.
  • A recuperação de registros em massa também acelerará as coisas.
  • Atualizações individuais são a única rota possível agora, mas a 10Gen está trabalhando nisso. Presumivelmente, isso será na versão 2.6, embora eu não tenha certeza se será concluído até lá, há muitas coisas para fazer (eu tenho seguido o sistema Jira).
Kevin J. Rice
fonte
5

Eu não acho que o mongodb suporta esse tipo de upserting seletivo. Eu tenho o mesmo problema que o LeMiz, e o uso da atualização (critérios, newObj, upsert, multi) não funciona direito ao lidar com um carimbo de data / hora 'criado' e 'atualizado'. Dada a seguinte declaração upsert:

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

Cenário # 1 - o documento com 'nome' de 'abc' não existe: o novo documento é criado com 'name' = 'abc', 'created' = 14-07-2010 11:11:11 e 'updated' = 2010-07-14 11:11:11.

Cenário # 2 - o documento com 'nome' de 'abc' já existe com o seguinte: 'name' = 'abc', 'created' = 2010-07-12 09:09:09 e 'updated' = 2010-07 -13 10:10:10. Após a upsert, o documento agora seria o mesmo que o resultado no cenário 1. Não há como especificar em um upsert quais campos serão definidos ao inserir e quais serão deixados em paz se forem atualizados.

Minha solução foi criar um índice exclusivo nos campos de critério , executar uma inserção e imediatamente depois executar uma atualização apenas no campo 'atualizado'.

Yonsink
fonte
4

Em geral, o uso da atualização é melhor no MongoDB, pois ele apenas criará o documento, se ainda não existir, embora não tenha certeza de como trabalhar isso com o seu adaptador python.

Segundo, se você precisar apenas saber se esse documento existe ou não, count () que retorna apenas um número será uma opção melhor do que o find_one, que supostamente transfere todo o documento do MongoDB, causando tráfego desnecessário.

Thomas R. Koll
fonte