SQLite - UPSERT * nie * WSTAW lub WYMIANA

535

http://en.wikipedia.org/wiki/Upsert

Wstaw aktualizację przechowywaną proc na SQL Server

Czy jest jakiś sprytny sposób na zrobienie tego w SQLite, o którym nie myślałem?

Zasadniczo chcę zaktualizować trzy z czterech kolumn, jeśli rekord istnieje, jeśli nie istnieje, chcę WSTAWić rekord z wartością domyślną (NUL) dla czwartej kolumny.

Identyfikator jest kluczem podstawowym, więc UPSERT będzie miał tylko jeden rekord.

(Próbuję uniknąć narzutu SELECT, aby ustalić, czy muszę oczywiście ZAKTUALIZOWAĆ, czy WSTAWIĆ)

Propozycje?


Nie mogę potwierdzić tej składni na stronie SQLite dla TABLE CREATE. Nie zbudowałem wersji demo, aby ją przetestować, ale wydaje się, że nie jest obsługiwana.

Gdyby tak było, mam trzy kolumny, więc faktycznie wyglądałoby to tak:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    Blob1 BLOB ON CONFLICT REPLACE, 
    Blob2 BLOB ON CONFLICT REPLACE, 
    Blob3 BLOB 
);

ale pierwsze dwa obiekty BLOB nie spowodują konfliktu, tylko identyfikator byłby więc więc asusme Blob1 i Blob2 nie zostaną zastąpione (zgodnie z życzeniem)


AKTUALIZACJE w SQLite, gdy powiązanie danych jest kompletną transakcją, co oznacza, że ​​każdy wysłany wiersz do aktualizacji wymaga: Przygotuj / Powiąż / Krok / Zakończ instrukcje w przeciwieństwie do INSERT, który pozwala na użycie funkcji resetowania

Życie obiektu instrukcji przebiega mniej więcej tak:

  1. Utwórz obiekt za pomocą sqlite3_prepare_v2 ()
  2. Powiąż wartości z parametrami hosta za pomocą interfejsów sqlite3_bind_.
  3. Uruchom SQL, wywołując sqlite3_step ()
  4. Zresetuj instrukcję za pomocą sqlite3_reset (), a następnie wróć do kroku 2 i powtórz.
  5. Zniszcz obiekt instrukcji za pomocą narzędzia sqlite3_finalize ().

AKTUALIZACJA Zgaduję, że jest powolny w porównaniu do INSERT, ale jak to się ma do SELECT przy użyciu klucza podstawowego?

Być może powinienem użyć polecenia select, aby odczytać czwartą kolumnę (Blob3), a następnie użyć REPLACE, aby napisać nowy rekord mieszający oryginalną czwartą kolumnę z nowymi danymi dla pierwszych 3 kolumn?

Mike Trader
źródło
5
SQLite - UPSERT dostępny w wersji wstępnej: sqlite.1065341.n5.nabble.com/…
gaurav
3
UPSERT dostępny w wersji 3.24.0 SQLite
pablo_worker

Odpowiedzi:

854

Zakładając trzy kolumny w tabeli: ID, NAZWA, ROLA


ZŁE: Spowoduje to wstawienie lub zastąpienie wszystkich kolumn nowymi wartościami dla ID = 1:

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (1, 'John Foo', 'CEO');

ŹLE: Spowoduje to wstawienie lub zastąpienie 2 kolumn ... kolumna NAME zostanie ustawiona na NULL lub wartość domyślna:

INSERT OR REPLACE INTO Employee (id, role) 
  VALUES (1, 'code monkey');

DOBRY : Użyj klauzuli konfliktu SQLite Obsługa UPSERT w SQLite! Składnia UPSERT została dodana do SQLite w wersji 3.24.0!

UPSERT to specjalny dodatek do składni INSERT, który powoduje, że INSERT zachowuje się jak AKTUALIZACJA lub nie działa, jeśli INSERT naruszy ograniczenie związane z unikalnością. UPSERT nie jest standardowym SQL. UPSERT w SQLite jest zgodny ze składnią ustaloną przez PostgreSQL.

wprowadź opis zdjęcia tutaj

DOBRY, ale delikatny: zaktualizuje 2 kolumny. Gdy ID = 1 istnieje, NAZWA nie ulegnie zmianie. Gdy ID = 1 nie istnieje, nazwa będzie domyślna (NULL).

INSERT OR REPLACE INTO Employee (id, role, name) 
  VALUES (  1, 
            'code monkey',
            (SELECT name FROM Employee WHERE id = 1)
          );

Spowoduje to zaktualizowanie 2 kolumn. Gdy istnieje identyfikator = 1, nie ma to wpływu na rolę ROLE. Gdy ID = 1 nie istnieje, rola zostanie ustawiona na „Ogrzewacz stołowy” zamiast wartości domyślnej.

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
Eric B.
źródło
33
+1 genialny! Wbudowana klauzula wyboru daje elastyczność w zakresie zastępowania domyślnej funkcji WYMIANY KONFLIKTU, jeśli konieczne jest połączenie / porównanie starej wartości i nowej wartości dla dowolnego pola.
G__
24
Jeśli do pracownika odnoszą się inne wiersze z kaskadowym usunięciem, wówczas pozostałe wiersze będą nadal usuwane przez zastąpienie.
Don Reba
246
To boli. SQlite potrzebuje UPSERT.
andig
21
Czy możesz wyjaśnić, dlaczego spowoduje to wstawienie lub zastąpienie wszystkich kolumn nowymi wartościami dla ID = 1: uważa się za ZŁE w twoim pierwszym przykładzie? Przedstawione tam polecenie ma na celu utworzenie nowego rekordu o identyfikatorze 1, nazwie John Foo i roli CEO lub zastąpienie rekordu o identyfikatorze 1, jeśli już tam jest, tymi danymi (zakładając, że identyfikator jest kluczem podstawowym) . Dlaczego więc jest źle, jeśli dokładnie tak się dzieje?
LUB Mapper
10
@Cornelius: To jasne, ale nie tak dzieje się w pierwszym przykładzie. Pierwszy przykład ma na celu przymusowe ustawienie wszystkich kolumn, co dokładnie się dzieje, bez względu na to, czy rekord zostanie wstawiony, czy zastąpiony. Dlaczego więc uważa się to za złe? Połączona odpowiedź wskazuje tylko, dlaczego coś złego może się zdarzyć przy określaniu podzbioru kolumn, tak jak w drugim przykładzie; wydaje się, że nie omawia żadnych złych skutków tego, co dzieje się w twoim pierwszym przykładzie, INSERT OR REPLACEpodając wartości dla wszystkich kolumn.
LUB Mapper
134

WSTAW LUB WYMIANA NIE JEST równoważne z „UPSERT”.

Powiedzmy, że mam tabelę Pracownik z polami id, nazwa i rola:

INSERT OR REPLACE INTO Employee ("id", "name", "role") VALUES (1, "John Foo", "CEO")
INSERT OR REPLACE INTO Employee ("id", "role") VALUES (1, "code monkey")

Boom, straciłeś nazwisko pracownika numer 1. SQLite zastąpił go wartością domyślną.

Oczekiwanym wyjściem UPSERT byłaby zmiana roli i zachowanie nazwy.

gregschlom
źródło
21
-1 ode mnie, obawiam się. Tak, zaakceptowana odpowiedź jest błędna, ale chociaż twoja odpowiedź wskazuje na problem, nie jest to również odpowiedź. Aby uzyskać rzeczywistą odpowiedź, zobacz sprytne rozwiązanie Erica B z wykorzystaniem coalesce((select ..), 'new value')klauzuli osadzonej . Myślę, że odpowiedź Erica wymaga więcej głosów.
Dzień
22
W rzeczy samej. Eric jest zdecydowanie najlepszą odpowiedzią i zasługuje na więcej głosów. Biorąc to pod uwagę, myślę, że wskazując problem, przyczyniłem się trochę do znalezienia dobrej odpowiedzi (odpowiedź Erica przyszła później i opiera się na przykładowych tabelach sql w mojej odpowiedzi). Więc nie jestem pewien, czy zasługuję na -1, ale nieważne :)
gregschlom
6
+1, aby zrównoważyć -1 powyżej. lol. Ciekawa oś czasu w tym wątku. Najwyraźniej Twoja odpowiedź była ponad tydzień przed odpowiedzią Erica, ale obaj odpowiedzieliście na pytanie prawie dwa lata po tym, jak zostało zadane. +1 dla Erica również za opracowanie.
Rich
2
@ QED nie, ponieważ delete + insert (który jest zamiennikiem) to 2 instrukcje dml, na przykład z własnymi wyzwalaczami. To nie to samo, co tylko 1 instrukcja aktualizacji.
Sebas
109

Odpowiedź Erica B jest OK, jeśli chcesz zachować tylko jedną lub dwie kolumny z istniejącego wiersza. Jeśli chcesz zachować wiele kolumn, staje się zbyt kłopotliwy.

Oto podejście, które dobrze skaluje się do dowolnej liczby kolumn po obu stronach. Aby to zilustrować, przyjmuję następujący schemat:

 CREATE TABLE page (
     id      INTEGER PRIMARY KEY,
     name    TEXT UNIQUE,
     title   TEXT,
     content TEXT,
     author  INTEGER NOT NULL REFERENCES user (id),
     ts      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );

Zwróć uwagę w szczególności, że namejest to naturalny klucz wiersza - idjest używany tylko dla kluczy obcych, więc chodzi o to, aby SQLite sam wybrał wartość identyfikatora podczas wstawiania nowego wiersza. Ale aktualizując istniejący wiersz na jego podstawie name, chcę, aby nadal miał starą wartość identyfikatora (oczywiście!).

Osiągam prawdę UPSERTdzięki następującym konstrukcjom:

 WITH new (name, title, author) AS ( VALUES('about', 'About this site', 42) )
 INSERT OR REPLACE INTO page (id, name, title, content, author)
 SELECT old.id, new.name, new.title, old.content, new.author
 FROM new LEFT JOIN page AS old ON new.name = old.name;

Dokładna forma tego zapytania może się nieco różnić. Kluczem jest użycieINSERT SELECT z lewym łączeniem zewnętrznym, aby połączyć istniejący wiersz z nowymi wartościami.

Tutaj, jeśli wiersz wcześniej nie istniał, old.idbędzie, NULLa SQLite automatycznie przypisze identyfikator, ale jeśli już taki wiersz był, old.idbędzie miał rzeczywistą wartość i zostanie on ponownie wykorzystany. Właśnie tego chciałem.

W rzeczywistości jest to bardzo elastyczne. Zwróć uwagę, że tskolumna jest kompletnie brakująca ze wszystkich stron - ponieważ ma DEFAULTwartość, SQLite i tak zrobi właściwą rzecz w każdym przypadku, więc nie muszę się tym zajmować.

Można również dołączyć kolumnę na obu newi oldpo bokach, a następnie użyć na przykład COALESCE(new.content, old.content)w zewnętrznej SELECTpowiedzieć „Włóż nowe treści, jeśli nie było w ogóle, inaczej zachować starą treść” - np jeśli używasz stałego zapytania i są wiążące nowa wartości z symbolami zastępczymi.

Arystoteles Pagaltzis
źródło
13
+1, działa świetnie, ale dodaj WHERE name = "about"ograniczenie, SELECT ... AS oldaby przyspieszyć. Jeśli masz 1m + wierszy, jest to bardzo wolne.
Dobra uwaga, +1 w Twoim komentarzu. Zostawię to jednak poza odpowiedzią, ponieważ dodanie takiej WHEREklauzuli wymaga po prostu tego rodzaju redundancji w zapytaniu, którego starałem się w pierwszej kolejności uniknąć, kiedy wymyśliłem takie podejście. Jak zawsze: kiedy potrzebujesz wydajności, denormalizuj - w tym przypadku strukturę zapytania.
Arystoteles Pagaltzis
4
Możesz uprościć przykład arystotelesa, jeśli chcesz: INSERT OR REPLACE INTO page (id, name, title, content, author) SELECT id, 'about', 'About this site', content, 42 FROM ( SELECT NULL ) LEFT JOIN ( SELECT * FROM page WHERE name = 'about' )
jcox 18.08.16
4
Czy nie spowodowałoby to niepotrzebnie ON DELETEwyzwalaczy podczas przeprowadzania zamiany (czyli aktualizacji)?
Karakuri,
3
Z pewnością wyzwoli ON DELETEwyzwalacze. Nie wiem o niepotrzebnie. Dla większości użytkowników byłoby to prawdopodobnie niepotrzebne, a nawet niepożądane, ale może nie dla wszystkich użytkowników. Podobnie z tego powodu, że kasuje również kaskadowo wszelkie wiersze z kluczami obcymi do danego wiersza - prawdopodobnie problem dla wielu użytkowników. Niestety, SQLite nie ma nic bliższego prawdziwemu UPSERT. ( INSTEAD OF UPDATEChyba
zafałszowanie
86

Jeśli generalnie wykonujesz aktualizacje, zrobiłbym ...

  1. Rozpocznij transakcję
  2. Wykonaj aktualizację
  3. Sprawdź liczbę wierszy
  4. Jeśli jest 0, wstaw wstawkę
  5. Popełnić

Jeśli ogólnie robisz wstawki, zrobiłbym to

  1. Rozpocznij transakcję
  2. Wypróbuj wkładkę
  3. Sprawdź błąd naruszenia klucza podstawowego
  4. jeśli wystąpi błąd, wykonaj aktualizację
  5. Popełnić

W ten sposób unikasz wyboru i jesteś transakcyjnie zdrowy na Sqlite.

Sam Saffron
źródło
Dzięki, właśnie zadał to @ stackoverflow.com/questions/2717590/... . +1 ode mnie! =)
Alix Axel
4
Jeśli zamierzasz sprawdzić liczbę wierszy za pomocą narzędzia sqlite3_changes () w trzecim kroku, upewnij się, że nie używasz uchwytu DB z wielu wątków do modyfikacji.
Linulin
3
Czy poniższe działania nie byłyby mniej niespokojne z tym samym skutkiem: 1) wybierz tabelę postaci id, gdzie id = „x” 2) if (ResultSet.rows.length == 0) aktualizuj tabelę, gdzie id = „x”;
Florin,
20
Naprawdę chciałbym, żeby INSERT OR UPDATE było częścią tego języka
Florin
Skończyło się to dla mnie najlepszym działaniem, próbując zrobić WYMIENIĆ NA lub zawsze i wstawianie / aktualizowanie zabijało moją wydajność, kiedy przede wszystkim robiłem aktualizacje zamiast wstawiania.
PherricOxide
83

Ta odpowiedź została zaktualizowana, więc poniższe komentarze nie mają już zastosowania.

2018-05-18 STOP PRESS.

Obsługa UPSERT w SQLite!Składnia UPSERT została dodana do SQLite w wersji 3.24.0 (w toku)!

UPSERT to specjalny dodatek do składni INSERT, który powoduje, że INSERT zachowuje się jak AKTUALIZACJA lub nie działa, jeśli INSERT naruszy ograniczenie związane z unikalnością. UPSERT nie jest standardowym SQL. UPSERT w SQLite jest zgodny ze składnią ustaloną przez PostgreSQL.

wprowadź opis zdjęcia tutaj

alternatywnie:

Innym całkowicie odmiennym sposobem jest: w mojej aplikacji ustawiam w wierszu memoryID wartość long.MaxValue podczas tworzenia wiersza w pamięci. (MaxValue nigdy nie zostanie użyty jako identyfikator, którego nie będziesz żył wystarczająco długo ... Zatem jeśli rowID nie jest taką wartością, to musi już znajdować się w bazie danych, więc potrzebuje UPDATE, jeśli jest MaxValue, wówczas wymaga wstawienia. Jest to przydatne tylko wtedy, gdy możesz śledzić rowIDs w swojej aplikacji.

Anthony Lambert
źródło
5
Amen. Prosty jest lepszy niż złożony. Wydaje się to nieco prostsze niż zaakceptowana odpowiedź.
kkurian
4
Myślałem, że nie możesz WSTAWIĆ DO ... GDZIE w sqlite? Jest to dla mnie błąd składniowy w sqlite3
Charlie Martin
5
@CharlieMartin jest poprawny. Ta składnia jest niepoprawna dla SQLite - tego zażądał OP. WHEREKlauzula nie może być dodawana do INSERTrachunku: sqlite-wkładki ...
colm.anseo
4
Ta odpowiedź sprawiła, że ​​straciłem dużo czasu, pytanie dotyczy SQLITE i nie wiedziałem, że INSERT WHERE nie był obsługiwany w sqite, proszę dodać tę notatkę, że jest niepoprawna dla sqlite w swojej odpowiedzi
Miquel
3
@colminator i inni: Naprawiłem jego instrukcję SQL, używając twojej sugestii.
F Lekschas
61

Zdaję sobie sprawę, że to stary wątek, ale ostatnio pracowałem w sqlite3 i opracowałem tę metodę, która lepiej odpowiada moim potrzebom dynamicznego generowania sparametryzowanych zapytań:

insert or ignore into <table>(<primaryKey>, <column1>, <column2>, ...) values(<primaryKeyValue>, <value1>, <value2>, ...); 
update <table> set <column1>=<value1>, <column2>=<value2>, ... where changes()=0 and <primaryKey>=<primaryKeyValue>; 

Nadal są 2 zapytania z klauzulą ​​where w aktualizacji, ale wydaje się, że załatwia sprawę. Mam też w głowie tę wizję, że sqlite może całkowicie zoptymalizować instrukcję aktualizacji, jeśli wywołanie change () jest większe od zera. Niezależnie od tego, czy tak się dzieje, czy nie, ale człowiek może śnić, prawda? ;)

W przypadku punktów bonusowych możesz dołączyć tę linię, która zwraca identyfikator wiersza, niezależnie od tego, czy jest to nowo wstawiony wiersz, czy istniejący wiersz.

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
Chris Stavropoulos
źródło
+ 1. Dokładnie to, co próbowałem zastąpić, wykonując konwersję z jakiegoś kodu SQL Server TSQL. Dzięki. SQL, który próbowałem zastąpić, był jakUpdate <statement> If @@ROWCOUNT=0 INSERT INTO <statement>
DarrenMB
14

Oto rozwiązanie, które naprawdę jest UPSERT (UPDATE lub INSERT) zamiast INSERT OR REPLACE (który działa inaczej w wielu sytuacjach).

Działa to tak:
1. Spróbuj zaktualizować, jeśli istnieje rekord o tym samym identyfikatorze.
2. Jeśli aktualizacja nie zmieniła żadnych wierszy ( NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)), następnie wstaw rekord.

Tak więc albo istniejący rekord został zaktualizowany, albo zostanie wstawione.

Ważnym szczegółem jest użycie funkcji SQL zmian () do sprawdzenia, czy instrukcja aktualizacji trafiła do istniejących rekordów i wykonanie instrukcji insert tylko, jeśli nie trafiła do żadnego rekordu.

Należy wspomnieć, że funkcja zmiany () nie zwraca zmian wykonanych przez wyzwalacze niższego poziomu (patrz http://sqlite.org/lang_corefunc.html#changes ), więc pamiętaj o tym.

Oto SQL ...

Aktualizacja testowa:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 2;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 2, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;

Wkładka testowa:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 3;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 3, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;
David Liebeherr
źródło
2
To wydaje mi się lepsze rozwiązanie dla mnie niż Erica. Jednak INSERT INTO Contact (Id, Name) SELECT 3, 'Bob' WHERE changes() = 0;powinno również działać.
bkausbk
Dziękuję człowieku, działa również w WebSQL (przy użyciu wtyczki Cordova i SQLite)
Zappescu,
11

Począwszy od wersji 3.24.0 UPSERT jest obsługiwany przez SQLite.

Z dokumentacji :

UPSERT to specjalny dodatek do składni INSERT, który powoduje, że INSERT zachowuje się jak AKTUALIZACJA lub nie działa, jeśli INSERT naruszy ograniczenie związane z unikalnością. UPSERT nie jest standardowym SQL. UPSERT w SQLite jest zgodny ze składnią ustaloną przez PostgreSQL. Składnia UPSERT została dodana do SQLite w wersji 3.24.0 (w toku).

UPSERT to zwykła instrukcja INSERT, po której następuje specjalna klauzula ON CONFLICT

wprowadź opis zdjęcia tutaj

Źródło obrazu: https://www.sqlite.org/images/syntax/upsert-clause.gif

Łukasz Szozda
źródło
8

Rzeczywiście możesz zrobić wstawkę w SQLite, wygląda ona nieco inaczej niż zwykle. Wyglądałoby to tak:

INSERT INTO table name (column1, column2) 
VALUES ("value12", "value2") WHERE id = 123 
ON CONFLICT DO UPDATE 
SET column1 = "value1", column2 = "value2" WHERE id = 123
Brill Pappin
źródło
5

Rozwijając odpowiedź Arystotelesa, możesz WYBRAĆ z obojętnego stołu „singleton” (stół własnego dzieła z jednym rzędem). Pozwala to uniknąć powielania.

Zachowałem również przykład przenośny między MySQL i SQLite i wykorzystałem kolumnę „data_added” jako przykład tego, jak można ustawić kolumnę tylko za pierwszym razem.

 REPLACE INTO page (
   id,
   name,
   title,
   content,
   author,
   date_added)
 SELECT
   old.id,
   "about",
   "About this site",
   old.content,
   42,
   IFNULL(old.date_added,"21/05/2013")
 FROM singleton
 LEFT JOIN page AS old ON old.name = "about";
Społeczność
źródło
4

Najlepszym podejściem, jakie znam, jest aktualizacja, a następnie wstawienie. „Narzut zaznaczenia” jest konieczny, ale nie jest to straszne obciążenie, ponieważ szukasz klucza podstawowego, który jest szybki.

Powinieneś być w stanie zmodyfikować poniższe instrukcje za pomocą nazw tabel i pól, aby robić to, co chcesz.

--first, update any matches
UPDATE DESTINATION_TABLE DT
SET
  MY_FIELD1 = (
              SELECT MY_FIELD1
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
 ,MY_FIELD2 = (
              SELECT MY_FIELD2
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
WHERE EXISTS(
            SELECT ST2.PRIMARY_KEY
            FROM
              SOURCE_TABLE ST2
             ,DESTINATION_TABLE DT2
            WHERE ST2.PRIMARY_KEY = DT2.PRIMARY_KEY
            );

--second, insert any non-matches
INSERT INTO DESTINATION_TABLE(
  MY_FIELD1
 ,MY_FIELD2
)
SELECT
  ST.MY_FIELD1
 ,NULL AS MY_FIELD2  --insert NULL into this field
FROM
  SOURCE_TABLE ST
WHERE NOT EXISTS(
                SELECT DT2.PRIMARY_KEY
                FROM DESTINATION_TABLE DT2
                WHERE DT2.PRIMARY_KEY = ST.PRIMARY_KEY
                );
JosephStyons
źródło
7
Sposób na skomplikowane.
kontrast
Myślę, że to nie jest dobry pomysł, ponieważ musisz zrobić dwa razy żądanie do silnika bazy danych.
Ricardo da Rocha Vitor
3

Jeśli ktoś chce przeczytać moje rozwiązanie dla SQLite w Cordova, dostałem tę ogólną metodę js dzięki powyższej odpowiedzi @david.

function    addOrUpdateRecords(tableName, values, callback) {
get_columnNames(tableName, function (data) {
    var columnNames = data;
    myDb.transaction(function (transaction) {
        var query_update = "";
        var query_insert = "";
        var update_string = "UPDATE " + tableName + " SET ";
        var insert_string = "INSERT INTO " + tableName + " SELECT ";
        myDb.transaction(function (transaction) {
            // Data from the array [[data1, ... datan],[()],[()]...]:
            $.each(values, function (index1, value1) {
                var sel_str = "";
                var upd_str = "";
                var remoteid = "";
                $.each(value1, function (index2, value2) {
                    if (index2 == 0) remoteid = value2;
                    upd_str = upd_str + columnNames[index2] + "='" + value2 + "', ";
                    sel_str = sel_str + "'" + value2 + "', ";
                });
                sel_str = sel_str.substr(0, sel_str.length - 2);
                sel_str = sel_str + " WHERE NOT EXISTS(SELECT changes() AS change FROM "+tableName+" WHERE change <> 0);";
                upd_str = upd_str.substr(0, upd_str.length - 2);
                upd_str = upd_str + " WHERE remoteid = '" + remoteid + "';";                    
                query_update = update_string + upd_str;
                query_insert = insert_string + sel_str;  
                // Start transaction:
                transaction.executeSql(query_update);
                transaction.executeSql(query_insert);                    
            });
        }, function (error) {
            callback("Error: " + error);
        }, function () {
            callback("Success");
        });
    });
});
}

Więc najpierw wybierz nazwy kolumn za pomocą tej funkcji:

function get_columnNames(tableName, callback) {
myDb.transaction(function (transaction) {
    var query_exec = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name ='" + tableName + "'";
    transaction.executeSql(query_exec, [], function (tx, results) {
        var columnParts = results.rows.item(0).sql.replace(/^[^\(]+\(([^\)]+)\)/g, '$1').split(','); ///// RegEx
        var columnNames = [];
        for (i in columnParts) {
            if (typeof columnParts[i] === 'string')
                columnNames.push(columnParts[i].split(" ")[0]);
        };
        callback(columnNames);
    });
});
}

Następnie zbuduj transakcje programowo.

„Wartości” to tablica, którą powinieneś zbudować wcześniej i reprezentuje ona wiersze, które chcesz wstawić lub zaktualizować w tabeli.

„remoteid” to identyfikator, którego użyłem jako odniesienia, ponieważ synchronizuję się z moim zdalnym serwerem.

Aby skorzystać z wtyczki SQLite Cordova, zapoznaj się z oficjalnym linkiem

Zappescu
źródło
2

Ta metoda remiksuje kilka innych metod z odpowiedzi na to pytanie i obejmuje użycie CTE (Common Table Expressions). Przedstawię zapytanie, a następnie wyjaśnię, dlaczego zrobiłem to, co zrobiłem.

Chciałbym zmienić nazwisko pracownika 300 na DAVIS, jeśli istnieje pracownik 300. W przeciwnym razie dodam nowego pracownika.

Nazwa tabeli: pracownicy Kolumny: id, imię, nazwisko

Zapytanie to:

INSERT OR REPLACE INTO employees (employee_id, first_name, last_name)
WITH registered_employees AS ( --CTE for checking if the row exists or not
    SELECT --this is needed to ensure that the null row comes second
        *
    FROM (
        SELECT --an existing row
            *
        FROM
            employees
        WHERE
            employee_id = '300'

        UNION

        SELECT --a dummy row if the original cannot be found
            NULL AS employee_id,
            NULL AS first_name,
            NULL AS last_name
    )
    ORDER BY
        employee_id IS NULL --we want nulls to be last
    LIMIT 1 --we only want one row from this statement
)
SELECT --this is where you provide defaults for what you would like to insert
    registered_employees.employee_id, --if this is null the SQLite default will be used
    COALESCE(registered_employees.first_name, 'SALLY'),
    'DAVIS'
FROM
    registered_employees
;

Zasadniczo użyłem CTE, aby zmniejszyć liczbę przypadków, gdy instrukcja select musi być użyta do ustalenia wartości domyślnych. Ponieważ jest to CTE, po prostu wybieramy kolumny, które chcemy z tabeli, a instrukcja INSERT używa tego.

Teraz możesz zdecydować, jakie wartości domyślne chcesz zastosować, zastępując wartości null w funkcji COALESCE odpowiednimi wartościami.

Dodzi Dzakuma
źródło
2

Podążając za Arystotelesem Pagaltzisem i pomysłem COALESCEz odpowiedzi Erica B , tutaj jest opcja upsert, aby zaktualizować tylko kilka kolumn lub wstawić pełny wiersz, jeśli nie istnieje.

W takim przypadku wyobraź sobie, że tytuł i treść powinny zostać zaktualizowane, zachowując pozostałe stare wartości, gdy istnieją i wstawiając dostarczone, gdy nie znaleziono nazwy:

UWAGA id jest wymuszone, aby mieć wartość NULL, gdy INSERTma to być autowzrost. Jeśli jest to tylko wygenerowany klucz podstawowy, COALESCEmożna go również użyć (patrz komentarz Arystotelesa Pagaltza ).

WITH new (id, name, title, content, author)
     AS ( VALUES(100, 'about', 'About this site', 'Whatever new content here', 42) )
INSERT OR REPLACE INTO page (id, name, title, content, author)
SELECT
     old.id, COALESCE(old.name, new.name),
     new.title, new.content,
     COALESCE(old.author, new.author)
FROM new LEFT JOIN page AS old ON new.name = old.name;

Tak więc ogólna zasada brzmi: jeśli chcesz zachować stare wartości, użyj COALESCE, gdy chcesz zaktualizować wartości, użyjnew.fieldname

Miquel
źródło
COALESCE(old.id, new.id)jest zdecydowanie nie tak z kluczem autoinkrementacji. I chociaż „nie zmieniaj większości wiersza bez zmian, z wyjątkiem przypadków, w których brakuje wartości”, brzmi to jak przypadek użycia, który ktoś może faktycznie mieć, nie sądzę, że ludzie tego szukają, gdy szukają sposobu na wykonanie UPSERT.
Arystoteles Pagaltzis,
@AristotlePagaltzis przepraszam, jeśli się mylę, nie używam autoinkrementów. W tym zapytaniu szukam tylko aktualizacji kilku kolumn lub wstawienia pełnego wiersza z podanymi wartościami, jeśli nie istnieje . Bawiłem się twoim zapytaniem i nie mogłem go osiągnąć podczas wstawiania: kolumny wybrane z oldtabeli, do której zostały przypisane NULL, a nie wartości podane w new. To jest powód do użycia COALESCE. Nie jestem ekspertem od sqlite, testowałem to zapytanie i wydaje się, że działa w tej sprawie, bardzo dziękuję, jeśli możesz wskazać mi rozwiązanie z autoinkrementami
Miquel
2
W przypadku klucza autoinkrementującego, chcesz wstawić NULLjako klucz, ponieważ to mówi SQLite, aby zamiast tego wstawił następną dostępną wartość.
Arystoteles Pagaltzis
Nie twierdzę, że nie powinieneś robić tego, co robisz (jeśli potrzebujesz, to potrzebujesz), po prostu nie jest to tak naprawdę odpowiedź na pytanie tutaj. UPSERT ogólnie oznacza, że ​​masz wiersz, który chcesz zapisać w tabeli, i po prostu nie wiesz, czy masz już pasujący wiersz do wprowadzenia wartości, czy też musisz wstawić je jako nowy wiersz. Twój przypadek użycia jest taki, że jeśli masz już pasujący wiersz, chcesz zignorować większość wartości z nowego wiersza . W porządku, po prostu nie zadano pytania.
Arystoteles Pagaltzis
1

Myślę, że może to być to, czego szukasz: klauzula ON CONFLICT .

Jeśli zdefiniujesz swoją tabelę w ten sposób:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    field1 TEXT 
); 

Teraz, jeśli wykonasz INSERT z identyfikatorem, który już istnieje, SQLite automatycznie wykonuje UPDATE zamiast INSERT.

Hth ...

kmelvn
źródło
6
Nie sądzę, żeby to zadziałało, wyczyści kolumny brakujące w instrukcji insert
Sam Saffron
3
@Mosor: -1 ode mnie, przepraszam. Jest to to samo, co wydanie REPLACEoświadczenia.
Alix Axel
7
-1, ponieważ powoduje to usunięcie, a następnie wstawienie, jeśli klucz podstawowy już istnieje.
frast
0

Jeśli nie masz nic przeciwko robieniu tego w dwóch operacjach.

Kroki:

1) Dodaj nowe elementy za pomocą „INSERT OR IGNORE”

2) Zaktualizuj istniejące elementy za pomocą „UPDATE”

Dane wejściowe do obu kroków to ta sama kolekcja nowych lub aktualizowanych elementów. Działa dobrze z istniejącymi elementami, które nie wymagają zmian. Będą aktualizowane, ale z tymi samymi danymi, a zatem wynik netto nie będzie żadnych zmian.

Jasne, wolniejsze itp. Nieefektywne. Tak.

Łatwo napisać sql, utrzymać i zrozumieć? Zdecydowanie.

To kompromis do rozważenia. Działa świetnie w przypadku małych upserts. Działa świetnie dla tych, którym nie przeszkadza poświęcenie wydajności w celu utrzymania kodu.

sapbucket
źródło
Uwaga dla wszystkich: WSTAW ANI WYMIANA NIE jest tym, o czym jest ten post. WSTAW LUB WYMIEŃ utworzy NOWY wiersz w tabeli z nowym identyfikatorem. To nie jest AKTUALIZACJA.
sapbucket
-2

Po przeczytaniu tego wątku i rozczarowaniu, że nie było łatwo przejść do tego „UPSERT”, zbadałem dalej ...

Możesz to zrobić bezpośrednio i łatwo w SQLITE.

Zamiast używać: INSERT INTO

Posługiwać się: INSERT OR REPLACE INTO

To robi dokładnie to, co chcesz!

SBB
źródło
21
-1 INSERT OR REPLACEnie jest an UPSERT. Zobacz „odpowiedź” gregschloma, z jakiego powodu. Rozwiązanie Erica B faktycznie działa i wymaga pewnych pozytywnych opinii.
Dzień
-4
SELECT COUNT(*) FROM table1 WHERE id = 1;

gdyby COUNT(*) = 0

INSERT INTO table1(col1, col2, cole) VALUES(var1,var2,var3);

inaczej jeśli COUNT(*) > 0

UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;
mjb
źródło
Jest to zbyt skomplikowane, SQL potrafi sobie z tym poradzić w jednym zapytaniu
Robin Kanters