在MySQL中如何执行'如果不存在则插入'的操作?

1085
我开始通过谷歌搜索,并找到了一篇文章《如何在标准SQL中编写INSERT if NOT EXISTS查询》,其中讨论了互斥表。
我有一个包含大约1400万条记录的表。如果我想以相同的格式添加更多数据,有没有一种方法可以确保我要插入的记录不存在,而不需要使用一对查询(即,一个查询用于检查,一个查询用于在结果集为空时插入)?
在字段上设置唯一约束是否能保证如果已经存在,则插入操作将失败?
似乎仅仅使用约束,当我通过PHP执行插入时,脚本会出错。

2
http://dev.mysql.com/doc/refman/5.0/en/if.html - Uğur Gümüşhan
请参考以下讨论关于如何在MySQL中实现原子性的插入操作,以避免烧毁自增值:https://stackoverflow.com/questions/44550788/mysql-atomic-insert-if-not-exists-with-stable-autoincrement-not-duplicate。 - Rick James
@RickJames - 这是一个有趣的问题..但不确定它是否直接与这个问题相关 :) - warren
1
在评论中提到,另一个问题声称这个问题是“完全重复的”。因此,我认为将这些问题链接在一起,以造福他人,是一个好主意。 - Rick James
1
哦,我从来没有想过看侧边栏。 - Rick James
显示剩余7条评论
11个回答

1004

使用INSERT IGNORE INTO table

还有INSERT … ON DUPLICATE KEY UPDATE语法,你可以在13.2.6.2 INSERT ... ON DUPLICATE KEY UPDATE Statement中找到解释。


来自 bogdan.org.ua 的文章,根据 Google's webcache

2007年10月18日

首先,从最新的MySQL开始,标题中呈现的语法不再可行。但是,使用现有功能可以轻松实现预期的目标。

有三种可能的解决方案:使用INSERT IGNORE、REPLACE或INSERT … ON DUPLICATE KEY UPDATE。

假设我们有一个表:

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;

现在想象一下,我们有一个自动流水线从Ensembl导入转录物元数据,由于各种原因,流水线可能在执行的任何步骤中出现故障。因此,我们需要确保两件事:

  1. 流水线的重复执行不会破坏我们的数据库。
  2. 由于“重复主键”错误而导致的重复执行不会死亡。

方法1:使用REPLACE

这很简单:

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

如果记录存在,它将被覆盖;如果记录尚不存在,则将创建一个新记录。但是,对于我们的情况,使用此方法并不高效:我们不需要覆盖现有记录,只需跳过它们即可。

方法2:使用INSERT IGNORE也非常简单:

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

如果在数据库中已经存在‘ensembl_transcript_id’,则它将被静默跳过(忽略)。更确切地说,在MySQL参考手册中有一句引用:“如果使用IGNORE关键字,则执行INSERT语句时发生的错误将被视为警告。例如,如果没有IGNORE,则表中重复现有唯一索引或主键值的行会导致重复键错误,并且语句将被中止。”如果记录尚不存在,则将创建该记录。

第二种方法存在几个潜在的弱点,包括在任何其他问题发生时不会终止查询(请参阅手册)。因此,应在未使用IGNORE关键字进行测试的情况下使用此方法。

方法3:使用INSERT ... ON DUPLICATE KEY UPDATE:

第三个选项是使用INSERT ... ON DUPLICATE KEY UPDATE语法,在UPDATE部分中只做一些无意义的操作,比如计算0+0(Geoffray建议对id=id赋值,以使MySQL优化引擎忽略此操作)。这种方法的优点是它仅忽略重复键事件,并仍然在其他错误上中止。

最后注意:这篇文章的灵感来源于Xaprb。我还建议查看他关于编写灵活的SQL查询的其他帖子。


3
我可以将“delayed”与之结合以加快脚本运行吗? - warren
19
INSERT … ON DUPLICATE KEY UPDATE更好,因为它不会删除行,保留任何auto_increment列和其他数据。 - redolent
23
仅供大家参考。使用INSERT ... ON DUPLICATE KEY UPDATE方法不会增加任何AUTO_INCREMENT列的插入失败。可能是因为它并没有真正失败,而是被更新了。 - not2qubit
3
在InnoDB引擎中使用INSERT ON DUPLICATE KEY时,会出现奇怪的增量行为。即使插入失败,自动递增列也会递增。因此,如果您已经存储了Keyword1、Keyword2并尝试添加一个新的关键字Keyword3,由于之前的重复查找和失败,Keyword3的ID实际上会增加3。请注意不要改变原意。 - TheCarver
显示剩余14条评论

323

解决方案:

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) 

解释:

最内层的查询

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

用作 WHERE NOT EXISTS 条件,以检测是否已经存在一行具有要插入的数据。找到这种类型的一行后,查询可能会停止,因此使用了 LIMIT 1(微调优化,可以省略)。

中间查询

SELECT 'stuff for value1', 'stuff for value2' FROM DUAL
代表要插入的值。DUAL是Oracle数据库中默认存在的一个特殊一行一列表(参见 https://en.wikipedia.org/wiki/DUAL_table)。在MySQL-Server版本5.7.26上,当省略FROM DUAL时,我得到了一个有效的查询,但旧版本(如5.5.60)似乎需要FROM信息。通过使用WHERE NOT EXISTS,如果最内层查询找到匹配数据,则中间查询返回一个空结果集。
外部查询
INSERT INTO `table` (`value1`, `value2`) 

如果中间查询返回任何数据,则插入该数据。


4
你能提供关于如何使用这个的更多信息吗? - Alex V
63
如果表中不存在唯一键(INSERT IGNOREINSERT ON DUPLICATE KEY 需要唯一键约束),则可以使用此变量。 - rabudde
2
如果您在第2行使用"from dual"而不是"from table",那么您就不需要"limit 1"子句。 - Rich
7
如果“value1的内容”和“value2的内容”完全相同会怎样?这会导致出现“重复列名”的错误提示。 - Robin
2
你可以使用以下语句替代DUAL(至少在MySQL中): INSERT INTO table (value1, value2) SELECT 'stuff for value1', 'stuff for value2' FROM (select 1) x WHERE NOT EXISTS (SELECT * FROM table WHERE value1='stuff for value1' AND value2='stuff for value2'); - noonex
显示剩余6条评论

73

在MySQL中,ON DUPLICATE KEY UPDATEINSERT IGNORE 可以成为可行的解决方案。


ON DUPLICATE KEY UPDATE 的一个例子来自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;

一个基于mysql.com的INSERT IGNORE示例

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] ... ]
或者:
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] ... ]

或者:

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

27

如果可以接受异常,任何简单的约束都可以胜任。例如:

  • 非代理主键
  • 某列上的唯一约束
  • 多列唯一约束

如果这看起来过于简单,对不起。我知道与您分享的链接相比看起来很糟糕。但是我还是给出了这个答案,因为它似乎能够满足您的需求。 (如果不能,它可能会促使您更新您的要求,这也是“好事”(TM))。

如果插入操作会破坏数据库的唯一约束条件,则会在数据库级别抛出异常,并由驱动程序中继。它肯定会导致您的脚本失败停止。在PHP中必须有办法处理这种情况...


1
我在问题中添加了一个澄清 - 你的回答仍然适用吗? - warren
2
我相信它会。唯一约束将导致不正确的插入操作失败。注意:您必须在代码中处理此故障,但这是非常标准的。 - KLE
1
目前我会坚持采用我接受的解决方案,但随着应用程序的发展,我会进一步研究如何处理插入失败等问题。 - warren
5
INSERT IGNORE 基本上会将所有错误转换为警告,以便您的脚本不会中断。然后,您可以使用 SHOW WARNINGS 命令查看任何警告。另一个重要提示:唯一约束条件不能与NULL值一起使用,例如,行1(1,NULL)和行2(1,NULL)都将被插入(除非违反其他约束条件,如主键)。很遗憾。 - Simon East

24

请尝试以下方法:

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

13
“尝试这个”(Try This)的答案在StackOverflow上价值较低,因为它们对于教育提问者和未来成千上万的研究人员几乎没有作用。请编辑此答案并说明解决方案的工作原理及其为什么是一个好主意。 - mickmackusa
5
如果待匹配的字段不是主键,完美的解决方案! - Leo

18

下面是一个 PHP 函数,它只会在所有指定的列值都不存在于表中时才插入一行。

  • 如果其中一个列不同,那么该行将被添加。

  • 如果表为空,则该行将被添加。

  • 如果存在一行,其中所有指定的列具有指定的值,则不会添加该行。

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

使用示例:

<?php
  insert_unique('mytable', array(
    'mycolumn1' => 'myvalue1',
    'mycolumn2' => 'myvalue2',
    'mycolumn3' => 'myvalue3'
    )
  );
?>

7
如果您需要进行大量插入,那么费用会非常昂贵。 - Eyad Fallatah
如果需要添加特定的检查,返回true,但要高效。 - Charles Forest
2
警告:mysql_*扩展自PHP 5.5.0起已被弃用,并在PHP 7.0.0中删除。相反,应使用mysqliPDO_MySQL扩展。在选择MySQL API时,还可以参考MySQL API概述获取更多帮助。 - Dharman
做mysql_real_escape_string($table)有什么意义? - Your Common Sense

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

如果记录已经存在,它将被覆盖;如果它还不存在,它将被创建。


14
REPLACE 可能会删除行并插入新行以代替更新。这样会导致约束条件删除其他对象并触发删除触发器。 - xmedeko
2
从MySQL手册中:“REPLACE仅在表具有PRIMARY KEY或UNIQUE索引时才有意义。否则,它将变成等同于INSERT,因为没有索引可用于确定新行是否重复另一行。” - BurninLeo
REPLACE INTO 或许是对这个问题最糟糕的回答。 - undefined

9
有几个答案可以解决这个问题,如果你有一个可以使用 ON DUPLICATE KEY 或者 INSERT IGNORE 检查的 UNIQUE 索引。但是并不总是这种情况,并且由于 UNIQUE 有长度约束(1000字节),可能无法更改。例如,我必须在 WordPress (wp_postmeta) 中处理元数据。

最终我用了两个查询来解决它:

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

当所涉及数据集不存在时,查询1是一个常规的UPDATE查询,没有任何影响。查询2是一个INSERT,它依赖于NOT EXISTS,即只有在数据集不存在时才执行INSERT


这个例子有点问题。它会将重复的行插入到 meta_key 表中存在的次数中。如果在末尾添加 LIMIT 1,它会起作用,但仍然感觉不太安全/不专业。我更喜欢在插入后的选择部分写成这样:SELECT * FROM (SELECT DISTINCT ?, ?, ?) as tmp - 这样即使忘记写 LIMIT 1,也会更加安全。 - Janeks Malinovskis

5
值得注意的是,INSERT IGNORE 命令仍然会像常规 INSERT 命令一样增加主键值,无论语句是否成功。这会导致主键之间出现间隙,可能会让程序员变得不稳定。如果您的应用程序设计不佳,并且依赖于完美的递增主键,则可能会成为一个头疼的问题。建议查看服务器设置中的 innodb_autoinc_lock_mode = 0 (可能会稍微影响性能),或者先使用 SELECT 命令确保您的查询不会失败(这也会降低性能并增加额外的代码)。

1
为什么“主键中的间隙”甚至可能会“使程序员精神不稳定”?在主键中经常出现间隙 - 例如,每次删除记录时都会出现。 - warren
SELECT开始就违背了只是交付一大批INSERT并且不想担心重复的整个目的。 - warren

4

没有已知主键的更新或插入

如果您已经有了唯一或主键,其他的答案可以使用INSERT INTO ... ON DUPLICATE KEY UPDATE ... 或者 REPLACE INTO ... (请注意,replace into 如果存在,则删除然后插入 - 因此不会部分更新现有值)。

但是,如果您知道组合some_column_idsome_type的值是唯一的。并且您想要在存在时更新some_value,不存在时插入。并且您想要在一个查询中完成它(以避免使用事务)。这可能是一个解决方案:

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 = ?

基本上,查询的执行过程如下(比看起来简单):

  • 通过 WHERE 子句匹配选择一行现有数据。
  • 将该结果与一个潜在的新行(表 s)进行合并,其中列值被显式地给出(s.id 为 NULL,则会生成一个新的自动递增标识符)。
  • 如果找到现有行,则从表 t 中丢弃来自表 s 的潜在新行(由于对表格 t 限制为 LIMIT 1),它将始终触发 ON DUPLICATE KEY,这将更新 some_value 列。
  • 如果未找到现有行,则插入潜在新行(由表 s 给出)。

注意:关系数据库中的每个表都应该至少有一个主键自动递增的 id 列。如果没有,请添加它,即使您乍一看不需要它。它绝对是这个“技巧”所需要的。


其他回答者提供了“INSERT INTO ... SELECT FROM”格式。你为什么也这样做? - warren
2
@warren 你要么没有看清我的回答,要么没有理解它,要么我没有解释清楚。无论哪种情况,让我强调一下以下内容:这不仅仅是一个常规的“INSERT INTO... SELECT FROM...”解决方案。如果你能找到一个相同的答案链接,请把它发给我,我会删除这个答案,否则请点赞我的答案(如何?)。请确保验证你要链接的答案只使用1个查询(用于更新+插入),没有事务,并且能够针对任何已知为唯一的列组合进行操作(因此这些列不需要分别唯一)。 - Yeti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接