mongodb: wstaw, jeśli nie istnieje

146

Codziennie otrzymuję zapas dokumentów (aktualizacja). Chcę wstawić każdy element, który jeszcze nie istnieje.

  • Chcę również śledzić, kiedy wstawiłem je po raz pierwszy i kiedy ostatnio widziałem je w aktualizacji.
  • Nie chcę mieć zduplikowanych dokumentów.
  • Nie chcę usuwać dokumentu, który został wcześniej zapisany, ale nie ma go w mojej aktualizacji.
  • 95% (szacunkowo) zapisów jest niezmienionych z dnia na dzień.

Używam sterownika Python (pymongo).

To, co obecnie robię, to (pseudokod):

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)

Mój problem polega na tym, że jest bardzo wolny (40 minut dla mniej niż 100 000 rekordów, a mam ich miliony w aktualizacji). Jestem prawie pewien, że jest coś wbudowanego, aby to zrobić, ale dokument do update () jest mmmhhh .... trochę lakoniczny .... ( http://www.mongodb.org/display/DOCS/Updating )

Czy ktoś może doradzić, jak to zrobić szybciej?

LeMiz
źródło

Odpowiedzi:

153

Wygląda na to, że chcesz zrobić „upert”. MongoDB ma wbudowaną obsługę tego. Przekaż dodatkowy parametr do wywołania update (): {upsert: true}. Na przykład:

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

Zastępuje to całkowicie blok aktualizacji if-find-else-update. Wstawi, jeśli klucz nie istnieje i zaktualizuje się, jeśli tak.

Przed:

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

Po:

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

Możesz również określić, jakie dane chcesz zapisać:

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

Teraz wybrany dokument zaktualizuje tylko wartość „klucz2” i pozostawi wszystko inne nietknięte.

Van Nguyen
źródło
5
To jest prawie to, czego chcę! Jak nie dotknąć pola insertion_date, jeśli obiekt już istnieje?
LeMiz
24
czy możesz podać przykład tylko ustawienia pola przy pierwszej wkładce i nie aktualizuj go, jeśli istnieje? @VanNguyen
Ali
7
Myślę, że pierwsza część twojej odpowiedzi jest błędna. coll.update zastąpi dane, chyba że użyjesz $ set. Więc After faktycznie będzie: {'klucz2': 'wartość2', 'klucz3': 'wartość3'}
James Blackburn
9
-1 Ta odpowiedź jest niebezpieczna. Znajdujesz według wartości „klucz”, a następnie kasujesz „klucz”, aby później nie można było go znaleźć ponownie. Jest to bardzo mało prawdopodobny przypadek użycia.
Mark E. Haase
23
Powinieneś użyć operatora $ setOnInsert! Upsert zaktualizuje nawet dokument, jeśli znajdzie zapytanie.
YulCheney
63

Od MongoDB 2.4 możesz używać $ setOnInsert ( http://docs.mongodb.org/manual/reference/operator/setOnInsert/ )

Ustaw „insertion_date” za pomocą $ setOnInsert i „last_update_date” używając $ set w poleceniu upsert.

Aby zamienić swój pseudokod w działający przykład:

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
źródło
3
To jest poprawne, możesz sprawdzić dokument pasujący do filtra i wstawić coś, jeśli nie zostanie znaleziony, używając $ setOnInsert. Zauważ jednak, że był błąd, w którym nie można było $ setOnInsert z polem _id - powiedziałoby coś w stylu "nie można zmodyfikować pola _id". To był błąd, naprawiony w wersji 2.5.4 lub tam. Jeśli widzisz ten komunikat lub problem, po prostu pobierz najnowszą wersję.
Kieren Johnstone
19

Zawsze możesz utworzyć unikalny indeks, co spowoduje, że MongoDB odrzuci konflikt zapisu. Rozważ następujące czynności wykonane przy użyciu powłoki 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
źródło
12

Możesz użyć Upsert z operatorem $ setOnInsert.

db.Table.update({noExist: true}, {"$setOnInsert": {xxxYourDocumentxxx}}, {upsert: true})
YulCheney
źródło
11
Dla każdego, kto pyta o pymongo, trzeci parametr powinien mieć wartość true lub upsert = True, a nie dict
S ..
6

1. Użyj aktualizacji.

Czerpiąc z powyższej odpowiedzi Van Nguyena, użyj aktualizacji zamiast zapisywania. Daje to dostęp do opcji upsert.

UWAGA : Ta metoda zastępuje cały dokument po znalezieniu ( z dokumentów )

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. Użyj $ set

Jeśli chcesz zaktualizować wybór dokumentu, ale nie całość, możesz użyć metody $ set z update. (znowu z dokumentów ) ... Więc jeśli chcesz ustawić ...

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

Wyślij to jako ...

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

Zapobiega to przypadkowemu zastąpieniu wszystkich dokumentów formatem { name: 'jason borne' }.

Meshach Jackson
źródło
6

Podsumowanie

  • Masz istniejącą kolekcję rekordów.
  • Masz zestaw rekordów, które zawierają aktualizacje istniejących rekordów.
  • Niektóre aktualizacje tak naprawdę niczego nie aktualizują, powielają to, co już masz.
  • Wszystkie aktualizacje zawierają te same pola, które już istnieją, tylko prawdopodobnie różne wartości.
  • Chcesz śledzić, kiedy rekord został ostatnio zmieniony, gdzie faktycznie zmieniła się wartość.

Uwaga, zakładam, że PyMongo, zmień, aby dopasować do wybranego języka.

Instrukcje:

  1. Utwórz kolekcję z indeksem z unique = true, aby nie uzyskać zduplikowanych rekordów.

  2. Powtarzaj swoje rekordy wejściowe, tworząc partie 15 000 rekordów. Dla każdego rekordu w partii utwórz dyktę składającą się z danych, które chcesz wstawić, zakładając, że każdy z nich będzie nowym rekordem. Dodaj do nich „utworzone” i „zaktualizowane” sygnatury czasowe. Wydaj to jako polecenie wstawiania wsadowego z flagą „ContinueOnError” = true, więc wstawianie wszystkiego innego ma miejsce, nawet jeśli jest tam zduplikowany klucz (co wydaje się, że będzie). TO SIĘ STANIE BARDZO SZYBKO. Duże wstawki rocka, osiągnąłem poziom wydajności 15k / sekundę. Więcej informacji na temat ContinueOnError można znaleźć pod adresem http://docs.mongodb.org/manual/core/write-operations/

    Wstawianie płyt odbywa się BARDZO szybko, więc w mgnieniu oka skończysz z tymi wstawkami. Teraz nadszedł czas, aby zaktualizować odpowiednie rekordy. Zrób to przy pobieraniu partii, znacznie szybciej niż pojedynczo.

  3. Ponownie wykonaj iterację wszystkich swoich rekordów wejściowych, tworząc partie po 15 KB. Wyjmij klucze (najlepiej, jeśli jest jeden klucz, ale nie można pomóc, jeśli go nie ma). Pobierz tę grupę rekordów z Mongo za pomocą zapytania db.collectionNameBlah.find ({field: {$ in: [1, 2,3 ...}). Dla każdego z tych rekordów określ, czy istnieje aktualizacja, a jeśli tak, przeprowadź aktualizację, w tym aktualizację „zaktualizowanej” sygnatury czasowej.

    Niestety, powinniśmy zauważyć, że MongoDB 2.4 i starsze NIE obejmują operacji zbiorczej aktualizacji. Pracują nad tym.

Kluczowe punkty optymalizacji:

  • Wkładki znacznie przyspieszą masowe operacje.
  • Masowe pobieranie nagrań również przyspieszy działanie.
  • Poszczególne aktualizacje są obecnie jedyną możliwą trasą, ale 10Gen nad nią pracuje. Przypuszczalnie będzie to 2.6, chociaż nie jestem pewien, czy do tego czasu się skończy, jest wiele rzeczy do zrobienia (śledzę ich system Jira).
Kevin J. Rice
źródło
5

Nie sądzę, że mongodb obsługuje tego typu selektywne podwyższanie. Mam ten sam problem co LeMiz, a używanie aktualizacji (kryteria, noweObj, upsert, multi) nie działa poprawnie, gdy mamy do czynienia zarówno z „utworzonym”, jak i „zaktualizowanym” znacznikiem czasu. Biorąc pod uwagę następujące oświadczenie upsert:

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

Scenariusz nr 1 - dokument o „nazwie” „abc” nie istnieje: nowy dokument jest tworzony z „name” = „abc”, „created” = 2010-07-14 11:11:11 i „updated” = 2010-07-14 11:11:11.

Scenariusz nr 2 - dokument o „nazwie” „abc” już istnieje z następującymi: „name” = „abc”, „created” = 2010-07-12 09:09:09 i „updated” = 2010-07 -13 10:10:10. Po poprawie dokument byłby teraz taki sam, jak wynik w scenariuszu nr 1. Nie ma sposobu, aby określić w upsert, które pola mają być ustawione podczas wstawiania, a które pola mają pozostać bez zmian w przypadku aktualizacji.

Moje rozwiązanie polegało na utworzeniu unikalnego indeksu dla pól krytycznych , wykonaniu wstawienia i natychmiast po wykonaniu aktualizacji tylko w polu „zaktualizowanym”.

Yonsink
źródło
4

Ogólnie rzecz biorąc, użycie aktualizacji jest lepsze w MongoDB, ponieważ po prostu utworzy dokument, jeśli jeszcze nie istnieje, chociaż nie jestem pewien, jak to zrobić z adapterem Pythona.

Po drugie, jeśli chcesz tylko wiedzieć, czy ten dokument istnieje, czy nie, funkcja count (), która zwraca tylko liczbę, będzie lepszą opcją niż find_one, która rzekomo przenosi cały dokument z bazy danych MongoDB, powodując niepotrzebny ruch.

Thomas R. Koll
źródło