SQLite - UPSERT * nicht * INSERT oder REPLACE

535

http://en.wikipedia.org/wiki/Upsert

Fügen Sie den auf SQL Server gespeicherten Update-Prozess ein

Gibt es eine clevere Möglichkeit, dies in SQLite zu tun, an die ich nicht gedacht habe?

Grundsätzlich möchte ich drei von vier Spalten aktualisieren, wenn der Datensatz vorhanden ist. Wenn er nicht vorhanden ist, möchte ich den Datensatz mit dem Standardwert (NUL) für die vierte Spalte einfügen.

Die ID ist ein Primärschlüssel, sodass UPSERT immer nur einen Datensatz enthält.

(Ich versuche, den Overhead von SELECT zu vermeiden, um festzustellen, ob ich offensichtlich AKTUALISIEREN oder EINFÜGEN muss.)

Vorschläge?


Ich kann diese Syntax auf der SQLite-Site für TABLE CREATE nicht bestätigen. Ich habe keine Demo erstellt, um sie zu testen, aber sie scheint nicht unterstützt zu werden.

Wenn es so wäre, hätte ich drei Spalten, so dass es tatsächlich so aussehen würde:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    Blob1 BLOB ON CONFLICT REPLACE, 
    Blob2 BLOB ON CONFLICT REPLACE, 
    Blob3 BLOB 
);

aber die ersten beiden Blobs verursachen keinen Konflikt, nur die ID würde also Asusme Blob1 und Blob2 würden nicht ersetzt (wie gewünscht)


UPDATEs in SQLite, wenn das Binden von Daten eine vollständige Transaktion ist, was bedeutet, dass jede zu aktualisierende gesendete Zeile Folgendes erfordert: Prepare / Bind / Step / Finalize-Anweisungen im Gegensatz zu INSERT, die die Verwendung der Reset-Funktion ermöglichen

Das Leben eines Anweisungsobjekts sieht ungefähr so ​​aus:

  1. Erstellen Sie das Objekt mit sqlite3_prepare_v2 ()
  2. Binden Sie Werte mithilfe von sqlite3_bind_-Schnittstellen an Hostparameter.
  3. Führen Sie die SQL aus, indem Sie sqlite3_step () aufrufen.
  4. Setzen Sie die Anweisung mit sqlite3_reset () zurück, kehren Sie dann zu Schritt 2 zurück und wiederholen Sie den Vorgang.
  5. Zerstören Sie das Anweisungsobjekt mit sqlite3_finalize ().

UPDATE Ich vermute, es ist langsam im Vergleich zu INSERT, aber wie ist es im Vergleich zu SELECT mit dem Primärschlüssel?

Vielleicht sollte ich die Auswahl verwenden, um die 4. Spalte (Blob3) zu lesen, und dann REPLACE verwenden, um einen neuen Datensatz zu schreiben, der die ursprüngliche 4. Spalte mit den neuen Daten für die ersten 3 Spalten mischt?

Mike Trader
quelle
5
SQLite - UPSERT in der Vorabversion verfügbar siehe: sqlite.1065341.n5.nabble.com/…
gaurav
3
UPSERT verfügbar in Version 3.24.0 von SQLite
pablo_worker

Antworten:

854

Angenommen, drei Spalten in der Tabelle: ID, NAME, ROLLE


BAD: Dadurch werden alle Spalten eingefügt oder durch neue Werte für ID = 1 ersetzt:

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (1, 'John Foo', 'CEO');

BAD: Dadurch werden 2 der Spalten eingefügt oder ersetzt. Die NAME-Spalte wird auf NULL oder den Standardwert gesetzt:

INSERT OR REPLACE INTO Employee (id, role) 
  VALUES (1, 'code monkey');

GUT : Verwenden Sie die UPSERT-Unterstützung für SQLite On-Konfliktklauseln in SQLite! Die UPSERT-Syntax wurde SQLite mit Version 3.24.0 hinzugefügt!

UPSERT ist eine spezielle Syntaxergänzung zu INSERT, die bewirkt, dass sich INSERT als UPDATE oder No-Op verhält, wenn INSERT eine Eindeutigkeitsbeschränkung verletzen würde. UPSERT ist kein Standard-SQL. UPSERT in SQLite folgt der von PostgreSQL festgelegten Syntax.

Geben Sie hier die Bildbeschreibung ein

GUT, aber nervös: Dadurch werden 2 der Spalten aktualisiert. Wenn ID = 1 vorhanden ist, bleibt der NAME unberührt. Wenn ID = 1 nicht vorhanden ist, ist der Name der Standardname (NULL).

INSERT OR REPLACE INTO Employee (id, role, name) 
  VALUES (  1, 
            'code monkey',
            (SELECT name FROM Employee WHERE id = 1)
          );

Dadurch werden 2 der Spalten aktualisiert. Wenn ID = 1 vorhanden ist, bleibt die ROLLE unberührt. Wenn ID = 1 nicht vorhanden ist, wird die Rolle anstelle des Standardwerts auf "Benchwarmer" gesetzt.

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
Eric B.
quelle
33
+1 brillant! Die eingebettete Auswahlklausel bietet Ihnen die Flexibilität, die Standardfunktion ON CONFLICT REPLACE zu überschreiben, wenn Sie den alten und den neuen Wert für ein beliebiges Feld kombinieren / vergleichen müssen.
G__
24
Wenn der Mitarbeiter von anderen Zeilen mit kaskadierendem Löschen referenziert wird, werden die anderen Zeilen weiterhin durch Ersetzen gelöscht.
Don Reba
246
Das tut weh. SQlite benötigt UPSERT.
andig
21
Können Sie erklären, warum dadurch alle Spalten durch neue Werte für ID = 1 eingefügt oder ersetzt werden: wird in Ihrem ersten Beispiel als SCHLECHT angesehen ? Der Befehl, den Sie dort präsentieren, soll einen neuen Datensatz mit der ID 1 erstellen, John Foo und den Rollen- CEO benennen oder den Datensatz mit der ID 1, falls er bereits vorhanden ist, mit diesen Daten überschreiben (vorausgesetzt, die ID ist der Primärschlüssel). . Warum ist es schlecht, wenn genau das passiert?
ODER Mapper
10
@Cornelius: Das ist klar, aber das passiert im ersten Beispiel nicht. Das erste Beispiel soll alle Spalten zwangsweise setzen, was genau der Fall ist, unabhängig davon, ob der Datensatz eingefügt oder ersetzt wird. Warum wird das als schlecht angesehen? Die verknüpfte Antwort zeigt auch nur auf, warum etwas Schlimmes passieren kann, wenn Sie eine Teilmenge der Spalten angeben, wie in Ihrem zweiten Beispiel. Es scheint nicht auf die negativen Auswirkungen des ersten Beispiels INSERT OR REPLACEeinzugehen , während Werte für alle Spalten angegeben werden.
ODER Mapper
134

INSERT OR REPLACE entspricht NICHT "UPSERT".

Angenommen, ich habe die Tabelle Mitarbeiter mit den Feldern ID, Name und Rolle:

INSERT OR REPLACE INTO Employee ("id", "name", "role") VALUES (1, "John Foo", "CEO")
INSERT OR REPLACE INTO Employee ("id", "role") VALUES (1, "code monkey")

Boom, Sie haben den Namen der Mitarbeiternummer 1 verloren. SQLite hat ihn durch einen Standardwert ersetzt.

Die erwartete Ausgabe eines UPSERT wäre, die Rolle zu ändern und den Namen beizubehalten.

gregschlom
quelle
21
-1 von mir fürchte ich. Ja, die akzeptierte Antwort ist falsch, aber während Ihre Antwort auf das Problem hinweist, ist sie auch keine Antwort. Eine tatsächliche Antwort finden Sie in der cleveren Lösung von Eric B unter Verwendung einer eingebetteten coalesce((select ..), 'new value')Klausel. Erics Antwort braucht hier mehr Stimmen, denke ich.
Tag
22
Tatsächlich. Eric ist definitiv die beste Antwort und verdient mehr Stimmen. Abgesehen davon denke ich, dass ich durch das Aufzeigen des Problems ein wenig dazu beigetragen habe, die gute Antwort zu finden (Erics Antwort kam später und baut auf den Beispiel-SQL-Tabellen in meiner Antwort auf). Also nicht sicher, ob ich eine -1 verdiene, aber vergiss es :)
Gregschlom
6
+1, um die -1 oben auszugleichen. lol. Interessante Zeitleiste für diesen Thread. Natürlich war Ihre Antwort mehr als eine Woche vor Erics, aber Sie haben beide die Frage fast zwei Jahre nach ihrer Beantwortung beantwortet. +1 auch für Eric zum Ausarbeiten.
Rich
Hier ist eine Upsert-Bibliothek für Ruby und eine Upsert-Bibliothek für Python
Seamus Abshere
2
@QED nein, da delete + insert (was ein Ersetzen ist) 2 dml-Anweisungen sind, mit ihren eigenen Triggern zum Beispiel. Es ist nicht dasselbe wie nur eine Update-Anweisung.
Sebas
109

Die Antwort von Eric B ist OK, wenn Sie nur eine oder vielleicht zwei Spalten aus der vorhandenen Zeile beibehalten möchten. Wenn Sie viele Spalten beibehalten möchten, wird dies schnell zu umständlich.

Hier ist ein Ansatz, der sich gut auf eine beliebige Anzahl von Spalten auf beiden Seiten skalieren lässt. Zur Veranschaulichung gehe ich von folgendem Schema aus:

 CREATE TABLE page (
     id      INTEGER PRIMARY KEY,
     name    TEXT UNIQUE,
     title   TEXT,
     content TEXT,
     author  INTEGER NOT NULL REFERENCES user (id),
     ts      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );

Beachten Sie insbesondere, dass dies nameder natürliche Schlüssel der Zeile idist. Er wird nur für Fremdschlüssel verwendet. Daher muss SQLite beim Einfügen einer neuen Zeile den ID-Wert selbst auswählen. Wenn Sie jedoch eine vorhandene Zeile basierend auf ihrer aktualisieren name, möchte ich, dass sie weiterhin den alten ID-Wert hat (offensichtlich!).

Ich erreiche ein wahres UPSERTmit dem folgenden Konstrukt:

 WITH new (name, title, author) AS ( VALUES('about', 'About this site', 42) )
 INSERT OR REPLACE INTO page (id, name, title, content, author)
 SELECT old.id, new.name, new.title, old.content, new.author
 FROM new LEFT JOIN page AS old ON new.name = old.name;

Die genaue Form dieser Abfrage kann etwas variieren. Der Schlüssel ist die Verwendung INSERT SELECTmit einem linken äußeren Join, um eine vorhandene Zeile mit den neuen Werten zu verbinden.

Wenn eine Zeile zuvor nicht vorhanden war, old.idwird NULLSQLite automatisch eine ID zuweisen. Wenn jedoch bereits eine solche Zeile vorhanden war, old.idhat sie einen tatsächlichen Wert und wird wiederverwendet. Welches ist genau das, was ich wollte.

In der Tat ist dies sehr flexibel. Beachten Sie, dass die tsSpalte auf allen Seiten vollständig fehlt. Da DEFAULTSQLite einen Wert hat, wird SQLite auf jeden Fall genau das Richtige tun, sodass ich mich nicht selbst darum kümmern muss.

Sie können auch eine Spalte auf die beide newund oldSeiten und dann zB Verwendung COALESCE(new.content, old.content)im Außen SELECTzu sagen : „ Bitte geben Sie den neuen Inhalt , wenn es eine war, hält sonst den alten Inhalt“ - zB wenn Sie eine feste Abfrage verwenden und unverbindlich die neuen Werte mit Platzhaltern.

Aristoteles Pagaltzis
quelle
13
+1, funktioniert großartig, aber füge eine WHERE name = "about"Einschränkung hinzu, um die SELECT ... AS oldDinge zu beschleunigen. Wenn Sie mehr als 1 m Zeilen haben, ist dies sehr langsam.
Guter Punkt, +1 auf Ihren Kommentar. Ich werde das jedoch aus der Antwort herauslassen, da das Hinzufügen einer solchen WHEREKlausel nur die Art von Redundanz in der Abfrage erfordert, die ich zu vermeiden versuchte, als ich diesen Ansatz entwickelte. Wie immer: Wenn Sie Leistung benötigen, denormalisieren Sie in diesem Fall die Struktur der Abfrage.
Aristoteles Pagaltzis
4
Sie können das Beispiel von Aristoteles bis dahin vereinfachen, wenn Sie möchten: INSERT OR REPLACE INTO page (id, name, title, content, author) SELECT id, 'about', 'About this site', content, 42 FROM ( SELECT NULL ) LEFT JOIN ( SELECT * FROM page WHERE name = 'about' )
jcox
4
Würde dies nicht unnötig Trigger auslösen, ON DELETEwenn ein Ersetzen (dh ein Update) durchgeführt wird?
Karakuri
3
Es wird sicherlich Auslöser auslösen ON DELETE. Keine Ahnung über unnötig. Für die meisten Benutzer wäre dies wahrscheinlich unnötig, sogar unerwünscht, aber möglicherweise nicht für alle Benutzer. Ebenso für die Tatsache, dass es auch alle Zeilen mit Fremdschlüsseln in die betreffende Zeile kaskadiert - wahrscheinlich ein Problem für viele Benutzer. SQLite hat leider nichts näher an einem echten UPSERT. (Außer, um es mit einem INSTEAD OF UPDATEAbzug
vorzutäuschen
86

Wenn Sie im Allgemeinen Updates durchführen, würde ich ..

  1. Starten Sie eine Transaktion
  2. Mach das Update
  3. Überprüfen Sie die Zeilenanzahl
  4. Wenn es 0 ist, fügen Sie ein
  5. Verpflichten

Wenn Sie im Allgemeinen Beilagen machen, würde ich

  1. Starten Sie eine Transaktion
  2. Versuchen Sie es mit einer Beilage
  3. Überprüfen Sie, ob ein Primärschlüssel verletzt wurde
  4. Wenn wir einen Fehler haben, führen Sie das Update durch
  5. Verpflichten

Auf diese Weise vermeiden Sie die Auswahl und sind auf Sqlite transaktionssicher.

Sam Safran
quelle
Vielen Dank, habe gerade diese @ stackoverflow.com/questions/2717590/… gefragt . +1 von mir! =)
Alix Axel
4
Wenn Sie die Zeilenanzahl im dritten Schritt mit sqlite3_changes () überprüfen möchten, stellen Sie sicher, dass Sie das DB-Handle aus mehreren Threads nicht für Änderungen verwenden.
Linulin
3
Wäre das Folgende noch nicht weniger wortreich mit dem gleichen Effekt: 1) Wählen Sie die ID-Formulartabelle mit id = 'x' aus. 2) if (ResultSet.rows.length == 0) update table mit id = 'x';
Florin
20
Ich wünschte wirklich, INSERT OR UPDATE wäre Teil der Sprache
Florin
Dies funktionierte am besten für mich. Der Versuch, REPLACE INTO oder immer auszuführen, und das Einfügen / Aktualisieren beeinträchtigten meine Leistung, wenn ich hauptsächlich Aktualisierungen anstelle von Einfügungen durchführte.
PherricOxide
83

Diese Antwort wurde aktualisiert und daher gelten die folgenden Kommentare nicht mehr.

2018-05-18 STOP PRESSE.

UPSERT-Unterstützung in SQLite! Die UPSERT-Syntax wurde SQLite mit Version 3.24.0 hinzugefügt (ausstehend)!

UPSERT ist eine spezielle Syntaxergänzung zu INSERT, die bewirkt, dass sich INSERT als UPDATE oder No-Op verhält, wenn INSERT eine Eindeutigkeitsbeschränkung verletzen würde. UPSERT ist kein Standard-SQL. UPSERT in SQLite folgt der von PostgreSQL festgelegten Syntax.

Geben Sie hier die Bildbeschreibung ein

Alternative:

Eine andere völlig andere Art, dies zu tun, ist: In meiner Anwendung habe ich meine Zeilen-ID im Speicher auf long festgelegt. MaxValue, wenn ich die Zeile im Speicher erstelle. (MaxValue wird niemals als ID verwendet, Sie werden nicht lange genug leben ... Wenn rowID nicht dieser Wert ist, muss es sich bereits in der Datenbank befinden, benötigt also ein UPDATE, wenn es MaxValue ist, dann braucht es eine Einfügung. Dies ist nur nützlich, wenn Sie die rowIDs in Ihrer App verfolgen können.

Anthony Lambert
quelle
5
Amen. Einfach ist besser als komplex. Dies fühlt sich etwas einfacher an als die akzeptierte Antwort.
kkurian
4
Ich dachte, Sie könnten INSERT INTO ... WHERE in SQLite nicht ausführen? Dies ist ein Syntaxfehler für mich in sqlite3
Charlie Martin
5
@ CharlieMartin ist korrekt. Diese Syntax ist für SQLite ungültig - was das OP angefordert hat. Eine WHEREKlausel kann nicht an eine INSERTAnweisung angehängt werden : sqlite-insert ...
colm.anseo
4
Diese Antwort hat mich viel Zeit verloren, die Frage bezieht sich auf SQLITE und ich wusste nicht, dass INSERT WHERE in sqite nicht unterstützt wird. Bitte fügen Sie diesen Hinweis hinzu, dass es für sqlite in Ihrer Antwort ungültig ist
Miquel
3
@colminator und andere: Ich habe seine SQL-Anweisung anhand Ihres Vorschlags korrigiert.
F Lekschas
62

Mir ist klar, dass dies ein alter Thread ist, aber ich habe in letzter Zeit in sqlite3 gearbeitet und mir diese Methode ausgedacht, die meinen Anforderungen an die dynamische Generierung parametrisierter Abfragen besser entspricht:

insert or ignore into <table>(<primaryKey>, <column1>, <column2>, ...) values(<primaryKeyValue>, <value1>, <value2>, ...); 
update <table> set <column1>=<value1>, <column2>=<value2>, ... where changes()=0 and <primaryKey>=<primaryKeyValue>; 

Es sind immer noch 2 Abfragen mit einer where-Klausel im Update, aber es scheint den Trick zu tun. Ich habe auch die Vision im Kopf, dass SQLite die Update-Anweisung vollständig optimieren kann, wenn der Aufruf von change () größer als Null ist. Ob dies tatsächlich der Fall ist oder nicht, kann ich nicht wissen, aber ein Mann kann träumen, nicht wahr? ;)

Für Bonuspunkte können Sie diese Zeile anhängen, die Ihnen die ID der Zeile zurückgibt, unabhängig davon, ob es sich um eine neu eingefügte Zeile oder eine vorhandene Zeile handelt.

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
Chris Stavropoulos
quelle
+ 1. Genau das, was ich durch eine Konvertierung von SQL Server TSQL-Code ersetzen wollte. Vielen Dank. SQL, das ich zu ersetzen versuchte, war wieUpdate <statement> If @@ROWCOUNT=0 INSERT INTO <statement>
DarrenMB
14

Hier ist eine Lösung, die wirklich ein UPSERT (UPDATE oder INSERT) anstelle eines INSERT OR REPLACE ist (was in vielen Situationen anders funktioniert).

Dies funktioniert folgendermaßen:
1. Versuchen Sie zu aktualisieren, wenn ein Datensatz mit derselben ID vorhanden ist.
2. Wenn das Update keine Zeilen ( NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)) geändert hat , fügen Sie den Datensatz ein.

Es wurde also entweder ein vorhandener Datensatz aktualisiert oder eine Einfügung durchgeführt.

Das wichtige Detail besteht darin, mit der SQL-Funktion change () zu überprüfen, ob die Update-Anweisung auf vorhandene Datensätze trifft, und die Einfügeanweisung nur auszuführen, wenn sie keinen Datensatz getroffen hat.

Zu erwähnen ist, dass die Funktion "changes ()" keine Änderungen zurückgibt, die von Triggern niedrigerer Ebene ausgeführt wurden (siehe http://sqlite.org/lang_corefunc.html#changes ). Berücksichtigen Sie dies daher unbedingt.

Hier ist die SQL ...

Testupdate:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 2;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 2, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;

Testeinsatz:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 3;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 3, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;
David Liebeherr
quelle
2
Dies scheint für mich die bessere Lösung zu sein als für Eric. Sollte aber INSERT INTO Contact (Id, Name) SELECT 3, 'Bob' WHERE changes() = 0;auch funktionieren.
Bkausbk
Vielen Dank Mann, es funktioniert auch in WebSQL (mit Cordova und SQLite Plugin)
Zappescu
11

Ab Version 3.24.0 wird UPSERT von SQLite unterstützt.

Aus der Dokumentation :

UPSERT ist eine spezielle Syntaxergänzung zu INSERT, die bewirkt, dass sich INSERT als UPDATE oder No-Op verhält, wenn INSERT eine Eindeutigkeitsbeschränkung verletzen würde. UPSERT ist kein Standard-SQL. UPSERT in SQLite folgt der von PostgreSQL festgelegten Syntax. Die UPSERT-Syntax wurde SQLite mit Version 3.24.0 (ausstehend) hinzugefügt.

Ein UPSERT ist eine gewöhnliche INSERT-Anweisung, auf die die spezielle ON CONFLICT-Klausel folgt

Geben Sie hier die Bildbeschreibung ein

Bildquelle: https://www.sqlite.org/images/syntax/upsert-clause.gif

Lukasz Szozda
quelle
8

Sie können in SQLite tatsächlich ein Upsert durchführen, es sieht nur ein wenig anders aus, als Sie es gewohnt sind. Es würde ungefähr so ​​aussehen:

INSERT INTO table name (column1, column2) 
VALUES ("value12", "value2") WHERE id = 123 
ON CONFLICT DO UPDATE 
SET column1 = "value1", column2 = "value2" WHERE id = 123
Brill Pappin
quelle
5

Wenn Sie die Antwort von Aristoteles erweitern, können Sie aus einer Dummy-Singleton-Tabelle (einer Tabelle Ihrer eigenen Erstellung mit einer einzelnen Zeile) SELECT auswählen. Dies vermeidet Doppelarbeit.

Ich habe das Beispiel auch für MySQL und SQLite portierbar gehalten und eine 'date_added'-Spalte als Beispiel dafür verwendet, wie Sie eine Spalte nur beim ersten Mal festlegen können.

 REPLACE INTO page (
   id,
   name,
   title,
   content,
   author,
   date_added)
 SELECT
   old.id,
   "about",
   "About this site",
   old.content,
   42,
   IFNULL(old.date_added,"21/05/2013")
 FROM singleton
 LEFT JOIN page AS old ON old.name = "about";
Gemeinschaft
quelle
4

Der beste Ansatz, den ich kenne, ist ein Update, gefolgt von einer Einfügung. Der "Overhead einer Auswahl" ist notwendig, aber keine schreckliche Belastung, da Sie nach dem Primärschlüssel suchen, der schnell ist.

Sie sollten in der Lage sein, die folgenden Anweisungen mit Ihren Tabellen- und Feldnamen zu ändern, um das zu tun, was Sie wollen.

--first, update any matches
UPDATE DESTINATION_TABLE DT
SET
  MY_FIELD1 = (
              SELECT MY_FIELD1
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
 ,MY_FIELD2 = (
              SELECT MY_FIELD2
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
WHERE EXISTS(
            SELECT ST2.PRIMARY_KEY
            FROM
              SOURCE_TABLE ST2
             ,DESTINATION_TABLE DT2
            WHERE ST2.PRIMARY_KEY = DT2.PRIMARY_KEY
            );

--second, insert any non-matches
INSERT INTO DESTINATION_TABLE(
  MY_FIELD1
 ,MY_FIELD2
)
SELECT
  ST.MY_FIELD1
 ,NULL AS MY_FIELD2  --insert NULL into this field
FROM
  SOURCE_TABLE ST
WHERE NOT EXISTS(
                SELECT DT2.PRIMARY_KEY
                FROM DESTINATION_TABLE DT2
                WHERE DT2.PRIMARY_KEY = ST.PRIMARY_KEY
                );
JosephStyons
quelle
7
Viel zu kompliziert.
Braten
Ich denke, das ist keine gute Idee, da Sie zweimal eine Anfrage an das Datenbankmodul stellen müssen.
Ricardo da Rocha Vitor
3

Wenn jemand meine Lösung für SQLite in Cordova lesen möchte, habe ich diese generische js-Methode dank der obigen Antwort von @david erhalten.

function    addOrUpdateRecords(tableName, values, callback) {
get_columnNames(tableName, function (data) {
    var columnNames = data;
    myDb.transaction(function (transaction) {
        var query_update = "";
        var query_insert = "";
        var update_string = "UPDATE " + tableName + " SET ";
        var insert_string = "INSERT INTO " + tableName + " SELECT ";
        myDb.transaction(function (transaction) {
            // Data from the array [[data1, ... datan],[()],[()]...]:
            $.each(values, function (index1, value1) {
                var sel_str = "";
                var upd_str = "";
                var remoteid = "";
                $.each(value1, function (index2, value2) {
                    if (index2 == 0) remoteid = value2;
                    upd_str = upd_str + columnNames[index2] + "='" + value2 + "', ";
                    sel_str = sel_str + "'" + value2 + "', ";
                });
                sel_str = sel_str.substr(0, sel_str.length - 2);
                sel_str = sel_str + " WHERE NOT EXISTS(SELECT changes() AS change FROM "+tableName+" WHERE change <> 0);";
                upd_str = upd_str.substr(0, upd_str.length - 2);
                upd_str = upd_str + " WHERE remoteid = '" + remoteid + "';";                    
                query_update = update_string + upd_str;
                query_insert = insert_string + sel_str;  
                // Start transaction:
                transaction.executeSql(query_update);
                transaction.executeSql(query_insert);                    
            });
        }, function (error) {
            callback("Error: " + error);
        }, function () {
            callback("Success");
        });
    });
});
}

Nehmen Sie also zuerst die Spaltennamen mit dieser Funktion auf:

function get_columnNames(tableName, callback) {
myDb.transaction(function (transaction) {
    var query_exec = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name ='" + tableName + "'";
    transaction.executeSql(query_exec, [], function (tx, results) {
        var columnParts = results.rows.item(0).sql.replace(/^[^\(]+\(([^\)]+)\)/g, '$1').split(','); ///// RegEx
        var columnNames = [];
        for (i in columnParts) {
            if (typeof columnParts[i] === 'string')
                columnNames.push(columnParts[i].split(" ")[0]);
        };
        callback(columnNames);
    });
});
}

Erstellen Sie dann die Transaktionen programmgesteuert.

"Werte" ist ein Array, das Sie zuvor erstellen sollten und das die Zeilen darstellt, die Sie in die Tabelle einfügen oder aktualisieren möchten.

"remoteid" ist die ID, die ich als Referenz verwendet habe, da ich mit meinem Remote-Server synchronisiere.

Informationen zur Verwendung des SQLite Cordova-Plugins finden Sie unter dem offiziellen Link

Zappescu
quelle
2

Diese Methode mischt einige der anderen Methoden aus der Antwort auf diese Frage neu und beinhaltet die Verwendung von CTE (Common Table Expressions). Ich werde die Abfrage einführen und dann erklären, warum ich das getan habe, was ich getan habe.

Ich möchte den Nachnamen für Mitarbeiter 300 in DAVIS ändern, wenn es einen Mitarbeiter 300 gibt. Andernfalls füge ich einen neuen Mitarbeiter hinzu.

Tabellenname: Mitarbeiter Spalten: ID, Vorname, Nachname

Die Abfrage lautet:

INSERT OR REPLACE INTO employees (employee_id, first_name, last_name)
WITH registered_employees AS ( --CTE for checking if the row exists or not
    SELECT --this is needed to ensure that the null row comes second
        *
    FROM (
        SELECT --an existing row
            *
        FROM
            employees
        WHERE
            employee_id = '300'

        UNION

        SELECT --a dummy row if the original cannot be found
            NULL AS employee_id,
            NULL AS first_name,
            NULL AS last_name
    )
    ORDER BY
        employee_id IS NULL --we want nulls to be last
    LIMIT 1 --we only want one row from this statement
)
SELECT --this is where you provide defaults for what you would like to insert
    registered_employees.employee_id, --if this is null the SQLite default will be used
    COALESCE(registered_employees.first_name, 'SALLY'),
    'DAVIS'
FROM
    registered_employees
;

Grundsätzlich habe ich den CTE verwendet, um die Häufigkeit zu reduzieren, mit der die select-Anweisung zur Ermittlung der Standardwerte verwendet werden muss. Da dies ein CTE ist, wählen wir einfach die gewünschten Spalten aus der Tabelle aus und die INSERT-Anweisung verwendet diese.

Jetzt können Sie entscheiden, welche Standardeinstellungen Sie verwenden möchten, indem Sie die Nullen in der COALESCE-Funktion durch die Werte ersetzen.

Dodzi Dzakuma
quelle
2

Nach Aristoteles Pagaltzis und der Idee COALESCEvon Eric B's Antwort ist es hier eine Upsert-Option, nur wenige Spalten zu aktualisieren oder eine vollständige Zeile einzufügen, wenn sie nicht existiert.

Stellen Sie sich in diesem Fall vor, dass Titel und Inhalt aktualisiert werden sollten, wobei die anderen alten Werte beibehalten werden, wenn sie vorhanden sind, und die angegebenen Werte eingefügt werden, wenn der Name nicht gefunden wird:

HINWEIS id muss NULL sein, wenn INSERTes sich um eine automatische Inkrementierung handelt. Wenn es sich nur um einen generierten Primärschlüssel handelt, COALESCEkann dieser auch verwendet werden (siehe Kommentar von Aristoteles Pagaltzis ).

WITH new (id, name, title, content, author)
     AS ( VALUES(100, 'about', 'About this site', 'Whatever new content here', 42) )
INSERT OR REPLACE INTO page (id, name, title, content, author)
SELECT
     old.id, COALESCE(old.name, new.name),
     new.title, new.content,
     COALESCE(old.author, new.author)
FROM new LEFT JOIN page AS old ON new.name = old.name;

Die allgemeine Regel wäre also, wenn Sie alte Werte COALESCEbeibehalten möchten , verwenden Sie , wenn Sie Werte aktualisieren möchten, verwenden Sienew.fieldname

Miquel
quelle
COALESCE(old.id, new.id)ist definitiv falsch mit einem automatisch inkrementierenden Schlüssel. Und während "den größten Teil der Zeile unverändert lassen, außer wenn Werte fehlen" wie ein Anwendungsfall klingt, den jemand tatsächlich haben könnte, denke ich nicht, dass die Leute danach suchen, wenn sie nach einer UPSERT-Methode suchen.
Aristoteles Pagaltzis
@AristotlePagaltzis entschuldige mich, wenn ich falsch liege, ich verwende keine automatischen Inkremente. Was ich bei dieser Abfrage suche, ist, nur wenige Spalten zu aktualisieren oder eine vollständige Zeile mit den angegebenen Werten einzufügen, falls diese nicht vorhanden ist . Ich habe mit Ihrer Abfrage gespielt und konnte sie beim Einfügen nicht erreichen: Die aus der oldTabelle ausgewählten Spalten wurden zugewiesen NULL, nicht den angegebenen Werten new. Dies ist der Grund zu verwenden COALESCE. Ich bin kein Experte für SQLite, ich habe diese Abfrage getestet und scheint für den Fall zu funktionieren. Ich würde Ihnen vielmals danken, wenn Sie mich auf die Lösung mit automatischen Inkrementen hinweisen könnten
Miquel
2
Bei einem automatisch inkrementierenden Schlüssel möchten Sie NULLals Schlüssel einfügen , da dies SQLite anweist, stattdessen den nächsten verfügbaren Wert einzufügen.
Aristoteles Pagaltzis
Ich sage nicht, dass Sie nicht tun sollten, was Sie tun (wenn Sie es brauchen, dann brauchen Sie es), nur dass es hier keine wirkliche Antwort auf die Frage ist. UPSERT bedeutet im Allgemeinen, dass Sie eine Zeile haben, die in der Tabelle gespeichert werden soll, und nur nicht wissen, ob Sie bereits eine übereinstimmende Zeile haben, in die die Werte eingefügt werden sollen, oder ob Sie sie als neue Zeile einfügen müssen. Ihr Anwendungsfall ist, dass Sie die meisten Werte aus der neuen Zeile ignorieren möchten, wenn Sie bereits eine übereinstimmende Zeile haben . Das ist in Ordnung, es ist einfach nicht die Frage, die gestellt wurde.
Aristoteles Pagaltzis
1

Ich denke, dies könnte das sein, wonach Sie suchen: ON CONFLICT-Klausel .

Wenn Sie Ihre Tabelle folgendermaßen definieren:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    field1 TEXT 
); 

Wenn Sie jetzt ein INSERT mit einer bereits vorhandenen ID ausführen, führt SQLite automatisch UPDATE anstelle von INSERT durch.

Hth ...

kmelvn
quelle
6
Ich denke nicht, dass dies funktioniert, es wird die Spalten löschen, die in der Einfügeanweisung fehlen
Sam Saffron
3
@ Mosor: -1 von mir, sorry. Dies entspricht der Abgabe einer REPLACEErklärung.
Alix Axel
7
-1, da dies ein Löschen und dann ein Einfügen ausführt, wenn der Primärschlüssel bereits vorhanden ist.
Braten
1

Wenn es Ihnen nichts ausmacht, dies in zwei Operationen zu tun.

Schritte:

1) Fügen Sie neue Elemente mit "EINFÜGEN ODER IGNORIEREN" hinzu

2) Aktualisieren Sie vorhandene Elemente mit "UPDATE"

Die Eingabe für beide Schritte ist dieselbe Sammlung neuer oder aktualisierbarer Elemente. Funktioniert gut mit vorhandenen Elementen, die keine Änderungen benötigen. Sie werden aktualisiert, jedoch mit denselben Daten, und daher ändert sich das Nettoergebnis nicht.

Sicher, langsamer usw. Ineffizient. Ja.

Einfach die SQL zu schreiben und zu pflegen und zu verstehen? Bestimmt.

Es ist ein Kompromiss. Funktioniert hervorragend für kleine Aufsätze. Funktioniert hervorragend für diejenigen, denen es nichts ausmacht, die Effizienz für die Wartbarkeit des Codes zu beeinträchtigen.

Sapbucket
quelle
Hinweis für alle: INSERT OR REPLACE ist NICHT das, worum es in diesem Beitrag geht. INSERT OR REPLACE erstellt eine NEUE Zeile in Ihrer Tabelle mit einer neuen ID. Das ist kein UPDATE.
Sapbucket
-2

Nachdem ich gerade diesen Thread gelesen hatte und enttäuscht war, dass es nicht einfach war, nur zu diesem "UPSERT" zu gehen, untersuchte ich weiter ...

Sie können dies direkt und einfach in SQLITE tun.

Anstatt zu verwenden: INSERT INTO

Verwenden: INSERT OR REPLACE INTO

Dies macht genau das, was Sie wollen!

SBB
quelle
21
-1 INSERT OR REPLACEist kein UPSERT. Siehe Gregschloms "Antwort" für den Grund warum. Die Lösung von Eric B funktioniert tatsächlich und benötigt einige positive Stimmen.
Tag
-4
SELECT COUNT(*) FROM table1 WHERE id = 1;

wenn COUNT(*) = 0

INSERT INTO table1(col1, col2, cole) VALUES(var1,var2,var3);

sonst wenn COUNT(*) > 0

UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;
mjb
quelle
Dies ist viel zu kompliziert, SQL kann dies in einer Abfrage
problemlos verarbeiten