SQLite UPSERT / UPDATE LUB INSERT

103

Muszę wykonać UPSERT / INSERT LUB UPDATE w bazie danych SQLite.

Istnieje polecenie WSTAW LUB ZAMIEŃ, które w wielu przypadkach może być przydatne. Ale jeśli chcesz zachować swój identyfikator z autoincrement na miejscu z powodu kluczy obcych, to nie działa, ponieważ usuwa wiersz, tworzy nowy, aw konsekwencji nowy wiersz ma nowy identyfikator.

To byłaby tabela:

gracze - (klucz główny na id, unikalna nazwa_użytkownika)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
bgusach
źródło

Odpowiedzi:

52

To jest późna odpowiedź. Począwszy od wersji SQLIte 3.24.0, wydanej 4 czerwca 2018 r., Wreszcie dostępna jest obsługa klauzuli UPSERT zgodnie ze składnią PostgreSQL.

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

Uwaga: dla tych, którzy muszą używać wersji SQLite wcześniejszej niż 3.24.0, prosimy o odniesienie się do tej odpowiedzi poniżej (opublikowanej przeze mnie, @MarqueIV).

Jeśli jednak masz możliwość uaktualnienia, zdecydowanie zachęcam do zrobienia tego, ponieważ w przeciwieństwie do mojego rozwiązania, tutaj zamieszczone osiąga pożądane zachowanie w jednym oświadczeniu. Dodatkowo otrzymujesz wszystkie inne funkcje, ulepszenia i poprawki błędów, które zwykle są dostarczane w nowszej wersji.

prapin
źródło
Na razie nie ma tego wydania w repozytorium Ubuntu.
bl79
Dlaczego nie mogę tego używać na Androidzie? Próbowałem db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Daje mi błąd składniowy w słowie „wł.”
Bastian Voigt,
1
@BastianVoigt Ponieważ biblioteki SQLite3 zainstalowane w różnych wersjach Androida są starsze niż 3.24.0. Zobacz: developer.android.com/reference/android/database/sqlite/ ... Niestety, potrzebujesz nowej funkcji SQLite3 (lub dowolnej innej biblioteki systemowej) na Androida lub iOS, musisz dołączyć określoną wersję SQLite do swojego aplikacja zamiast polegać na zainstalowanym systemie.
prapin
Czy nie jest to bardziej INDATE niż UPSERT, ponieważ najpierw próbuje wstawić? ;)
Mark A. Donohoe
@BastianVoigt, zobacz moją odpowiedź poniżej (link w powyższym pytaniu), która dotyczy wersji wcześniejszych niż 3.24.0.
Mark A. Donohoe
106

Styl pytań i odpowiedzi

Cóż, po godzinach badań i walki z tym problemem, dowiedziałem się, że są dwa sposoby na osiągnięcie tego, w zależności od struktury twojego stołu i czy masz włączone ograniczenia kluczy obcych, aby zachować integralność. Chciałbym podzielić się tym w przejrzystym formacie, aby zaoszczędzić trochę czasu ludziom, którzy mogą być w mojej sytuacji.


Opcja 1: Możesz sobie pozwolić na usunięcie wiersza

Innymi słowy, nie masz klucza obcego lub jeśli go masz, silnik SQLite jest skonfigurowany tak, aby nie było żadnych wyjątków integralności. Najlepszą drogą jest WSTAWIĆ LUB WYMIENIĆ . Jeśli próbujesz wstawić / zaktualizować odtwarzacz, którego identyfikator już istnieje, silnik SQLite usunie ten wiersz i wstawi podane przez Ciebie dane. Teraz pojawia się pytanie: co zrobić, aby stary identyfikator był powiązany?

Powiedzmy, że chcemy UPSERT z danymi user_name = 'steven' i age = 32.

Spójrz na ten kod:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

Sztuczka polega na połączeniu. Zwraca identyfikator użytkownika „steven”, jeśli taki istnieje, w przeciwnym razie zwraca nowy, świeży identyfikator.


Opcja 2: Nie możesz sobie pozwolić na usunięcie wiersza

Po małpowaniu z poprzednim rozwiązaniem zdałem sobie sprawę, że w moim przypadku może to skończyć się zniszczeniem danych, ponieważ ten identyfikator działa jako klucz obcy dla innej tabeli. Poza tym stworzyłem tabelę z klauzulą ON DELETE CASCADE , co oznaczałoby, że po cichu usuwałoby dane. Niebezpieczny.

Tak więc najpierw pomyślałem o klauzuli IF, ale SQLite ma tylko CASE . A tego CASE nie można użyć (a przynajmniej mi się to nie udało) do wykonania jednej AKTUALIZACJI zapytania jeśli ISTNIEJE (wybierz identyfikator z graczy, gdzie nazwa_użytkownika = 'steven') i WSTAWIĆ, jeśli nie. Nie idź.

A potem, w końcu, z powodzeniem użyłem brutalnej siły. Logika jest taka, że ​​dla każdego UPSERT , który chcesz wykonać, najpierw wykonaj INSERT LUB IGNORUJ aby upewnić się, że jest wiersz z naszym użytkownikiem, a następnie wykonaj zapytanie UPDATE z dokładnie tymi samymi danymi, które próbujesz wstawić.

Te same dane co poprzednio: user_name = 'steven' i age = 32.

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

I to wszystko!

EDYTOWAĆ

Jak skomentował Andy, próba wstawienia najpierw, a następnie aktualizacji może prowadzić do wyzwalania wyzwalaczy częściej niż oczekiwano. Nie jest to moim zdaniem kwestia bezpieczeństwa danych, ale prawdą jest, że odpalanie niepotrzebnych zdarzeń ma niewielki sens. Dlatego ulepszonym rozwiązaniem byłoby:

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 
bgusach
źródło
10
Tak samo ... opcja 2 jest świetna. Z wyjątkiem tego, że zrobiłem to na odwrót: spróbuj zaktualizować, sprawdź, czy rowsAffected> 0, jeśli nie, zrób wstawkę.
Tom Spencer,
To też całkiem dobre podejście, jedyną małą wadą jest to, że nie masz tylko jednego kodu SQL dla „upsert”.
bgusach
2
nie trzeba ponownie ustawiać nazwa_użytkownika w instrukcji aktualizacji w ostatnim przykładzie kodu. Wystarczy ustawić wiek.
Serg Stetsuk
72

Oto podejście, które nie wymaga brutalnej siły „ignoruj”, która działałaby tylko w przypadku naruszenia klucza. W ten sposób działa w oparciu o wszelkie warunki określone w aktualizacji.

Spróbuj tego...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

Jak to działa

„Magiczny sos” jest używany Changes()w Whereklauzuli. Changes()reprezentuje liczbę wierszy, na które ma wpływ ostatnia operacja, którą w tym przypadku jest aktualizacja.

W powyższym przykładzie, jeśli nie ma zmian od aktualizacji (tj. Rekord nie istnieje), to Changes()= 0, więc Whereklauzula w Insertinstrukcji przyjmuje wartość prawda i wstawiany jest nowy wiersz z określonymi danymi.

Jeśli Update nie aktualizacji istniejącego wiersza, a Changes()= 1 (albo dokładniej, nie zero, jeśli więcej niż jeden rząd został zaktualizowany), więc „gdzie” klauzulaInsert teraz ma wartość false, a tym samym nie odbędzie się wkładka.

Piękno tego polega na tym, że nie jest potrzebna żadna brutalna siła, ani niepotrzebne usuwanie, a następnie ponowne wstawianie danych, co może spowodować zepsucie kolejnych kluczy w relacjach kluczy obcych.

Ponadto, ponieważ jest to tylko standardowa Whereklauzula, może opierać się na wszystkim, co zdefiniujesz, a nie tylko na kluczowych naruszeniach. Podobnie możesz używać Changes()w połączeniu z czymkolwiek innym, czego chcesz / potrzebujesz, wszędzie tam, gdzie dozwolone są wyrażenia.

Mark A. Donohoe
źródło
1
To działało świetnie dla mnie. Nie widziałem tego rozwiązania nigdzie indziej, obok wszystkich przykładów WSTAW LUB WYMIEN, jest znacznie bardziej elastyczne w moim przypadku użycia.
csab
@MarqueIV a co w przypadku, gdy trzeba zaktualizować lub wstawić dwie pozycje? na przykład pierwsza została zaktualizowana, a druga nie istnieje. w takim przypadku Changes() = 0zwróci false i dwa wiersze spowodują WSTAWIENIE LUB WYMIENIENIE
Andriy Antonov
Zwykle UPSERT ma działać na jednym rekordzie. Jeśli twierdzisz, że wiesz na pewno, że działa on na więcej niż jednym rekordzie, zmień odpowiednio licznik.
Mark A. Donohoe
Złe jest to, że jeśli wiersz istnieje, metoda aktualizacji musi zostać wykonana niezależnie od tego, czy wiersz się zmienił, czy nie.
Jimi
1
Dlaczego to jest zła rzecz? A jeśli dane się nie zmieniły, dlaczego dzwonisz UPSERTw pierwszej kolejności? Ale mimo to dobrze się stało, że aktualizacja się wydarzy, ustawienie Changes=1lub w przeciwnym razie INSERTinstrukcja zostanie nieprawidłowo uruchomiona, czego nie chcesz.
Mark A. Donohoe
25

Problem ze wszystkimi przedstawionymi odpowiedziami polega na całkowitym braku uwzględnienia wyzwalaczy (i prawdopodobnie innych skutków ubocznych). Rozwiązanie jak

INSERT OR IGNORE ...
UPDATE ...

prowadzi do wykonania obu wyzwalaczy (do wstawienia, a następnie do aktualizacji), gdy wiersz nie istnieje.

Właściwym rozwiązaniem jest

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

w takim przypadku wykonywana jest tylko jedna instrukcja (jeśli wiersz istnieje lub nie).

Andy
źródło
1
Rozumiem co masz na myśli. Zaktualizuję moje pytanie. Nawiasem mówiąc, nie wiem, dlaczego UPDATE OR IGNOREjest to konieczne, ponieważ aktualizacja nie ulegnie awarii, jeśli nie zostaną znalezione żadne wiersze.
bgusach
1
czytelność? W mgnieniu oka widzę, co robi kod Andy'ego. Pozdrawiam bgusach Musiałem się chwilę uczyć, żeby się domyślić.
Brandan
6

Aby mieć czysty UPSERT bez dziur (dla programistów), które nie polegają na unikalnych i innych kluczach:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

Funkcja SELECT changes () zwróci liczbę aktualizacji wykonanych w ostatnim zapytaniu. Następnie sprawdź, czy wartość zwracana przez changes () wynosi 0, jeśli tak, wykonaj:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
Gilco
źródło
Jest to odpowiednik tego, co @fiznool zaproponował w swoim komentarzu (chociaż wybrałbym jego rozwiązanie). Wszystko w porządku i faktycznie działa dobrze, ale nie masz unikalnej instrukcji SQL. UPSERT nie oparty na PK lub innych unikalnych kluczach nie ma dla mnie sensu.
bgusach
4

Możesz także po prostu dodać klauzulę ON CONFLICT REPLACE do unikalnego ograniczenia nazwa_użytkownika, a następnie po prostu WSTAWIĆ, pozostawiając SQLite, aby dowiedzieć się, co zrobić w przypadku konfliktu. Zobacz: https://sqlite.org/lang_conflict.html .

Zwróć także uwagę na zdanie dotyczące wyzwalaczy usuwania: gdy strategia rozwiązywania konfliktów REPLACE usuwa wiersze w celu spełnienia ograniczenia, wyzwalacze usuwania są uruchamiane wtedy i tylko wtedy, gdy są włączone wyzwalacze rekurencyjne.

Maksymiliana Tyrtanii
źródło
1

Opcja 1: Wstaw -> Aktualizuj

Jeśli chcesz uniknąć obu changes()=0iINSERT OR IGNORE nawet jeśli nie możesz sobie pozwolić na usunięcie wiersza - możesz użyć tej logiki;

Najpierw włóż (jeśli nie istnieje), a następnie zaktualizuj , filtrując za pomocą unikalnego klucza.

Przykład

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

Odnośnie wyzwalaczy

Uwaga: nie testowałem tego, aby zobaczyć, które wyzwalacze są wywoływane, ale zakładam że:

jeśli wiersz nie istnieje

  • PRZED WSTAWIENIEM
  • INSERT przy użyciu INSTEAD OF
  • PO WKŁADZIE
  • PRZED AKTUALIZACJĄ
  • UPDATE using INSTEAD OF
  • PO AKTUALIZACJI

jeśli wiersz istnieje

  • PRZED AKTUALIZACJĄ
  • UPDATE using INSTEAD OF
  • PO AKTUALIZACJI

Opcja 2: Włóż lub wymień - zachowaj własny identyfikator

w ten sposób możesz mieć jedno polecenie SQL

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

Edycja: dodana opcja 2.

itsho
źródło