SQLite UPSERT / UPDATE O INSERT


103

Devo eseguire UPSERT / INSERT O UPDATE su un database SQLite.

C'è il comando INSERT OR REPLACE che in molti casi può essere utile. Ma se vuoi mantenere i tuoi ID con autoincremento in posizione a causa di chiavi esterne, non funziona poiché elimina la riga, ne crea una nuova e di conseguenza questa nuova riga ha un nuovo ID.

Questa sarebbe la tabella:

giocatori - (chiave primaria su ID, nome_utente univoco)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |

Risposte:


52

Questa è una risposta tardiva. A partire da SQLIte 3.24.0, rilasciato il 4 giugno 2018, è finalmente disponibile un supporto per la clausola UPSERT seguendo la sintassi PostgreSQL.

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

Nota: per coloro che devono utilizzare una versione di SQLite precedente alla 3.24.0, fare riferimento a questa risposta di seguito (pubblicata da me, @MarqueIV).

Tuttavia, se hai la possibilità di eseguire l'aggiornamento, sei fortemente incoraggiato a farlo poiché, a differenza della mia soluzione, quella pubblicata qui ottiene il comportamento desiderato in una singola istruzione. Inoltre ottieni tutte le altre funzionalità, miglioramenti e correzioni di bug che di solito vengono fornite con una versione più recente.


Per ora, non ancora questa versione nel repository di Ubuntu.
bl79

Perché non posso usarlo su Android? Ho provato db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Mi dà un errore di sintassi sulla parola "on"
Bastian Voigt

1
@BastianVoigt Perché le librerie SQLite3 installate su varie versioni di Android sono precedenti alla 3.24.0. Vedi: developer.android.com/reference/android/database/sqlite/… Purtroppo, se hai bisogno di una nuova funzionalità di SQLite3 (o qualsiasi altra libreria di sistema) su Android o iOS, devi raggruppare una versione specifica di SQLite nel tuo l'applicazione invece di affidarsi a quella installata dal sistema.
prapin

Piuttosto che UPSERT, non è più un INDATE dato che prova prima l'inserimento? ;)
Mark A. Donohoe

@BastianVoigt, vedi la mia risposta sotto (collegata alla domanda sopra) che è per le versioni precedenti alla 3.24.0.
Mark A. Donohoe

106

Stile di domande e risposte

Bene, dopo aver ricercato e combattuto il problema per ore, ho scoperto che ci sono due modi per farlo, a seconda della struttura della tua tabella e se hai attivato le restrizioni per le chiavi esterne per mantenere l'integrità. Mi piacerebbe condividerlo in un formato pulito per risparmiare tempo alle persone che potrebbero trovarsi nella mia situazione.


Opzione 1: puoi permetterti di eliminare la riga

In altre parole, non hai una chiave esterna o, se le hai, il tuo motore SQLite è configurato in modo che non ci siano eccezioni di integrità. La strada da percorrere è INSERIRE O SOSTITUIRE . Se stai cercando di inserire / aggiornare un giocatore il cui ID esiste già, il motore SQLite eliminerà quella riga e inserirà i dati che fornisci. Ora viene la domanda: cosa fare per mantenere associato il vecchio ID?

Diciamo che vogliamo UPSERT con i dati user_name = 'steven' e age = 32.

Guarda questo codice:

INSERT INTO players (id, name, age)

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

Il trucco sta nella fusione. Restituisce l'id dell'utente "steven", se presente, e in caso contrario, restituisce un nuovo ID nuovo.


Opzione 2: non puoi permetterti di eliminare la riga

Dopo aver cercato la soluzione precedente, mi sono reso conto che nel mio caso ciò potrebbe finire per distruggere i dati, poiché questo ID funziona come una chiave esterna per un'altra tabella. Inoltre, ho creato la tabella con la clausola ON DELETE CASCADE , il che significherebbe che cancellerebbe i dati in silenzio. Pericoloso.

Quindi, prima ho pensato a una clausola IF, ma SQLite ha solo CASE . E questo CASE non può essere utilizzato (o almeno non l'ho gestito) per eseguire una query UPDATE se ESISTE (seleziona l'id dai giocatori dove user_name = 'steven') e INSERT se non lo è. No go.

E poi, finalmente, ho usato la forza bruta, con successo. La logica è, per ogni UPSERT che si desidera eseguire, eseguire prima un INSERT OR IGNORE per assicurarsi che ci sia una riga con il nostro utente, quindi eseguire una query UPDATE con esattamente gli stessi dati che si è tentato di inserire.

Stessi dati di prima: nome_utente = 'steven' ed età = 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'; 

E questo è tutto!

MODIFICARE

Come ha commentato Andy, il tentativo di inserire prima e poi aggiornare può portare ad attivare trigger più spesso del previsto. Questo non è a mio parere un problema di sicurezza dei dati, ma è vero che sparare eventi non necessari ha poco senso. Pertanto, una soluzione migliore sarebbe:

-- 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); 

10
Idem ... l'opzione 2 è fantastica. Tranne che l'ho fatto al contrario: prova un aggiornamento, controlla se rowsAffected> 0, in caso contrario esegui un inserimento.
Tom Spencer

Anche questo è un approccio abbastanza buono, l'unico piccolo inconveniente è che non hai un solo SQL per "upsert".
bgusach

2
non è necessario reimpostare nome_utente nell'istruzione di aggiornamento nell'ultimo esempio di codice. È sufficiente impostare l'età.
Serg Stetsuk

72

Ecco un approccio che non richiede la forza bruta "ignora" che funzionerebbe solo se ci fosse una violazione chiave. In questo modo Funziona sulla base di tutte le condizioni specificate nell'aggiornamento.

Prova questo...

-- 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);

Come funziona

La "salsa magica" qui sta usando Changes()nella Whereclausola. Changes()rappresenta il numero di righe interessate dall'ultima operazione, che in questo caso è l'aggiornamento.

Nell'esempio precedente, se non ci sono modifiche dall'aggiornamento (cioè il record non esiste), allora Changes()= 0 quindi la Whereclausola Insertnell'istruzione restituisce true e viene inserita una nuova riga con i dati specificati.

Se Update ha aggiornato una riga esistente, allora Changes()= 1 (o più precisamente, non zero se è stata aggiornata più di una riga), quindi la clausola "Where" in Insertnow restituisce false e quindi non avrà luogo alcun inserimento.

Il bello di questo è che non è necessaria la forza bruta, né l'eliminazione inutile, quindi il reinserimento dei dati che potrebbero causare confusione nelle chiavi a valle nelle relazioni di chiave esterna.

Inoltre, poiché è solo una Whereclausola standard , può essere basata su qualsiasi cosa tu definisca, non solo sulle violazioni chiave. Allo stesso modo, puoi usare Changes()in combinazione con qualsiasi altra cosa tu voglia / necessiti ovunque siano consentite espressioni.


1
Questo ha funzionato benissimo per me. Non ho visto questa soluzione da nessun'altra parte insieme a tutti gli esempi INSERT OR REPLACE, è molto più flessibile per il mio caso d'uso.
CSAB

@MarqueIV e se ci sono due elementi che devono essere aggiornati o inseriti? ad esempio, il primo è stato aggiornato e il secondo non esiste. in tal caso Changes() = 0restituirà false e due righe faranno INSERT OR REPLACE
Andriy Antonov

Di solito un UPSERT dovrebbe agire su un record. Se stai dicendo di sapere per certo che sta agendo su più di un record, modifica di conseguenza il controllo del conteggio.
Mark A. Donohoe

La cosa negativa è che se la riga esiste, il metodo di aggiornamento deve essere eseguito indipendentemente dal fatto che la riga sia cambiata o meno.
Jimi

1
Perché è una brutta cosa? E se i dati non sono cambiati, perché stai chiamando UPSERTin primo luogo? Ma anche così, è una buona cosa che l'aggiornamento avvenga, impostando Changes=1o altrimenti l' INSERTistruzione si attiva in modo errato, cosa che non vuoi che accada .
Mark A. Donohoe

25

Il problema con tutte le risposte presentate è la completa mancanza di prendere in considerazione i trigger (e probabilmente altri effetti collaterali). Soluzione come

INSERT OR IGNORE ...
UPDATE ...

porta ad entrambi i trigger eseguiti (per l'inserimento e poi per l'aggiornamento) quando la riga non esiste.

La soluzione corretta è

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

in tal caso viene eseguita una sola istruzione (quando la riga esiste o meno).


1
Capisco il tuo punto. Aggiornerò la mia domanda. A proposito, non so perché UPDATE OR IGNOREè necessario, poiché l'aggiornamento non si arresta in modo anomalo se non vengono trovate righe.
bgusach

1
leggibilità? Riesco a vedere cosa sta facendo il codice di Andy a colpo d'occhio. Il tuo bgusach ho dovuto studiare un minuto per capirlo.
Brandan

6

Per avere un UPSERT puro senza buchi (per programmatori) che non si basano su chiavi univoche e di altro tipo:

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

SELEZIONA modifiche () restituirà il numero di aggiornamenti effettuati nell'ultima richiesta. Quindi controlla se il valore di ritorno da changes () è 0, in tal caso esegui:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 

Ciò equivale a ciò che @fiznool ha proposto nel suo commento (anche se sceglierei la sua soluzione). Va tutto bene e funziona davvero bene, ma non hai un'istruzione SQL univoca. UPSERT non basato su PK o altre chiavi univoche ha poco o nessun senso per me.
bgusach

4

Puoi anche aggiungere una clausola ON CONFLICT REPLACE al tuo vincolo univoco user_name e poi semplicemente INSERT, lasciando a SQLite il compito di capire cosa fare in caso di conflitto. Vedi: https://sqlite.org/lang_conflict.html .

Notare anche la frase relativa ai trigger di eliminazione: quando la strategia di risoluzione dei conflitti REPLACE elimina righe per soddisfare un vincolo, i trigger di eliminazione vengono attivati ​​se e solo se sono abilitati i trigger ricorsivi.


1

Opzione 1: Inserisci -> Aggiorna

Se desideri evitare entrambi changes()=0e INSERT OR IGNOREanche se non puoi permetterti di eliminare la riga, puoi utilizzare questa logica;

Innanzitutto, inserisci (se non esiste) e quindi aggiorna filtrando con la chiave univoca.

Esempio

-- 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';

Per quanto riguarda i trigger

Avviso: non l'ho testato per vedere quali trigger vengono chiamati, ma presumo quanto segue:

se la riga non esiste

  • PRIMA DI INSERIRE
  • INSERISCI usando INVECE DI
  • DOPO L'INSERIMENTO
  • PRIMA DELL'AGGIORNAMENTO
  • AGGIORNA utilizzando INVECE DI
  • DOPO L'AGGIORNAMENTO

se la riga esiste

  • PRIMA DELL'AGGIORNAMENTO
  • AGGIORNA utilizzando INVECE DI
  • DOPO L'AGGIORNAMENTO

Opzione 2: inserisci o sostituisci: mantieni il tuo ID

in questo modo puoi avere un solo comando 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);

Modifica: aggiunta opzione 2.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.