Comment mettre à jour une ligne dans une table ou l'INSÉRER si elle n'existe pas?


83

J'ai le tableau de compteurs suivant:

CREATE TABLE cache (
    key text PRIMARY KEY,
    generation int
);

Je voudrais incrémenter l'un des compteurs ou le mettre à zéro si la ligne correspondante n'existe pas encore. Existe-t-il un moyen de le faire sans problèmes de concurrence dans SQL standard? L'opération fait parfois partie d'une transaction, parfois séparée.

Le SQL doit s'exécuter sans modification sur SQLite, PostgreSQL et MySQL, si possible.

Une recherche a donné plusieurs idées qui souffrent de problèmes de concurrence ou sont spécifiques à une base de données:

  • Essayez INSERTune nouvelle ligne et UPDATEs'il y a eu une erreur. Malheureusement, l'erreur sur INSERTabandonne la transaction en cours.

  • UPDATEla ligne, et si aucune ligne n'a été modifiée, INSERTune nouvelle ligne.

  • MySQL a une ON DUPLICATE KEY UPDATEclause.

EDIT: Merci pour toutes les bonnes réponses. Il semble que Paul a raison, et il n'y a pas un seul moyen portable de le faire. C'est assez surprenant pour moi, car cela ressemble à une opération très basique.


6
Vous n'allez pas trouver une solution unique qui fonctionne pour tous ces SGBDR. Pardon.
Paul Tomblin


Réponses:


137

MySQL (et ensuite SQLite) prennent également en charge la syntaxe REPLACE INTO:

REPLACE INTO my_table (pk_id, col1) VALUES (5, '123');

Cela identifie automatiquement la clé primaire et trouve une ligne correspondante à mettre à jour, en insérant une nouvelle si aucune n'est trouvée.


13
En fait, pour être précis, REPLACE de MySQL fait toujours une insertion, mais il supprimera d'abord la ligne si elle existe déjà. dev.mysql.com/doc/refman/4.1/en/replace.html
Evan

90
Il est important de comprendre qu'il s'agit d'une insertion + suppression et jamais et mise à jour. La conséquence de ceci est que vous voudrez toujours vous assurer que lorsque vous effectuez un remplacement, vous devez toujours inclure des données pour tous les champs.
Zoredache

2
@Zoredache Techniquement, c'est un «supprimer, puis insérer», car un (n) «insérer + supprimer» est en fait la même chose qu'une suppression mais c'est couper les cheveux.
Agi Hammerthief

2
@Agihammerthief il y a une différence très réelle à savoir que la ligne nouvellement insérée n'aura PAS la même clé primaire que la ligne qui a été supprimée. Avec ON DUPLICATE, ce sera la même clé primaire (sauf si vous la modifiez spécifiquement).
Tim Strijdhorst

32

SQLite prend en charge le remplacement d'une ligne si elle existe déjà:

INSERT OR REPLACE INTO [...blah...]

Vous pouvez raccourcir ceci en

REPLACE INTO [...blah...]

Ce raccourci a été ajouté pour être compatible avec l' REPLACE INTOexpression MySQL .


Cela nécessite que vous définissiez un PRAMARY KEYdans vos valeurs.
DawnSong

24

Je ferais quelque chose comme ce qui suit:

INSERT INTO cache VALUES (key, generation)
ON DUPLICATE KEY UPDATE (key = key, generation = generation + 1);

Définition de la valeur de génération à 0 dans le code ou dans le sql mais en utilisant ON DUP ... pour incrémenter la valeur. Je pense que c'est la syntaxe de toute façon.


1
Cette réponse devrait honnêtement être la réponse choisie. C'est une modification non destructive si vous ne soumettez pas tous les champs à une entrée.
Jordanie

9

la clause ON DUPLICATE KEY UPDATE est la meilleure solution car: REPLACE effectue un DELETE suivi d'un INSERT donc pendant une période très courte, l'enregistrement est supprimé, ce qui crée la très faible possibilité qu'une requête puisse revenir après avoir ignoré cela si la page était visualisé lors de la requête REMPLACER.

Je préfère INSÉRER ... SUR LA MISE À JOUR DUPLICATE ... pour cette raison.

La solution de jmoz est la meilleure: bien que je préfère la syntaxe SET aux parenthèses

INSERT INTO cache 
SET key = 'key', generation = 'generation'
ON DUPLICATE KEY 
UPDATE key = 'key', generation = (generation + 1)
;

4
REPLACE est atomique, il n'y a donc pas de période où la ligne n'existe pas.
Brilliand


5

Dans PostgreSQL il n'y a pas de commande de fusion, et en fait l'écrire n'est pas trivial - il y a en fait des cas de bord étranges qui rendent la tâche "intéressante".

La meilleure approche (comme dans: travailler dans les conditions les plus possibles) est d'utiliser une fonction - telle que celle illustrée dans le manuel (merge_db).

Si vous ne souhaitez pas utiliser la fonction, vous pouvez généralement vous en sortir avec:

updated = db.execute(UPDATE ... RETURNING 1)
if (!updated)
  db.execute(INSERT...)

Rappelez - vous qu'il est pas faute de preuve et il va échouer par la suite.


4

Le SQL standard fournit l'instruction MERGE pour cette tâche. Tous les SGBD ne prennent pas en charge l'instruction MERGE.


0

Si vous n'avez pas de moyen commun de mettre à jour ou d'insérer atomiquement (par exemple, via une transaction), vous pouvez revenir à un autre schéma de verrouillage. Un fichier de 0 octet, un mutex système, un tube nommé, etc ...


0

Pourriez-vous utiliser un déclencheur d'insertion? En cas d'échec, effectuez une mise à jour.


Trigger (au moins dans PostgreSQL) est en cours d'exécution lorsque la commande a fonctionné. c'est à dire que vous ne pouvez pas avoir de déclencheur qui s'exécute lorsque la commande de base a échoué.

0

Si vous êtes d'accord avec l'utilisation d'une bibliothèque qui écrit le SQL pour vous, vous pouvez utiliser Upsert (actuellement Ruby et Python uniquement):

Pet.upsert({:name => 'Jerry'}, :breed => 'beagle')
Pet.upsert({:name => 'Jerry'}, :color => 'brown')

Cela fonctionne avec MySQL, Postgres et SQLite3.

Il écrit une procédure stockée ou une fonction définie par l'utilisateur (UDF) dans MySQL et Postgres. Il utilise INSERT OR REPLACEdans SQLite3.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.