MongoDB:如果不存在则插入

194

每天我会收到一批文档(一个更新)。我的目标是插入那些不存在的项目。

  • 我还想追踪第一次插入它们的时间以及它们最后一次出现在更新中的时间。
  • 我不想有重复的文档。
  • 我不想删除已经保存过但不在我的更新中的文档。
  • 大约95%的记录从一天到另一天都没有改变(估计值)。

我正在使用 Python 驱动程序 (pymongo)。

我目前的做法是(伪代码):

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)
我的问题是更新速度非常慢(不到 100,000 条记录需要 40 分钟,而我的记录数有百万级别)。我相信 MongoDB 内置了一些方法可以更快地完成更新,但是 update() 的文档有点简略了。(http://www.mongodb.org/display/DOCS/Updating ) 请问有谁能提供更快的方法吗?
10个回答

190

看起来你想进行一次"upsert"操作。MongoDB内置了对此的支持。在调用update()时传入一个额外的参数:{upsert:true}。例如:

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

这将完全替换您的if-find-else-update块。如果该键不存在,它将插入新值,如果该键存在,则会更新其对应的值。

之前:

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

之后:

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

你还可以指定要写入的数据:

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

现在,您选择的文档将仅更新key2的值,而不影响其他内容。


11
这几乎是我想要的!如果对象已经存在,我如何不触碰 insertion_date 字段? - LeMiz
35
能否举一个只在第一次插入时设置字段并且如果该字段已存在则不更新它的例子?@VanNguyen - Ali Shakiba
8
我认为,你回答的第一部分是错误的。我认为coll.update将会替换数据,除非你使用$set。因此,最终结果实际上应该是: {'key2':'value2', 'key3':'value3'} - James Blackburn
13
这个答案有危险性。它通过“key”的值进行查找,然后删除“key”,这样以后就无法再次找到它了。这是一个非常不太可能的使用情况。 - Mark E. Haase
32
你应该使用 $setOnInsert 操作符! 如果找到查询条件,upsert 甚至会更新文档。 - YulCheney
显示剩余8条评论

103

从MongoDB 2.4开始,您可以使用$setOnInsert (http://docs.mongodb.org/manual/reference/operator/setOnInsert/)。

在您的upsert命令中,使用$setOnInsert设置insertion_date并使用$set设置last_update_date

要将伪代码转换为工作示例:

now = datetime.utcnow()
for document in update:
    collection.update_one(
        filter={
            '_id': document['_id'],
        },
        update={
            '$setOnInsert': {
                'insertion_date': now,
            },
            '$set': {
                'last_update_date': now,
            },
        },
        upsert=True,
    )

6
这是正确的,您可以使用 $setOnInsert 检查与过滤器匹配的文档,并在找不到时插入内容。请注意,存在一个错误,无法使用 _id 字段进行 $setOnInsert - 它会显示类似于“无法对 _id 字段进行修改”的消息。这是一个已修复的错误,在版本 2.5.4 或附近的版本中已经解决。如果您看到此消息或问题,请获取最新版本。 - Kieren Johnstone

27

您可以始终创建一个唯一索引,这会导致MongoDB拒绝保存冲突的数据。考虑使用mongodb shell执行以下操作:

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

现在可以使用createIndex方法。 - Timo
{"a" : 1}, {unique: true} 的意思是字段 a 的内容必须是唯一的,没有第二个条目可以具有相同的内容吗? - Timo

16

您可以使用具有$setOnInsert运算符的Upsert。

db.Table.update({noExist: true}, {"$setOnInsert": {xxxYourDocumentxxx}}, {upsert: true})

http://docs.mongodb.org/manual/reference/operator/update/setOnInsert/#up._S_setOnInsert - The Demz
13
对于任何使用pymongo的人,第三个参数应该只是true或upsert=True,而不是一个字典。 - S..

7

概述

  • 您有一个现有的记录集合。
  • 您有一组记录,其中包含对现有记录的更新。
  • 其中一些更新实际上并没有更新任何内容,它们重复了您已经拥有的内容。
  • 所有更新都包含已经存在的相同字段,只是可能不同的值。
  • 您想要跟踪记录最后更改的时间和值何时更改。

请注意,我假设使用PyMongo,请根据您选择的语言进行更改。

说明:

  1. 创建带有unique=true索引的集合,以避免重复记录。

  2. 迭代您的输入记录,创建大约15,000条记录的批次。对于批次中的每个记录,创建一个包含要插入的数据的字典,假设每个都将成为新记录。添加“created”和“updated”时间戳。使用“ContinueOnError”标志=true作为批量插入命令,因此即使其中有重复键(听起来会有),其他所有内容的插入也会发生。这将非常快速。批量插入非常好用,我已经达到了15k/秒的性能水平。有关ContinueOnError的更多信息,请参见http://docs.mongodb.org/manual/core/write-operations/

    记录插入非常快,因此您很快就会完成这些插入。现在,是时候更新相关记录了。使用批量检索来完成,比逐个检索要快得多。

  3. 再次迭代所有输入记录,创建大约15K的批次。提取键(最好只有一个键,但如果没有,则不能帮助)。使用db.collectionNameBlah.find({ field : { $in : [ 1, 2,3 ...})查询从Mongo中检索这一批记录。对于这些记录中的每一个,确定是否有更新,如果有,则发出更新,包括更新“updated”时间戳。

    不幸的是,我们应该注意,MongoDB 2.4及以下版本不包括批量更新操作。他们正在解决这个问题。

关键优化点:

  • 批量插入将大大加快您的操作速度。
  • 批量检索记录也会加快速度。
  • 目前只能进行单个更新,但10Gen正在解决这个问题。据推测,这将在2.6版中实现,尽管我不确定它是否会在那时完成,因为还有很多事情要做(我一直在关注他们的Jira系统)。

6

1. 使用Update。

参考Van Nguyen的答案,使用update而不是save。这使您可以访问upsert选项。

注意:当找到时,此方法将覆盖整个文档(来自文档

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. 使用$set

如果你想要更新文档的一部分而不是整个文档,可以使用update方法中的$set。 (参考文档)... 所以,如果你想要设置...

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

发送方式...

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

这可以防止意外地用 { name: 'jason borne' } 覆盖所有文档。


6

我不认为MongoDB支持这种选择性upserting。我和 LeMiz 有同样的问题,使用 update(criteria, newObj, upsert, multi) 在处理“created”和“updated”时间戳时无法正常工作。给定以下upsert语句:

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

方案1 - 名为“abc”的文档不存在: 创建一个新文档,其中 'name' = 'abc','created' = 2010-07-14 11:11:11,'updated' = 2010-07-14 11:11:11。

方案2 - 名为“abc”的文档已经存在,并具有以下内容: 'name' = 'abc','created' = 2010-07-12 09:09:09,'updated' = 2010-07-13 10:10:10。 执行upsert操作后,文档将与方案1中的结果相同。在upsert中无法指定哪些字段应该在插入时设置,哪些字段应该在更新时保留不变。

我的解决方案是在条件字段上创建唯一索引,执行插入操作,然后立即仅对“updated”字段执行更新操作。


4

使用Pymongo的方法

Python官方MongoDB驱动程序

在5% 的情况下,您可能希望更新和覆盖现有记录,而其他情况下则需要插入新行,这可以通过 updateOneupsert 来完成。

  • 据估计,有95% 的记录从一天到另一天都没有发生变化。

以下解决方案源自于这个核心mongoDB函数:

db.collection.updateOne(filter, update, options)

基于筛选条件,在集合中更新单个文档。

使用Pymongo的update_one(filter, new_values, upsert=True)函数实现此操作。

代码示例:

# importing pymongo's MongoClient
from pymongo import MongoClient
 
conn = MongoClient('localhost', 27017)
db = conn.databaseName
 
# Filter by appliances called laptops
filter = { 'user_id': '4142480', 'question_id': '2801008' }
 
# Update number of laptops to
new_values = { "$set": { 'votes': 1400 } }
 
# Using update_one() method for single update with upsert.
db.collectionName.update_one(filter, new_values, upsert=True)

upsert=True是什么?

  • 如果没有符合筛选条件的文档,则创建一个新的文档。
  • 更新符合筛选条件的单个文档。

4

通常情况下,在MongoDB中使用update更好,因为它只会在文档不存在时创建文档,但我不确定如何在您的python适配器中使用它。

其次,如果您只需要知道该文档是否存在,则count()将返回一个数字,比find_one更好,后者会从MongoDB传输整个文档,导致不必要的流量。


0

我建议现在使用await。


你对评论特权的了解已经到了能够用言语表达的地步,但你并没有这个特权。你知道规则 https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead 。在这种情况下,请不要决定滥用其他机制(回答)来做一些它本来不应该做的事情,而且你还没有被允许这样做。 - Yunnosch
这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - DaveL17

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接