Menu

UPSERT w SQLite: ON CONFLICT DO UPDATE i DO NOTHING

Jak działa UPSERT w SQLite: klauzula ON CONFLICT, tabela excluded, DO NOTHING vs DO UPDATE i czym to się różni od INSERT OR REPLACE.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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 ograniczeniem UNIQUE albo PRIMARY 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. (Albo DO 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.column odnosi 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, żeby col była PRIMARY KEY albo miała ograniczenie UNIQUE. W przeciwnym razie SQLite zgłasza błąd „no such constraint”.
  • DO UPDATE nie 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.
  • excluded jest tylko do odczytu. Możesz z niej czytać, ale nie możesz do niej pisać. Celem SET jest 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ