web-dev-qa-db-fra.com

SQLite - UPSERT * not * INSERT ou REPLACE

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

Insert Update proc stocké sur SQL Server

Existe-t-il un moyen intelligent de faire cela dans SQLite auquel je n’ai pas pensé?

Fondamentalement, je veux mettre à jour trois colonnes sur quatre si l'enregistrement existe. S'il n'existe pas, je souhaite INSÉRER l'enregistrement avec la valeur par défaut (NUL) pour la quatrième colonne.

L'ID étant une clé primaire, il n'y aura jamais qu'un seul enregistrement dans UPSERT.

(J'essaie d'éviter les frais généraux liés à SELECT afin de déterminer si j'ai besoin de mettre à jour ou d'insérer de manière évidente)

Suggestions?


Je ne peux pas confirmer cette syntaxe sur le site SQLite pour TABLE CREATE. Je n'ai pas construit de démo pour le tester, mais cela ne semble pas être pris en charge.

Si c’était le cas, j’aurais trois colonnes pour que cela ressemble à:

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

mais les deux premiers blobs ne causeront pas de conflit, seul l'identifiant le serait. Ainsi, je n'aurais pas remplacé Blob1 et Blob2 (comme souhaité).


UPDATEs dans SQLite lorsque les données de liaison constituent une transaction complète, ce qui signifie que chaque ligne envoyée à mettre à jour requiert: des instructions Prepare/Bind/Step/Finalize contrairement à la commande INSERT qui permet d'utiliser la fonction de réinitialisation

La vie d'un objet de déclaration ressemble à ceci:

  1. Créez l'objet à l'aide de sqlite3_prepare_v2 ()
  2. Liez les valeurs aux paramètres de l'hôte à l'aide d'interfaces sqlite3_bind_.
  3. Exécutez le SQL en appelant sqlite3_step ()
  4. Réinitialisez l'instruction à l'aide de sqlite3_reset (), puis revenez à l'étape 2 et recommencez.
  5. Détruisez l'objet d'instruction à l'aide de sqlite3_finalize ().

UPDATE Je suppose que c'est lent par rapport à INSERT, mais comment cela se compare-t-il à SELECT en utilisant la clé primaire?

Peut-être devrais-je utiliser l'option de sélection pour lire la 4ème colonne (Blob3), puis utiliser REPLACE pour écrire un nouvel enregistrement fusionnant la 4ème colonne d'origine avec les nouvelles données des 3 premières colonnes?

501
Mike Trader

En supposant 3 colonnes dans la table. ID, NOM, ROLE


BAD: Ceci insérera ou remplacera toutes les colonnes avec de nouvelles valeurs pour ID = 1:

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

BAD: Ceci insère ou remplace 2 des colonnes ... la colonne NAME sera définie sur NULL ou la valeur par défaut:

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

GOOD: Ceci mettra à jour 2 des colonnes. Lorsque ID = 1 existe, le nom ne sera pas affecté. Lorsque ID = 1 n'existe pas, le nom sera par défaut (NULL).

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

Cela mettra à jour 2 des colonnes. Lorsque ID = 1 existe, le ROLE ne sera pas affecté. Lorsque ID = 1 n'existe pas, le rôle sera défini sur 'Benchwarmer' au lieu de la valeur par défaut.

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );
821
Eric B

INSERT OR REPLACE est NOT équivalent à "UPSERT".

Disons que j'ai la table Employee avec les champs id, name et role:

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, vous avez perdu le nom du numéro d'employé 1. SQLite l'a remplacé par une valeur par défaut.

Le résultat attendu d'un UPSERT serait de changer le rôle et de conserver le nom.

125
gregschlom

La réponse d’Eric B est acceptable si vous souhaitez conserver une ou deux colonnes de la ligne existante. Si vous souhaitez conserver un grand nombre de colonnes, la procédure devient trop lourde.

Voici une approche qui s'adapte à n'importe quel nombre de colonnes de chaque côté. Pour illustrer cela, supposons le schéma suivant:

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

Notez en particulier que name est la clé naturelle de la ligne - id n'est utilisé que pour les clés étrangères. Il est donc important que SQLite sélectionne elle-même la valeur de l'ID lors de l'insertion d'une nouvelle ligne. Mais lors de la mise à jour d'une ligne existante en fonction de sa name, je souhaite qu'elle conserve l'ancien ID (évidemment!).

J'obtiens un vrai UPSERT avec la construction suivante:

 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;

La forme exacte de cette requête peut varier un peu. La clé est l'utilisation de INSERT SELECT avec une jointure externe gauche, pour joindre une ligne existante aux nouvelles valeurs.

Ici, si une ligne n'existait pas auparavant, old.id sera NULL et SQLite attribuera alors automatiquement un ID, mais s'il existait déjà une telle ligne, old.id aurait une valeur réelle. et ce sera réutilisé. Ce qui est exactement ce que je voulais.

En fait, c'est très flexible. Notez que la colonne ts est complètement absente de tous les côtés - comme elle a une valeur DEFAULT, SQLite fera juste ce qu'il faut dans tous les cas, je n'ai donc pas à m'en occuper moi-même. .

Vous pouvez également inclure une colonne sur les deux côtés new et old, puis utiliser par ex. COALESCE(new.content, old.content) à l'extérieur SELECT pour dire "insérez le nouveau contenu s'il y en a, sinon conservez l'ancien contenu" - p. Ex. si vous utilisez une requête fixe et liez les nouvelles valeurs avec des espaces réservés.

106
Aristotle Pagaltzis

Si vous faites généralement des mises à jour, je ..

  1. Commencer une transaction
  2. Faire la mise à jour
  3. Vérifiez le nombre de lignes
  4. Si c'est 0 faire l'insert
  5. Commettre

Si vous faites généralement des inserts je voudrais

  1. Commencer une transaction
  2. Essayez un insert
  3. Vérifier l'erreur de violation de clé primaire
  4. si nous avons une erreur faire la mise à jour
  5. Commettre

De cette façon, vous évitez la sélection et vous avez une sonorité transactionnelle sur Sqlite.

80
Sam Saffron

Cette réponse a été mise à jour et les commentaires ci-dessous ne s'appliquent plus.

2018-05-18 STOP PRESS.

Prise en charge UPSERT dans SQLite! La syntaxe UPSERT a été ajoutée à SQLite avec la version 3.24.0 (en attente)!

UPSERT est un ajout de syntaxe spécial à INSERT qui fait en sorte que l'INSERT se comporte comme un UPDATE ou un no-op si l'INSERT enfreignait une contrainte d'unicité. UPSERT n'est pas un SQL standard. UPSERT dans SQLite suit la syntaxe établie par PostgreSQL.

enter image description here

alternativement:

Une autre façon complètement différente de procéder est la suivante: dans mon application, je mets long dans la mémoire ID.MaxValue lorsque je crée la ligne en mémoire. (MaxValue ne sera jamais utilisé en tant qu'ID, vous ne vivrez pas assez longtemps ... Alors si rowID n'est pas cette valeur, il doit déjà être dans la base de données, il faut donc un UPDATE s'il s'agit de MaxValue, puis un insert. Cela n'est utile que si vous pouvez suivre les ID de ligne dans votre application.

71
AnthonyLambert

Je réalise que c’est un vieux fil de discussion, mais je travaille depuis peu avec sqlite3 et je propose cette méthode qui répond mieux à mes besoins en matière de génération dynamique de requêtes paramétrées:

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

Il reste encore 2 requêtes avec une clause where sur la mise à jour mais semble faire l'affaire. J'ai aussi en tête cette vision que sqlite peut optimiser complètement la déclaration de mise à jour si l'appel à changes () est supérieur à zéro. Que ce soit ou non, cela dépasse mes connaissances, mais un homme peut rêver, n'est-ce pas? ;)

Pour les points bonus, vous pouvez ajouter cette ligne qui vous renvoie l'identifiant de la ligne, qu'il s'agisse d'une nouvelle ligne insérée ou d'une ligne existante.

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;
59
Chris Stavropoulos

Voici une solution qui est vraiment un UPSERT (UPDATE ou INSERT) au lieu d'un INSERT OR REPLACE (qui fonctionne différemment dans de nombreuses situations).

Cela fonctionne comme ceci:
1. Essayez de mettre à jour si un enregistrement avec le même Id existe.
2. Si la mise à jour n'a modifié aucune ligne (NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)), insérez l'enregistrement.

Donc, un enregistrement existant a été mis à jour ou une insertion sera effectuée.

Le détail important est d'utiliser la fonction SQL changes () pour vérifier si l'instruction de mise à jour touche des enregistrements existants et n'exécute l'instruction d'insertion que si elle n'a touché aucun enregistrement.

Une chose à mentionner est que la fonction changes () ne renvoie pas les modifications effectuées par les déclencheurs de niveau inférieur (voir http://sqlite.org/lang_corefunc.html#changes ), assurez-vous donc de prendre cela en compte.

Voici le SQL ...

Test de mise à jour:

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

Insert de test:

--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;
13
David Liebeherr

Depuis la version 3.24.0, UPSERT est pris en charge par SQLite.

De la documentation :

UPSERT est une addition de syntaxe spéciale à INSERT qui fait en sorte que l'INSERT se comporte comme un UPDATE ou un no-op si l'INSERT enfreignait une contrainte d'unicité. UPSERT n'est pas un SQL standard. UPSERT dans SQLite respecte la syntaxe établie par PostgreSQL. La syntaxe UPSERT a été ajoutée à SQLite avec la version 3.24.0 (en attente).

Un UPSERT est une instruction INSERT ordinaire suivie de la clause spéciale ON CONFLICT

enter image description here

Source de l'image: https://www.sqlite.org/images/syntax/upsert-clause.gif

7
Lukasz Szozda

Vous pouvez en effet faire un upsert dans SQLite, ça a juste un peu différent de ce que vous avez l'habitude de faire Cela ressemblerait à quelque chose comme:

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

En développant réponse d’Aristote vous pouvez SÉLECTIONNER à partir d’une fausse table 'singleton' (une table de votre propre création avec une seule ligne). Cela évite certaines duplications.

J'ai également conservé l'exemple portable dans MySQL et SQLite et utilisé une colonne 'date_added' comme exemple de la manière dont vous pouvez définir une colonne uniquement la première fois.

 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";
5
user2403761

La meilleure approche que je connaisse consiste à faire une mise à jour, suivie d'un insert. Les "frais généraux d'une sélection" sont nécessaires, mais ce n'est pas un fardeau énorme, car vous effectuez une recherche sur la clé primaire, ce qui est rapide.

Vous devriez pouvoir modifier les instructions ci-dessous avec vos noms de table et de champ pour faire ce que vous voulez.

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

Si quelqu'un veut lire ma solution pour SQLite dans Cordova, j'ai eu cette méthode générique js grâce à la réponse @david ci-dessus.

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

Alors, commencez par choisir les noms de colonnes avec cette fonction:

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

Ensuite, générez les transactions par programme.

"Values" est un tableau que vous devez construire avant et qui représente les lignes que vous souhaitez insérer ou mettre à jour dans la table.

"remoteid" est l'identifiant que j'ai utilisé comme référence car je synchronise avec mon serveur distant.

Pour l'utilisation du plugin SQLite Cordova, veuillez vous référer au site officiel lien

3
Zappescu

Après Aristotle Pagaltzis et l’idée de COALESCE à partir de réponse d’Eric B , ici c’est une option de réversion qui permet de ne mettre à jour que quelques colonnes ou d’insérer une ligne complète. exister.

Dans ce cas, imaginez que le titre et le contenu doivent être mis à jour, en conservant les anciennes valeurs existantes et en insérant les valeurs fournies lorsque le nom est introuvable:

NOTEid est forcé d'être NULL lorsque INSERT car il est supposé être auto-incrémenté. S'il ne s'agit que d'une clé primaire générée, alors COALESCE peut également être utilisé (voir commentaire d'Aristotle 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;

La règle générale serait donc que si vous voulez conserver les anciennes valeurs, utilisez COALESCE, lorsque vous souhaitez mettre à jour les valeurs, utilisez new.fieldname

1
Miquel

Je pense que c'est peut-être ce que vous recherchez: clause ON CONFLICT .

Si vous définissez votre table comme ceci:

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

Maintenant, si vous faites un INSERT avec un identifiant qui existe déjà, SQLite effectue automatiquement UPDATE au lieu de INSERT.

Hth ...

1
kmelvn

Cette méthode remixe quelques unes des autres méthodes de answer pour cette question et incorpore l'utilisation de CTE (Common Table Expressions). Je vais présenter la requête, puis expliquer pourquoi j'ai fait ce que j'ai fait.

J'aimerais changer le nom de famille de l'employé 300 en DAVIS s'il y a un employé 300. Sinon, j'ajouterai un nouvel employé.

Nom de la table: employés Colonnes: id, prénom, nom

La requête est:

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
;

Fondamentalement, j'ai utilisé le CTE pour réduire le nombre de fois où l'instruction select doit être utilisée pour déterminer les valeurs par défaut. Comme il s'agit d'un CTE, nous sélectionnons simplement les colonnes que nous voulons dans la table et l'instruction INSERT l'utilise.

Vous pouvez maintenant choisir les valeurs par défaut que vous souhaitez utiliser en remplaçant les valeurs NULL, dans la fonction COALESCE, par ce que les valeurs doivent être.

1
Dodzi Dzakuma