SQLite UPSERT / UPDATE OR INSERT


100

Ich muss UPSERT / INSERT ODER UPDATE für eine SQLite-Datenbank ausführen.

Es gibt den Befehl INSERT OR REPLACE, der in vielen Fällen nützlich sein kann. Wenn Sie jedoch Ihre IDs mit automatischer Inkrementierung aufgrund von Fremdschlüsseln beibehalten möchten, funktioniert dies nicht, da die Zeile gelöscht, eine neue erstellt und folglich diese neue Zeile eine neue ID hat.

Dies wäre die Tabelle:

Spieler - (Primärschlüssel auf ID, Benutzername eindeutig)

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

Antworten:


50

Dies ist eine späte Antwort. Ab SQLIte 3.24.0, das am 4. Juni 2018 veröffentlicht wurde, wird die UPSERT- Klausel nach der PostgreSQL-Syntax endlich unterstützt .

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

Hinweis: Wenn Sie eine Version von SQLite vor 3.24.0 verwenden müssen, verweisen Sie bitte auf diese Antwort unten (gepostet von mir, @MarqueIV).

Wenn Sie jedoch die Option zum Aktualisieren haben, wird Ihnen dringend empfohlen , dies zu tun, da im Gegensatz zu meiner Lösung die hier veröffentlichte das gewünschte Verhalten in einer einzigen Anweisung erzielt. Außerdem erhalten Sie alle anderen Funktionen, Verbesserungen und Fehlerbehebungen, die normalerweise mit einer neueren Version geliefert werden.


Derzeit noch keine Version im Ubuntu-Repository.
Bl79

Warum kann ich das nicht auf Android verwenden? Ich habe es versucht db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Gibt mir einen Syntaxfehler für das Wort "on"
Bastian Voigt

1
@BastianVoigt Da die auf verschiedenen Android-Versionen installierten SQLite3-Bibliotheken älter als 3.24.0 sind. Siehe: developer.android.com/reference/android/database/sqlite/… Leider benötigen Sie eine neue Funktion von SQLite3 (oder eine andere Systembibliothek) unter Android oder iOS und müssen eine bestimmte Version von SQLite in Ihrem Betriebssystem bündeln Anwendung, anstatt sich auf das installierte System zu verlassen.
Prapin

Ist dies nicht eher ein INDATE als ein UPSERT, da es zuerst das Einfügen versucht? ;)
Mark A. Donohoe

@ BastianVoigt, siehe meine Antwort unten (in der obigen Frage verlinkt), die für Versionen vor 3.24.0 gilt.
Mark A. Donohoe

105

Q & A-Stil

Nachdem ich stundenlang nachgeforscht und mit dem Problem gekämpft hatte, stellte ich fest, dass es zwei Möglichkeiten gibt, dies zu erreichen, abhängig von der Struktur Ihrer Tabelle und davon, ob Sie Fremdschlüsseleinschränkungen aktiviert haben, um die Integrität aufrechtzuerhalten. Ich möchte dies in einem sauberen Format teilen, um den Menschen, die sich möglicherweise in meiner Situation befinden, Zeit zu sparen.


Option 1: Sie können es sich leisten, die Zeile zu löschen

Mit anderen Worten, Sie haben keinen Fremdschlüssel, oder wenn Sie ihn haben, ist Ihre SQLite-Engine so konfiguriert, dass es keine Integritätsausnahmen gibt. Der Weg ist EINFÜGEN ODER ERSETZEN . Wenn Sie versuchen, einen Player einzufügen / zu aktualisieren, dessen ID bereits vorhanden ist, löscht die SQLite-Engine diese Zeile und fügt die von Ihnen bereitgestellten Daten ein. Nun stellt sich die Frage: Was tun, um die alte ID in Verbindung zu halten?

Angenommen , wir möchten UPSERT mit den Daten user_name = 'steven' und age = 32 erstellen.

Schauen Sie sich diesen Code an:

INSERT INTO players (id, name, age)

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

Der Trick ist in der Verschmelzung. Es gibt die ID des Benutzers 'steven' zurück, falls vorhanden, und andernfalls wird eine neue neue ID zurückgegeben.


Option 2: Sie können es sich nicht leisten, die Zeile zu löschen

Nachdem ich mit der vorherigen Lösung herumgespielt hatte, wurde mir klar, dass dies in meinem Fall dazu führen könnte, dass Daten zerstört werden, da diese ID als Fremdschlüssel für eine andere Tabelle fungiert. Außerdem habe ich die Tabelle mit der Klausel ON DELETE CASCADE erstellt , was bedeuten würde, dass Daten stillschweigend gelöscht würden. Gefährlich.

Also dachte ich zuerst an eine IF-Klausel, aber SQLite hat nur CASE . Und dieser CASE kann nicht verwendet werden (oder zumindest habe ich ihn nicht verwaltet), um eine UPDATE- Abfrage durchzuführen, wenn EXISTS (ID von Spielern auswählen, bei denen user_name = 'steven'), und INSERT, wenn dies nicht der Fall ist . No Go.

Und dann habe ich endlich die brutale Gewalt mit Erfolg eingesetzt. Die Logik besteht darin, für jedes UPSERT , das Sie ausführen möchten, zuerst ein INSERT ODER IGNORE auszuführen , um sicherzustellen, dass eine Zeile mit unserem Benutzer vorhanden ist, und dann ein UPDATE auszuführen Abfrage mit genau denselben Daten , die Sie einzufügen versucht haben.

Gleiche Daten wie zuvor: Benutzername = 'Steven' und Alter = 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'; 

Und das ist alles!

BEARBEITEN

Wie Andy kommentiert hat, kann der Versuch, zuerst einzufügen und dann zu aktualisieren, dazu führen, dass Auslöser häufiger als erwartet ausgelöst werden. Dies ist meiner Meinung nach kein Datensicherheitsproblem, aber es ist wahr, dass das Auslösen unnötiger Ereignisse wenig Sinn macht. Daher wäre eine verbesserte Lösung:

-- 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
Ditto ... Option 2 ist großartig. Außer, ich habe es umgekehrt gemacht: Versuchen Sie ein Update, überprüfen Sie, ob rowsAffected> 0 ist, und fügen Sie dann eine Einfügung ein.
Tom Spencer

Das ist auch ein ziemlich guter Ansatz. Der einzige kleine Nachteil ist, dass Sie nicht nur eine SQL für das "Upsert" haben.
Bgusach

2
Sie müssen den Benutzernamen in der Update-Anweisung im letzten Codebeispiel nicht zurücksetzen. Es ist genug, um das Alter festzulegen.
Serg Stetsuk

71

Hier ist ein Ansatz, bei dem das Brute-Force-Ignorieren nicht erforderlich ist. Dies würde nur funktionieren, wenn ein Schlüsselverstoß vorliegt. Dieser Weg funktioniert basierend auf jedem Bedingungen, die Sie im Update angegeben haben.

Versuche dies...

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

Wie es funktioniert

Die 'magische Sauce' wird hier Changes()in der WhereKlausel verwendet. Changes()stellt die Anzahl der Zeilen dar, die von der letzten Operation betroffen sind. In diesem Fall handelt es sich um die Aktualisierung.

Wenn im obigen Beispiel keine Änderungen gegenüber der Aktualisierung vorgenommen wurden (dh der Datensatz existiert nicht), ist Changes()= 0, sodass die WhereKlausel in der InsertAnweisung als wahr ausgewertet wird und eine neue Zeile mit den angegebenen Daten eingefügt wird.

Wenn das Update tut Update eine vorhandene Zeile, dann Changes()= 1 (oder genauer gesagt, nicht Null , wenn mehr als eine Zeile aktualisiert wurde), so dass die ‚Wo‘ -Klausel in derInsert wertet nun falsch und somit wird kein Einsatz stattfinden.

Das Schöne daran ist, dass weder Brute-Force erforderlich ist noch unnötiges Löschen und erneutes Einfügen von Daten erforderlich ist, was dazu führen kann, dass nachgeschaltete Schlüssel in Fremdschlüsselbeziehungen durcheinander gebracht werden.

Da es sich nur um eine Standardklausel handelt Where, kann sie außerdem auf allem basieren, was Sie definieren, und nicht nur auf Schlüsselverletzungen. Ebenso können Sie Changes()in Kombination mit allem, was Sie wollen / brauchen, überall dort verwenden, wo Ausdrücke erlaubt sind.


1
Können Sie der Person, die dies abgelehnt hat, bitte erklären, warum?
Mark A. Donohoe

1
Das hat bei mir super funktioniert. Ich habe diese Lösung neben all den INSERT OR REPLACE-Beispielen nirgendwo anders gesehen. Sie ist für meinen Anwendungsfall viel flexibler.
Csab

@MarqueIV und was ist, wenn zwei Elemente aktualisiert oder eingefügt werden müssen? Zum Beispiel wurde die Tanne aktualisiert und die zweite existiert nicht. In diesem Fall Changes() = 0wird false zurückgegeben und zwei Zeilen werden INSERT OR REPLACE ausführen
Andriy Antonov

Normalerweise soll ein UPSERT auf einen Datensatz reagieren. Wenn Sie sagen, dass Sie sicher wissen, dass es auf mehr als einen Datensatz wirkt, ändern Sie die Zählprüfung entsprechend.
Mark A. Donohoe

1
Warum ist das eine schlechte Sache? Und wenn sich die Daten nicht geändert haben, warum rufen Sie überhaupt UPSERTan? Trotzdem ist es gut, dass das Update, die Einstellung Changes=1oder die INSERTAnweisung fälschlicherweise ausgelöst wird, was Sie nicht möchten.
Mark A. Donohoe

25

Das Problem mit allen vorgestellten Antworten ist das völlige Fehlen der Berücksichtigung von Auslösern (und wahrscheinlich anderen Nebenwirkungen). Lösung wie

INSERT OR IGNORE ...
UPDATE ...

führt dazu, dass beide Trigger ausgeführt werden (zum Einfügen und dann zum Aktualisieren), wenn keine Zeile vorhanden ist.

Die richtige Lösung ist

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

In diesem Fall wird nur eine Anweisung ausgeführt (wenn eine Zeile vorhanden ist oder nicht).


1
Ich weiß, worauf du hinauswillst. Ich werde meine Frage aktualisieren. Ich weiß übrigens nicht, warum UPDATE OR IGNOREdas notwendig ist, da die Aktualisierung nicht abstürzt, wenn keine Zeilen gefunden werden.
Bgusach

1
Lesbarkeit? Ich kann auf einen Blick sehen, was Andys Code tut. Mit freundlichen Grüßen, ich musste eine Minute lernen, um das herauszufinden.
Brandan

Ich denke, das ist eine großartige Antwort!
Funkgesteuert

6

So haben Sie ein reines UPSERT ohne Löcher (für Programmierer), die keine eindeutigen und anderen Tasten verwenden:

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

SELECT-Änderungen () geben die Anzahl der Aktualisierungen zurück, die bei der letzten Anfrage durchgeführt wurden. Überprüfen Sie dann, ob der Rückgabewert von change () 0 ist. Führen Sie in diesem Fall Folgendes aus:

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

Dies entspricht dem, was @fiznool in seinem Kommentar vorgeschlagen hat (obwohl ich mich für seine Lösung entscheiden würde). Es ist in Ordnung und funktioniert tatsächlich gut, aber Sie haben keine eindeutige SQL-Anweisung. UPSERT, das nicht auf PK oder anderen eindeutigen Schlüsseln basiert, macht für mich wenig bis gar keinen Sinn.
Bgusach

4

Sie können Ihrer eindeutigen Einschränkung für Benutzername auch einfach eine ON CONFLICT REPLACE-Klausel hinzufügen und dann einfach INSERT entfernen, sodass SQLite überlegt, was im Falle eines Konflikts zu tun ist. Siehe: https://sqlite.org/lang_conflict.html .

Beachten Sie auch den Satz zum Löschen von Triggern: Wenn die Konfliktlösungsstrategie REPLACE Zeilen löscht, um eine Einschränkung zu erfüllen, wird das Löschen von Triggern nur dann ausgelöst, wenn rekursive Trigger aktiviert sind.


1

Option 1: Einfügen -> Aktualisieren

Wenn Sie beides vermeiden möchten changes()=0und INSERT OR IGNOREsich das Löschen der Zeile nicht leisten können, können Sie diese Logik verwenden.

Zuerst einfügen (falls nicht vorhanden) und dann aktualisieren durch Filtern mit dem eindeutigen Schlüssel .

Beispiel

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

In Bezug auf Trigger

Hinweis: Ich habe es nicht getestet, um festzustellen, welche Trigger aufgerufen werden, aber ich gehe von Folgendem aus:

wenn Zeile nicht existiert

  • VOR EINFÜGEN
  • INSERT mit STATT OF
  • Nach dem Einfügen
  • VOR DEM UPDATE
  • UPDATE mit STATT VON
  • NACH DEM UPDATE

Wenn eine Zeile vorhanden ist

  • VOR DEM UPDATE
  • UPDATE mit STATT VON
  • NACH DEM UPDATE

Option 2: Einfügen oder Ersetzen - behalten Sie Ihre eigene ID

Auf diese Weise können Sie einen einzelnen SQL-Befehl haben

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

Bearbeiten: Option 2 hinzugefügt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.