Wstaw albo zaktualizuj, jeśli już istnieje
Częsta potrzeba: wstawić wiersz, ale jeśli wiersz z tym samym kluczem już istnieje, zaktualizować go. Bez UPSERT trzeba najpierw wykonać SELECT, a potem rozgałęzić się na INSERT albo UPDATE: dwa odwołania do bazy i wyścig między nimi.
UPSERT w SQLite robi to jedną instrukcją:
Za pierwszym uruchomieniem wiersz zostaje wstawiony. Uruchom to ponownie z inną ceną i tym samym sku, a istniejący wiersz zostanie zaktualizowany na miejscu. Bez duplikatu, bez błędu.
Budowa ON CONFLICT
Pełny schemat:
INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;
Liczą się trzy elementy:
conflict_target: kolumna lub kolumny z ograniczeniemUNIQUEalboPRIMARY KEY, na których spodziewasz się kolizji. SQLite używa tego, żeby wybrać indeks do obserwowania.DO UPDATE SET ...: co zmienić w istniejącym wierszu, gdy dojdzie do kolizji. (AlboDO NOTHING, żeby po cichu pominąć.)- Opcjonalne
WHERE: dodatkowy warunek, który musi być prawdziwy, żeby aktualizacja faktycznie się wykonała.
Cel konfliktu musi odpowiadać prawdziwemu ograniczeniu unikalności. ON CONFLICT(price) się nie skompiluje, jeśli price nie jest unikalne: SQLite nie ma względem czego wykrywać konfliktu.
DO NOTHING: wstaw, jeśli brak, w przeciwnym razie pomiń
Prostszy wariant. Przydaje się, gdy zasilasz bazę danymi albo zapisujesz zdarzenia, a duplikaty mają być po cichu ignorowane:
Drugie wstawienie trafia na ten sam event_id i normalnie zgłosiłoby UNIQUE constraint failed. Z DO NOTHING SQLite po prostu je pomija. Bez wyjątku, bez zmienionych wierszy.
To „idempotentne wstawienie”, do którego ludzie często używają INSERT OR IGNORE. DO NOTHING z UPSERT robi to samo i lepiej współgra z klauzulami WHERE i RETURNING.
Pseudotabela excluded
Gdy dochodzi do konfliktu, nagle w grze są dwa wiersze: istniejący w tabeli i nowy, który próbujesz wstawić. SQLite daje sposób, żeby mówić o obu.
- Same nazwy kolumn (
price,name) odnoszą się do istniejącego wiersza. excluded.columnodnosi się do przychodzącego wiersza, który został odrzucony.
quantity = quantity + excluded.quantity czytaj jako „istniejąca ilość plus nowa”. Po dwóch wstawieniach A-100 ma ilość 8. Ten wzorzec, czyli sumowanie w istniejącym wierszu, to jedna z najbardziej przydatnych sztuczek UPSERT.
Warunkowy UPSERT z WHERE
Końcowe WHERE pozwala pominąć aktualizację, jeśli jakiś warunek nie jest spełniony. Jest sprawdzane względem istniejącego wiersza (i może odwoływać się do excluded.* dla przychodzącego):
Nowy wiersz ma starsze updated_at, więc WHERE jest fałszywe i aktualizacja zostaje pominięta. Istniejący wiersz zachowuje nowszą cenę. Zamień daty, a aktualizacja się wykona. To standardowy wzorzec „nadpisuj tylko świeższymi danymi”.
Upsert wielu wierszy
VALUES może zawierać wiele wierszy, a ON CONFLICT działa dla każdego z nich niezależnie:
A-100 koliduje i zostaje zaktualizowany. A-200 i A-300 są nowe i zostają wstawione. Jedna instrukcja, mieszany wynik: wstawienia i aktualizacje. To czysty sposób na synchronizację paczki rekordów z zewnętrznego źródła.
UPSERT vs INSERT OR REPLACE
INSERT OR REPLACE wygląda, jakby robił to samo. Nie robi.
notes zniknęło. INSERT OR REPLACE całkowicie usunął wiersz 1 i wstawił nowy: każda kolumna, której nie wymieniono, została ustawiona na NULL albo wartość domyślną. Odpala też wyzwalacze DELETE i uruchamia kaskady przez klucze obce z ON DELETE.
UPSERT zachowuje wiersz:
notes wciąż tam jest. Zmieniły się tylko kolumny wymienione w SET. Domyślnie sięgaj po UPSERT; po INSERT OR REPLACE tylko wtedy, gdy naprawdę chcesz semantyki „usuń i wstaw od nowa”.
Wiele celów konfliktu
Jeśli wiersz może kolidować na więcej niż jednym ograniczeniu, możesz połączyć kilka klauzul ON CONFLICT:
Wygrywa to ograniczenie, które zadziała pierwsze, i wykonuje się DO UPDATE z tej gałęzi. W praktyce większość tabel ma jeden oczywisty cel konfliktu, czyli klucz główny albo jedną unikalną kolumnę, więc rzadko potrzebujesz więcej niż jednej klauzuli.
Typowe pułapki
Kilka rzeczy, które dają ludziom w kość:
- Brak pasującego indeksu unikalnego, brak UPSERT.
ON CONFLICT(col)wymaga, żebycolbyłaPRIMARY KEYalbo miała ograniczenieUNIQUE. W przeciwnym razie SQLite zgłasza błąd „no such constraint”. DO UPDATEnie działa, jeśli nie ma konfliktu. To alternatywa dla wstawienia, a nie dodatkowe zachowanie. Gdy klucz pojawia się pierwszy raz, wykonuje się tylko wstawienie.excludedjest tylko do odczytu. Możesz z niej czytać, ale nie możesz do niej pisać. CelemSETjest zawsze istniejący wiersz.- Generowane rowid dla
INTEGER PRIMARY KEY. Jeśli nie podasz id, każde wstawienie dostaje nowe, więc nie ma z czym kolidować. UPSERT ma sens tylko wtedy, gdy kolidująca kolumna ma deterministyczną wartość podaną przez wywołującego.
Dalej: RETURNING
UPSERT nie mówi, które wiersze zostały wstawione, a które zaktualizowane, ani jak wyglądają ich końcowe wartości. Do tego służy klauzula RETURNING: zwraca zmienione wiersze w tej samej instrukcji, bez dodatkowego SELECT. O tym dalej.
Najczęściej zadawane pytania
Czym jest UPSERT w SQLite?
UPSERT to INSERT, który zamienia się w UPDATE (albo w nic), gdy w przeciwnym razie naruszyłby ograniczenie UNIQUE lub PRIMARY KEY. Zapisujesz go jako INSERT ... ON CONFLICT(column) DO UPDATE SET ... albo DO NOTHING. SQLite obsługuje go od wersji 3.24.0 (2018).
Czym jest tabela excluded w UPSERT w SQLite?
excluded to specjalna pseudotabela, która przechowuje wiersz, który próbujesz wstawić. Wewnątrz DO UPDATE SET ... do istniejącego wiersza odwołujesz się przez nazwę kolumny, a do odrzuconego przez excluded.column. Dlatego SET price = excluded.price oznacza „nadpisz cenę tym, co przyniósł nowy INSERT”.
Czym różni się INSERT OR REPLACE od UPSERT?
INSERT OR REPLACE usuwa kolidujący wiersz i wstawia nowy: to odpala wyzwalacze DELETE, uruchamia kaskady kluczy obcych z ON DELETE CASCADE i przywraca wartości domyślne we wszystkich kolumnach. UPSERT aktualizuje istniejący wiersz na miejscu, więc zmieniają się tylko kolumny wymienione w SET. Wybieraj UPSERT, chyba że naprawdę chcesz usunąć wiersz i wstawić go od nowa.
Czy w SQLite można zrobić upsert wielu wierszy naraz?
Tak. INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(col) DO UPDATE SET ... działa bez problemu. Każdy wiersz jest sprawdzany osobno względem celu konfliktu, a wiersz excluded wewnątrz DO UPDATE odnosi się do tego przychodzącego wiersza, który wywołał konflikt.