INSERT … ON CONFLICT (upsert) în SQLite

Inserare și modificare SQLite 3.24.0

Inserează un rând sau, dacă cheia există deja, actualizează rândul existent (ori nu face nimic), într-o singură comandă.

Sintaxă
INSERT INTO tabel (coloane) VALUES (...)
  ON CONFLICT (coloana_unica) DO UPDATE SET coloana = excluded.coloana [WHERE ...];
INSERT INTO tabel (coloane) VALUES (...)
  ON CONFLICT [(coloana_unica)] DO NOTHING;

Ce face INSERT ... ON CONFLICT (upsert)

Un „upsert” este un INSERT care știe ce să facă atunci când rândul există deja. Clauza ON CONFLICT se activează când inserarea ar încălca o constrângere PRIMARY KEY sau UNIQUE. În loc de eroare, SQLite face ce scrii după DO: DO UPDATE SET ... modifică rândul existent, iar DO NOTHING ignoră rândul nou.

În DO UPDATE, numele de coloane simple se referă la rândul care există deja în tabel, iar excluded.coloana este valoarea pe care ai încercat să o inserezi. Așa poți aduna cantități, păstra cea mai nouă dată sau număra vizite. Un WHERE după SET face actualizarea doar dacă o condiție este adevărată.

Sintaxa este aceeași ca în PostgreSQL. Din SQLite 3.35.0 poți scrie mai multe clauze ON CONFLICT în aceeași comandă (pentru constrângeri diferite), iar la ultima clauză coloana țintă poate lipsi. Spre deosebire de INSERT OR REPLACE, upsert-ul nu șterge rândul vechi: coloanele pe care nu le pomenești în SET rămân neschimbate.

Parametri

Parametru Descriere
(coloana_unica) Coloana (sau coloanele) cu PRIMARY KEY sau UNIQUE care poate produce conflictul. Obligatorie pentru DO UPDATE, cu excepția ultimei clauze din 3.35.0.
DO UPDATE SET ... Modifică rândul existent. excluded.x este valoarea din rândul care nu a putut fi inserat.
DO NOTHING Rândul nou este ignorat, fără eroare.
WHERE (după SET) Opțional: actualizarea se face doar pentru rândurile care îndeplinesc condiția.

Exemple

Exemplele de pe această pagină pornesc de la datele de mai jos. Rulează mai întâi acest cod, apoi exemplul.

CREATE TABLE stoc (
  produs     TEXT PRIMARY KEY,
  cantitate  INTEGER NOT NULL,
  actualizat TEXT
);
INSERT INTO stoc VALUES
  ('mere', 10, '2026-10-20'),
  ('pere',  4, '2026-10-20');

Exemplul 1: Adună cantitatea dacă produsul există deja

„mere” exista, deci cantitatea devine 10 + 5 = 15 și data se schimbă. „prune” nu exista, deci a fost inserat normal. cantitate este valoarea din tabel, excluded.cantitate este valoarea nouă.

INSERT INTO stoc VALUES ('mere', 5, '2026-10-25'), ('prune', 7, '2026-10-25')
  ON CONFLICT (produs) DO UPDATE SET
    cantitate  = cantitate + excluded.cantitate,
    actualizat = excluded.actualizat;

SELECT * FROM stoc ORDER BY produs;

Rezultat

╭────────┬───────────┬────────────╮
│ produs │ cantitate │ actualizat │
╞════════╪═══════════╪════════════╡
│ mere   │        15 │ 2026-10-25 │
│ pere   │         4 │ 2026-10-20 │
│ prune  │         7 │ 2026-10-25 │
╰────────┴───────────┴────────────╯

Exemplul 2: DO NOTHING: păstrează rândul existent

Rândul pentru „pere” există, așa că inserarea este ignorată fără eroare. changes() arată câte rânduri a modificat ultima comandă: 0.

INSERT INTO stoc VALUES ('pere', 99, '2026-10-25')
  ON CONFLICT DO NOTHING;
SELECT changes() AS modificate;

SELECT * FROM stoc ORDER BY produs;

Rezultat

╭────────────╮
│ modificate │
╞════════════╡
│          0 │
╰────────────╯
╭────────┬───────────┬────────────╮
│ produs │ cantitate │ actualizat │
╞════════╪═══════════╪════════════╡
│ mere   │        10 │ 2026-10-20 │
│ pere   │         4 │ 2026-10-20 │
╰────────┴───────────┴────────────╯

Exemplul 3: Contor de vizite și actualizare condiționată

Primul tabel numără vizitele pe pagină: prima inserare creează rândul, următoarele cresc numărul. În al doilea caz, WHERE face ca data să fie înlocuită doar dacă cea nouă este mai recentă, deci rândul pentru „mere” rămâne cu data din 20 octombrie.

CREATE TABLE vizite (pagina TEXT PRIMARY KEY, numar INTEGER NOT NULL);
INSERT INTO vizite VALUES ('/acasa', 1) ON CONFLICT (pagina) DO UPDATE SET numar = numar + 1;
INSERT INTO vizite VALUES ('/acasa', 1) ON CONFLICT (pagina) DO UPDATE SET numar = numar + 1;
INSERT INTO vizite VALUES ('/contact', 1) ON CONFLICT (pagina) DO UPDATE SET numar = numar + 1;
INSERT INTO vizite VALUES ('/acasa', 1) ON CONFLICT (pagina) DO UPDATE SET numar = numar + 1;
SELECT * FROM vizite ORDER BY pagina;

INSERT INTO stoc VALUES ('mere', 3, '2026-10-01')
  ON CONFLICT (produs) DO UPDATE SET
    cantitate  = excluded.cantitate,
    actualizat = excluded.actualizat
  WHERE excluded.actualizat > stoc.actualizat;
SELECT * FROM stoc WHERE produs = 'mere';

Rezultat

╭──────────┬───────╮
│  pagina  │ numar │
╞══════════╪═══════╡
│ /acasa   │     3 │
│ /contact │     1 │
╰──────────┴───────╯
╭────────┬───────────┬────────────╮
│ produs │ cantitate │ actualizat │
╞════════╪═══════════╪════════════╡
│ mere   │        10 │ 2026-10-20 │
╰────────┴───────────┴────────────╯

Exemplul 4: Coloana din ON CONFLICT trebuie să fie unică

Aici b nu are UNIQUE și nici PRIMARY KEY, așa că SQLite refuză comanda încă de la citire.

CREATE TABLE t (a INTEGER, b INTEGER);
INSERT INTO t VALUES (1, 2) ON CONFLICT (b) DO NOTHING;

Rezultat (eroare)

Parse error near line 2: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint

Greșeli frecvente

  • Folosești INSERT OR REPLACE când vrei de fapt un upsert. REPLACE șterge rândul vechi și inserează unul nou: coloanele pe care nu le dai devin NULL sau valoarea implicită, iar declanșatorii de ștergere pot rula.
  • Scrii INSERT INTO t SELECT ... FROM x ON CONFLICT ... fără WHERE în SELECT. Parserul poate confunda ON cu condiția unui JOIN; documentația cere să adaugi măcar WHERE true în SELECT.
  • Uiți că un nume simplu de coloană din SET înseamnă valoarea veche. SET cantitate = cantitate nu schimbă nimic; valoarea nouă este excluded.cantitate.

Comenzi înrudite

  • INSERT OR IGNORE / OR REPLACE: Alege ce face SQLite când un INSERT sau UPDATE încalcă o constrângere: oprește comanda, sare peste rând sau înlocuiește rândul vechi.
  • RETURNING: Face ca INSERT, UPDATE sau DELETE să returneze rândurile modificate, de exemplu id-ul generat, fără un SELECT separat.
  • CREATE TRIGGER: Rulează automat instrucțiuni SQL când un rând este inserat, modificat sau șters: jurnal de modificări, validări, câmpuri calculate.
  • CREATE TABLE: Creează un tabel nou. În SQLite tipul coloanei este opțional și doar sugerează cum se salvează valorile.