Comment «insérer s'il n'existe pas» dans MySQL?

838

J'ai commencé par googler et j'ai trouvé cet article qui parle des tables mutex.

J'ai une table avec environ 14 millions d'enregistrements. Si je veux ajouter plus de données dans le même format, existe-t-il un moyen de s'assurer que l'enregistrement que je veux insérer n'existe pas déjà sans utiliser une paire de requêtes (c'est-à-dire, une requête à vérifier et une à insérer est l'ensemble de résultats est vide)?

Est-ce qu'une uniquecontrainte sur un champ garantit l' insertéchec s'il est déjà là?

Il semble qu'avec une simple contrainte, lorsque j'émets l'insert via php, le script coasse.

garenne
la source
Voir stackoverflow.com/questions/44550788/… pour une discussion sur la non-gravure des valeurs auto_inc.
Rick James
@RickJames - c'est un q intéressant mais pas sûr qu'il soit directement lié à ce q :)
warren
1
Elle a été mentionnée dans un commentaire et, selon une autre question, cette question était un "double exact". J'ai donc pensé que c'était une bonne idée de relier les questions entre elles pour le bien des autres.
Rick James
1
Oh, je ne pense jamais à regarder la barre latérale.
Rick James

Réponses:

808

utilisation INSERT IGNORE INTO table

voir http://bogdan.org.ua/2007/10/18/mysql-insert-if-not-exists-syntax.html

il y a aussi la INSERT … ON DUPLICATE KEY UPDATEsyntaxe, vous pouvez trouver des explications sur dev.mysql.com


Publier sur bogdan.org.ua selon le webcache de Google :

18 octobre 2007

Pour commencer: depuis la dernière version de MySQL, la syntaxe présentée dans le titre n'est pas possible. Mais il existe plusieurs façons très simples d'accomplir ce qui est attendu en utilisant les fonctionnalités existantes.

Il existe 3 solutions possibles: utiliser INSERT IGNORE, REPLACE ou INSERT… ON DUPLICATE KEY UPDATE.

Imaginez que nous ayons une table:

CREATE TABLE `transcripts` (
`ensembl_transcript_id` varchar(20) NOT NULL,
`transcript_chrom_start` int(10) unsigned NOT NULL,
`transcript_chrom_end` int(10) unsigned NOT NULL,
PRIMARY KEY (`ensembl_transcript_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Imaginez maintenant que nous ayons un pipeline automatique qui importe les métadonnées des transcriptions à partir d'Ensembl et que, pour diverses raisons, le pipeline peut être interrompu à n'importe quelle étape de l'exécution. Ainsi, nous devons garantir deux choses:

  1. les exécutions répétées du pipeline ne détruiront pas notre base de données

  2. les exécutions répétées ne mourront pas en raison d'erreurs de «clé primaire en double».

Méthode 1: utiliser REPLACE

C'est très simple:

REPLACE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Si l'enregistrement existe, il sera écrasé; s'il n'existe pas encore, il sera créé. Cependant, l'utilisation de cette méthode n'est pas efficace dans notre cas: nous n'avons pas besoin d'écraser les enregistrements existants, c'est bien de les ignorer.

Méthode 2: utiliser INSERT IGNORE Aussi très simple:

INSERT IGNORE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Ici, si 'ensembl_transcript_id' est déjà présent dans la base de données, il sera ignoré en silence (ignoré). (Pour être plus précis, voici une citation du manuel de référence MySQL: «Si vous utilisez le mot clé IGNORE, les erreurs qui se produisent lors de l'exécution de l'instruction INSERT sont traitées à la place comme des avertissements. Par exemple, sans IGNORE, une ligne qui duplique un index UNIQUE existant ou la valeur PRIMARY KEY dans le tableau provoque une erreur de clé en double et l'instruction est abandonnée. ”.) Si l'enregistrement n'existe pas encore, il sera créé.

Cette deuxième méthode présente plusieurs faiblesses potentielles, notamment la non interruption de la requête en cas de problème (voir le manuel). Il doit donc être utilisé s'il a été testé auparavant sans le mot clé IGNORE.

Méthode 3: en utilisant INSERT… ON DUPLICATE KEY UPDATE:

La troisième option consiste à utiliser la INSERT … ON DUPLICATE KEY UPDATE syntaxe, et dans la partie UPDATE, rien ne fait une opération dénuée de sens (vide), comme calculer 0 + 0 (Geoffray suggère de faire l'affectation id = id pour que le moteur d'optimisation MySQL ignore cette opération). L'avantage de cette méthode est qu'elle ignore uniquement les événements de clé en double et abandonne toujours d'autres erreurs.

Enfin, ce billet a été inspiré par Xaprb. Je conseillerais également de consulter son autre article sur l'écriture de requêtes SQL flexibles.

knittl
la source
3
et puis-je combiner cela avec "retardé" pour accélérer le script?
warren
3
oui, l'insertion retardée peut accélérer les choses pour vous. essayez-le
knittl
32
Oui, et gardez à l'esprit que REPLACE INTO efface puis insère, pas MISE À JOUR
bobobobo
10
INSERT … ON DUPLICATE KEY UPDATEest préférable car il ne supprime pas la ligne, préservant toutes les auto_incrementcolonnes et autres données.
renouvelé
15
Juste pour informer tout le monde. L'utilisation de la INSERT … ON DUPLICATE KEY UPDATEméthode incrémente toute colonne AUTO_INCREMENT dont l'insertion a échoué. Probablement parce que ce n'est pas vraiment échoué, mais MIS À JOUR.
not2qubit
216

Solution:

INSERT INTO `table` (`value1`, `value2`) 
SELECT 'stuff for value1', 'stuff for value2' FROM DUAL 
WHERE NOT EXISTS (SELECT * FROM `table` 
      WHERE `value1`='stuff for value1' AND `value2`='stuff for value2' LIMIT 1) 

Explication:

La requête la plus profonde

SELECT * FROM `table` 
      WHERE `value1`='stuff for value1' AND `value2`='stuff for value2' LIMIT 1

utilisé comme WHERE NOT EXISTS-condition détecte s'il existe déjà une ligne avec les données à insérer. Une fois qu'une ligne de ce type a été trouvée, la requête peut s'arrêter, d'où LIMIT 1(la micro-optimisation peut être omise).

La requête intermédiaire

SELECT 'stuff for value1', 'stuff for value2' FROM DUAL

représente les valeurs à insérer. DUALfait référence à une table spéciale à une ligne et à une colonne présente par défaut dans toutes les bases de données Oracle (voir https://en.wikipedia.org/wiki/DUAL_table ). Sur une version 5.7.26 de MySQL-Server, j'ai reçu une requête valide lors de l'omission FROM DUAL, mais les versions plus anciennes (comme 5.5.60) semblent exiger les FROMinformations. L'utilisation de WHERE NOT EXISTSla requête intermédiaire renvoie un jeu de résultats vide si la requête la plus interne a trouvé des données correspondantes.

La requête externe

INSERT INTO `table` (`value1`, `value2`) 

insère les données, le cas échéant, est renvoyée par la requête intermédiaire.

Serveur
la source
4
pouvez-vous donner plus d'informations sur la façon de l'utiliser?
Alex V
36
Cette variante convient si aucune clé unique sur la table n'existe ( INSERT IGNOREet INSERT ON DUPLICATE KEYnécessite des contraintes de clé uniques)
rabudde
2
Si vous utilisez "from dual" sur la ligne 2 au lieu de "from table", vous n'avez pas besoin de la clause "limit 1".
Rich
6
Et si stuff for value1et stuff for value2sont identiques? Cela jetterait unDuplicate column name
Robin
1
Je préfère également de loin SELECT 1plutôt que SELECT *dans les sous-requêtes. Il est beaucoup plus probable que cela puisse être satisfait par un indice.
Arth
58

sur la mise à jour de clé en double , ou l' insertion d'ignorer peut être une solution viable avec MySQL.


Exemple de mise à jour de mise à jour de clé en double basée sur mysql.com

INSERT INTO table (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

UPDATE table SET c=c+1 WHERE a=1;

Exemple d' insertion ignorer basé sur mysql.com

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name [(col_name,...)]
    {VALUES | VALUE} ({expr | DEFAULT},...),(...),...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

Ou:

INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name
    SET col_name={expr | DEFAULT}, ...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]

Ou:

INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE]
    [INTO] tbl_name [(col_name,...)]
    SELECT ...
    [ ON DUPLICATE KEY UPDATE
      col_name=expr
        [, col_name=expr] ... ]
Zed
la source
24

Toute contrainte simple devrait faire l'affaire, si une exception est acceptable. Exemples :

  • clé primaire sinon substitut
  • contrainte unique sur une colonne
  • contrainte unique multi-colonnes

Désolé, cela semble d'une simplicité trompeuse. Je sais que ça fait mal face au lien que vous partagez avec nous. ;-(

Mais je donne toujours cette réponse, car elle semble répondre à votre besoin. (Sinon, cela peut déclencher la mise à jour de vos besoins, ce qui serait également "une bonne chose" (TM)).

Modifié : si une insertion rompait la contrainte unique de la base de données, une exception est levée au niveau de la base de données, relayée par le pilote. Cela arrêtera certainement votre script, avec un échec. Il doit être possible en PHP de traiter ce cas ...

KLE
la source
1
j'ai ajouté une précision à la question - votre réponse s'applique-t-elle toujours?
warren
2
Je crois que oui. Une contrainte unique entraînera l'échec des insertions incorrectes. Remarque: vous devez gérer cet échec dans votre code, mais c'est assez standard.
KLE
1
pour l'instant, je vais m'en tenir à la solution que j'ai acceptée - mais j'examinerai davantage la gestion des échecs INSERT, etc. à mesure que l'application se développe
warren
3
INSERT IGNOREtransforme essentiellement toutes les erreurs en avertissements afin que votre script ne soit pas interrompu. Vous pouvez ensuite afficher tous les avertissements avec la commande SHOW WARNINGS. Et une autre remarque importante : les contraintes UNIQUES ne fonctionnent pas avec les valeurs NULL, c'est-à-dire. row1 (1, NULL) et row2 (1, NULL) seront tous deux insérés (sauf si une autre contrainte telle qu'une clé primaire est cassée). Malheureux.
Simon East
18

Voici une fonction PHP qui insérera une ligne uniquement si toutes les valeurs de colonnes spécifiées n'existent pas déjà dans la table.

  • Si l'une des colonnes diffère, la ligne sera ajoutée.

  • Si le tableau est vide, la ligne sera ajoutée.

  • S'il existe une ligne où toutes les colonnes spécifiées ont les valeurs spécifiées, la ligne ne sera pas ajoutée.

    function insert_unique($table, $vars)
    {
      if (count($vars)) {
        $table = mysql_real_escape_string($table);
        $vars = array_map('mysql_real_escape_string', $vars);
    
        $req = "INSERT INTO `$table` (`". join('`, `', array_keys($vars)) ."`) ";
        $req .= "SELECT '". join("', '", $vars) ."' FROM DUAL ";
        $req .= "WHERE NOT EXISTS (SELECT 1 FROM `$table` WHERE ";
    
        foreach ($vars AS $col => $val)
          $req .= "`$col`='$val' AND ";
    
        $req = substr($req, 0, -5) . ") LIMIT 1";
    
        $res = mysql_query($req) OR die();
        return mysql_insert_id();
      }
    
      return False;
    }

Exemple d'utilisation:

<?php
insert_unique('mytable', array(
  'mycolumn1' => 'myvalue1',
  'mycolumn2' => 'myvalue2',
  'mycolumn3' => 'myvalue3'
  )
);
?>
Jrm
la source
5
Assez cher si vous avez une énorme charge d'insertions.
Эџad Дьdulяңмaи
vrai, mais efficace si vous devez ajouter des vérifications spécifiques
Charles Forest
1
Avertissement: l' mysql_* extension est obsolète à partir de PHP 5.5.0 et a été supprimée à partir de PHP 7.0.0. À la place, l' extension mysqli ou PDO_MySQL doit être utilisée. Voir également la Présentation de l'API MySQL pour plus d'aide lors du choix d'une API MySQL.
Dharman
17
REPLACE INTO `transcripts`
SET `ensembl_transcript_id` = 'ENSORGT00000000001',
`transcript_chrom_start` = 12345,
`transcript_chrom_end` = 12678;

Si l'enregistrement existe, il sera écrasé; s'il n'existe pas encore, il sera créé.

Rocio
la source
10
REPLACEpeut supprimer la ligne, puis insérer au lieu de la mise à jour. L'effet secondaire est que les contraintes peuvent supprimer d'autres objets et que les déclencheurs de suppression sont déclenchés.
xmedeko
1
D'après le manuel MySQL: "REMPLACER n'a de sens que si une table a un index PRIMARY KEY ou UNIQUE. Sinon, cela devient équivalent à INSERT, car il n'y a pas d'index à utiliser pour déterminer si une nouvelle ligne en double un autre."
BurninLeo
16

Essayez ce qui suit:

IF (SELECT COUNT(*) FROM beta WHERE name = 'John' > 0)
  UPDATE alfa SET c1=(SELECT id FROM beta WHERE name = 'John')
ELSE
BEGIN
  INSERT INTO beta (name) VALUES ('John')
  INSERT INTO alfa (c1) VALUES (LAST_INSERT_ID())
END
Jeb's
la source
5
Try This réponses sont de faible valeur sur StackOverflow car elles font très peu pour éduquer le PO et des milliers de futurs chercheurs. Veuillez modifier cette réponse pour inclure le fonctionnement de la solution et pourquoi c'est une bonne idée.
mickmackusa
1
Solution parfaite au cas où les champs à faire correspondre ne sont pas des clés ..!
Leo
6

Il existe plusieurs réponses qui expliquent comment résoudre ce problème si vous disposez d'un UNIQUEindex que vous pouvez vérifier avec ON DUPLICATE KEYou INSERT IGNORE. Ce n'est pas toujours le cas, et comme UNIQUEpour une contrainte de longueur (1000 octets), vous ne pourrez peut-être pas changer cela. Par exemple, j'ai dû travailler avec des métadonnées dans WordPress ( wp_postmeta).

Je l'ai finalement résolu avec deux requêtes:

UPDATE wp_postmeta SET meta_value = ? WHERE meta_key = ? AND post_id = ?;
INSERT INTO wp_postmeta (post_id, meta_key, meta_value) SELECT DISTINCT ?, ?, ? FROM wp_postmeta WHERE NOT EXISTS(SELECT * FROM wp_postmeta WHERE meta_key = ? AND post_id = ?);

La requête 1 est une UPDATErequête régulière sans effet lorsque l'ensemble de données en question n'est pas là. La requête 2 est un INSERTqui dépend de a NOT EXISTS, c'est-à- INSERTdire que n'est exécuté que lorsque l'ensemble de données n'existe pas.

millepertuis
la source
3

Il convient de noter que INSERT IGNORE incrémentera toujours la clé primaire, que l'instruction soit un succès ou non, tout comme le ferait un INSERT normal.

Cela entraînera des lacunes dans vos clés primaires qui pourraient rendre un programmeur mentalement instable. Ou si votre application est mal conçue et dépend de clés primaires incrémentielles parfaites, cela pourrait devenir un casse-tête.

Examinez innodb_autoinc_lock_mode = 0(paramètre du serveur et est accompagné d'un léger hit de performance), ou utilisez d'abord SELECT pour vous assurer que votre requête n'échouera pas (qui est également fournie avec un hit de performance et du code supplémentaire).

Gilly
la source
Pourquoi des "lacunes dans vos clés primaires" - même potentiellement - "rendraient un programmeur mentalement instable"? Des lacunes se produisent tout le temps dans les clés primaires - chaque fois que vous supprimez un enregistrement, par exemple.
warren
Commencer par un SELECTéchoue dans le seul but de distribuer un gros lot de INSERTs et de ne pas vouloir se soucier des doublons.
warren
2

Mettre à jour ou insérer sans clé primaire connue

Si vous avez déjà une clé unique ou primaire, les autres réponses avec soit INSERT INTO ... ON DUPLICATE KEY UPDATE ...ou REPLACE INTO ...devraient fonctionner correctement (notez que remplacer en supprime s'il existe puis insère - donc ne met pas à jour partiellement les valeurs existantes).

Mais si vous avez les valeurs pour some_column_idet some_typedont la combinaison est connue pour être unique. Et vous souhaitez mettre à jour some_values'il existe, ou insérer s'il n'existe pas. Et vous voulez le faire en une seule requête (pour éviter d'utiliser une transaction). Cela pourrait être une solution:

INSERT INTO my_table (id, some_column_id, some_type, some_value)
SELECT t.id, t.some_column_id, t.some_type, t.some_value
FROM (
    SELECT id, some_column_id, some_type, some_value
    FROM my_table
    WHERE some_column_id = ? AND some_type = ?
    UNION ALL
    SELECT s.id, s.some_column_id, s.some_type, s.some_value
    FROM (SELECT NULL AS id, ? AS some_column_id, ? AS some_type, ? AS some_value) AS s
) AS t
LIMIT 1
ON DUPLICATE KEY UPDATE
some_value = ?

Fondamentalement, la requête s'exécute de cette façon (moins compliquée qu'elle n'y paraît):

  • Sélectionnez une ligne existante via la WHEREcorrespondance de clause.
  • Union qui résulte avec une nouvelle ligne potentielle (table s), où les valeurs de colonne sont explicitement données (s.id est NULL, donc il va générer un nouvel identifiant d'incrémentation automatique).
  • Si une ligne existante est trouvée, la nouvelle ligne potentielle de la table sest ignorée (en raison de LIMIT 1 sur la table t), et elle déclenchera toujours une ON DUPLICATE KEYqui sera UPDATEla some_valuecolonne.
  • Si une ligne existante n'est pas trouvée, la nouvelle ligne potentielle est insérée (comme indiqué par le tableau s).

Remarque: Chaque table dans une base de données relationnelle doit avoir au moins une incrémentation automatique principale id colonne d' . Si vous ne l'avez pas, ajoutez-le, même lorsque vous n'en avez pas besoin à première vue. Il est absolument nécessaire pour ce "truc".

Yeti
la source
Plusieurs autres répondeurs ont proposé un INSERT INTO ... SELECT FROMformat. Pourquoi aussi?
warren
2
@warren Soit vous n'avez pas lu ma réponse, vous ne la comprenez pas, soit je ne l'ai pas expliquée correctement. En tout cas, permettez-moi de souligner ce qui suit: ce n'est pas seulement une INSERT INTO... SELECT FROM...solution régulière . Veuillez me renvoyer un lien vers une réponse identique, si vous pouvez la trouver, je supprimerai cette réponse, sinon vous votez positivement ma réponse (deal?). Assurez-vous de vérifier que la réponse que vous allez lier n'utilise qu'une seule requête (pour la mise à jour + l'insertion), aucune transaction et est capable de cibler n'importe quelle combinaison de colonnes connues pour être uniques (donc séparément les colonnes ne le font pas doivent être uniques).
Yeti