mongodb: einfügen, falls nicht vorhanden


146

Jeden Tag erhalte ich einen Dokumentenbestand (ein Update). Ich möchte jedes Element einfügen, das noch nicht vorhanden ist.

  • Ich möchte auch verfolgen, wann ich sie zum ersten Mal eingefügt habe und wann ich sie das letzte Mal in einem Update gesehen habe.
  • Ich möchte keine doppelten Dokumente haben.
  • Ich möchte kein Dokument entfernen, das zuvor gespeichert wurde, aber nicht in meinem Update enthalten ist.
  • 95% (geschätzt) der Aufzeichnungen sind von Tag zu Tag unverändert.

Ich benutze den Python-Treiber (Pymongo).

Was ich derzeit mache, ist (Pseudocode):

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)

Mein Problem ist, dass es sehr langsam ist (40 Minuten für weniger als 100 000 Datensätze, und ich habe Millionen davon im Update). Ich bin mir ziemlich sicher, dass dafür etwas eingebaut ist, aber das Dokument für update () ist mmmhhh .... ein bisschen knapp .... ( http://www.mongodb.org/display/DOCS/Updating )

Kann jemand raten, wie es schneller geht?

Antworten:


153

Klingt so, als ob Sie einen "Upsert" machen möchten. MongoDB hat hierfür eine integrierte Unterstützung. Übergeben Sie einen zusätzlichen Parameter an Ihren update () -Aufruf: {upsert: true}. Beispielsweise:

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

Dies ersetzt Ihren if-find-else-Update-Block vollständig. Es wird eingefügt, wenn der Schlüssel nicht vorhanden ist, und aktualisiert, wenn dies der Fall ist.

Vor:

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

Nach dem:

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

Sie können auch angeben, welche Daten Sie schreiben möchten:

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

Jetzt aktualisiert Ihr ausgewähltes Dokument nur den Wert von "key2" und lässt alles andere unberührt.


5
Das ist fast was ich will! Wie kann ich das Feld insertion_date nicht berühren, wenn das Objekt bereits vorhanden ist?
LeMiz

24
Können Sie bitte ein Beispiel geben, wie Sie beim ersten Einfügen ein Feld festlegen und es nicht aktualisieren, falls vorhanden? @ VanNguyen
Ali Shakiba

7
Der erste Teil Ihrer Antwort ist falsch, denke ich. coll.update ersetzt Daten, sofern Sie nicht $ set verwenden. Also wird After tatsächlich sein: {'key2': 'value2', 'key3': 'value3'}
James Blackburn

9
-1 Diese Antwort ist gefährlich. Sie finden durch den Wert von "Schlüssel" und löschen dann "Schlüssel", so dass Sie ihn anschließend nicht mehr finden können. Dies ist ein sehr unwahrscheinlicher Anwendungsfall.
Mark E. Haase

23
Sie sollten den Operator $ setOnInsert verwenden! Upsert aktualisiert sogar das Dokument, wenn die Abfrage gefunden wird.
YulCheney

63

Ab MongoDB 2.4 können Sie $ setOnInsert ( http://docs.mongodb.org/manual/reference/operator/setOnInsert/ ) verwenden.

Setzen Sie 'insertion_date' mit $ setOnInsert und 'last_update_date' mit $ set in Ihrem Upsert-Befehl.

So verwandeln Sie Ihren Pseudocode in ein funktionierendes Beispiel:

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

3
Wenn dies korrekt ist, können Sie mithilfe von $ setOnInsert nach einem Dokument suchen, das mit einem Filter übereinstimmt, und etwas einfügen, wenn es nicht gefunden wird. Beachten Sie jedoch, dass es einen Fehler gab, bei dem Sie $ setOnInsert nicht mit dem Feld _id setzen konnten - es würde so etwas wie "Das Feld _id kann nicht geändert werden" sagen. Dies war ein Fehler, der in Version 2.5.4 oder so ungefähr behoben wurde. Wenn Sie diese Meldung oder dieses Problem sehen, holen Sie sich einfach die neueste Version.
Kieren Johnstone

19

Sie können jederzeit einen eindeutigen Index erstellen, wodurch MongoDB eine widersprüchliche Speicherung ablehnt. Betrachten Sie Folgendes mit der 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 }


6

1. Verwenden Sie Update.

Verwenden Sie Update aus Van Nguyens Antwort oben, anstatt zu speichern. Dadurch erhalten Sie Zugriff auf die Upsert-Option.

HINWEIS : Diese Methode überschreibt das gesamte Dokument, wenn es gefunden wird ( aus den Dokumenten ).

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. Verwenden Sie $ set

Wenn Sie eine Auswahl des Dokuments aktualisieren möchten, aber nicht das Ganze, können Sie die $ set-Methode mit update verwenden. (wieder aus den Dokumenten ) ... Also, wenn Sie einstellen möchten ...

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

Senden Sie es als ...

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

Dies verhindert, dass versehentlich alle Dokumente mit überschrieben werden { name: 'jason borne' }.


6

Zusammenfassung

  • Sie haben eine vorhandene Sammlung von Datensätzen.
  • Sie haben einen Datensatz festgelegt, der Aktualisierungen der vorhandenen Datensätze enthält.
  • Einige der Updates aktualisieren nichts wirklich, sie duplizieren das, was Sie bereits haben.
  • Alle Updates enthalten die gleichen Felder, die bereits vorhanden sind, nur möglicherweise unterschiedliche Werte.
  • Sie möchten verfolgen, wann ein Datensatz zuletzt geändert wurde und wo sich ein Wert tatsächlich geändert hat.

Beachten Sie, ich gehe davon aus, dass PyMongo sich an die Sprache Ihrer Wahl anpasst.

Anleitung:

  1. Erstellen Sie die Sammlung mit einem Index mit unique = true, damit Sie keine doppelten Datensätze erhalten.

  2. Durchlaufen Sie Ihre Eingabedatensätze und erstellen Sie Stapel von etwa 15.000 Datensätzen. Erstellen Sie für jeden Datensatz im Stapel ein Diktat, das aus den Daten besteht, die Sie einfügen möchten, wobei davon ausgegangen wird, dass jeder Datensatz ein neuer Datensatz ist. Fügen Sie diesen die "erstellten" und "aktualisierten" Zeitstempel hinzu. Geben Sie dies als Batch-Einfügebefehl mit dem Flag 'ContinueOnError' = true aus, sodass das Einfügen von allem anderen auch dann erfolgt, wenn sich dort ein doppelter Schlüssel befindet (wie es sich anhört). Dies wird sehr schnell geschehen. Bulk fügt Rock ein, ich habe 15k / Sekunde Leistungsstufen erreicht. Weitere Hinweise zu ContinueOnError finden Sie unter http://docs.mongodb.org/manual/core/write-operations/

    Aufnahmeeinsätze erfolgen SEHR schnell, sodass Sie mit diesen Einsätzen in kürzester Zeit fertig sind. Jetzt ist es Zeit, die relevanten Datensätze zu aktualisieren. Führen Sie dies mit einem Stapelabruf durch, der viel schneller als einer nach dem anderen ist.

  3. Durchlaufen Sie erneut alle Ihre Eingabedatensätze und erstellen Sie Stapel von etwa 15 KB. Extrahieren Sie die Schlüssel (am besten, wenn es einen Schlüssel gibt, aber es kann nicht geholfen werden, wenn es keinen gibt). Rufen Sie diese Datensätze mit einer Abfrage db.collectionNameBlah.find ({field: {$ in: [1, 2,3 ...})) aus Mongo ab. Stellen Sie für jeden dieser Datensätze fest, ob ein Update vorliegt, und geben Sie in diesem Fall das Update aus, einschließlich der Aktualisierung des 'aktualisierten' Zeitstempels.

    Leider sollten wir beachten, dass MongoDB 2.4 und niedriger KEINE Massenaktualisierung enthalten. Sie arbeiten daran.

Wichtige Optimierungspunkte:

  • Die Einsätze beschleunigen Ihre Arbeit in großen Mengen erheblich.
  • Das massenweise Abrufen von Datensätzen beschleunigt ebenfalls die Arbeit.
  • Einzelne Updates sind derzeit die einzig mögliche Route, aber 10Gen arbeitet daran. Vermutlich wird dies in 2.6 sein, obwohl ich nicht sicher bin, ob es bis dahin fertig sein wird, gibt es eine Menge zu tun (ich habe ihr Jira-System verfolgt).

5

Ich glaube nicht, dass Mongodb diese Art des selektiven Upsertings unterstützt. Ich habe das gleiche Problem wie LeMiz und die Verwendung von update (Kriterien, newObj, upsert, multi) funktioniert nicht richtig, wenn sowohl ein "erstellter" als auch ein "aktualisierter" Zeitstempel verwendet wird. Angesichts der folgenden Upsert-Aussage:

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

Szenario 1 - Dokument mit 'Name' von 'abc' existiert nicht: Neues Dokument wird mit 'Name' = 'abc', 'erstellt' = 2010-07-14 11:11:11 und 'aktualisiert' = erstellt 2010-07-14 11:11:11.

Szenario 2 - Dokument mit 'Name' von 'abc' existiert bereits mit folgendem: 'Name' = 'abc', 'erstellt' = 2010-07-12 09:09:09 und 'aktualisiert' = 2010-07 -13 10:10:10. Nach dem Upsert entspricht das Dokument nun dem Ergebnis in Szenario 1. In einem Upsert kann nicht angegeben werden, welche Felder beim Einfügen festgelegt werden und welche Felder beim Aktualisieren in Ruhe gelassen werden.

Meine Lösung bestand darin, einen eindeutigen Index für die Kriterienfelder zu erstellen, eine Einfügung durchzuführen und unmittelbar danach eine Aktualisierung nur für das Feld "Aktualisiert" durchzuführen.


4

Im Allgemeinen ist die Verwendung von Update in MongoDB besser, da das Dokument nur erstellt wird, wenn es noch nicht vorhanden ist, obwohl ich nicht sicher bin, wie ich das mit Ihrem Python-Adapter tun soll.

Zweitens, wenn Sie nur wissen müssen, ob dieses Dokument vorhanden ist oder nicht, ist count (), das nur eine Nummer zurückgibt, eine bessere Option als find_one, das angeblich das gesamte Dokument von Ihrer MongoDB überträgt und unnötigen Datenverkehr verursacht.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.