【问题标题】:MySQL's INSERT IGNORE INTO & foreign keysMySQL 的 INSERT IGNORE INTO & 外键
【发布时间】:2011-10-14 12:45:12
【问题描述】:

为什么在 MySQL 中,INSERT IGNORE INTO 不会将 外键约束 错误更改为警告?

我正在尝试将一些记录插入到一​​个表中,我希望 MySQL 会忽略那些导致错误的记录,任何错误,然后插入其余的记录。有人有什么建议吗?

SET FOREIGN_KEY_CHECKS = 0; 不是我的答案。因为我希望根本不插入违反约束的行。

谢谢

【问题讨论】:

  • InnoDB,我猜。否则,外键错误将不是问题。这是个好问题。文档似乎没有说明这一点。
  • 没错,我使用 InnoDB。我应该更准确,我为此道歉。但正如 Álvaro 指出的那样,存储引擎是隐含的,所以我相信我已经被覆盖了 :)
  • 还有一件事,我的 MySQL 版本是:5.1.33-community

标签: mysql insert foreign-keys


【解决方案1】:

[新答案]

感谢@NeverEndingQueue 提出这个问题。似乎 MySQL 终于解决了这个问题。我不确定这个问题最初是在哪个版本中修复的,但现在我使用以下版本进行了测试,问题不再存在:

mysql> SHOW VARIABLES LIKE "%version%";
+-------------------------+------------------------------+
| Variable_name           | Value                        |
+-------------------------+------------------------------+
| innodb_version          | 5.7.22                       |
| protocol_version        | 10                           |
| slave_type_conversions  |                              |
| tls_version             | TLSv1,TLSv1.1                |
| version                 | 5.7.22                       |
| version_comment         | MySQL Community Server (GPL) |
| version_compile_machine | x86_64                       |
| version_compile_os      | Linux                        |
+-------------------------+------------------------------+

要明确:

mysql> INSERT IGNORE INTO 孩子 -> 价值观 -> (NULL, 1) -> , (NULL, 2) -> , (NULL, 3) -> , (NULL, 4) -> , (NULL, 5) -> , (NULL, 6); 查询正常,4 行受影响,2 个警告(0.03 秒) 记录:6 重复:2 警告:2

为了更好地理解最后一个查询的含义以及为什么它显示问题已修复,请继续下面的旧答案。

[旧答案]

我的解决方案是解决问题,而实际的解决方案将始终在 MySQL 本身内解决问题。

以下步骤解决了我的问题:

a.考虑使用以下表格和数据:

mysql>
CREATE TABLE parent (id INT AUTO_INCREMENT NOT NULL
                     , PRIMARY KEY (id)
) ENGINE=INNODB;

mysql>
CREATE TABLE child (id INT AUTO_INCREMENT
                    , parent_id INT
                    , INDEX par_ind (parent_id)
                    , PRIMARY KEY (id)
                    , FOREIGN KEY (parent_id) REFERENCES parent(id)
                        ON DELETE CASCADE
                        ON UPDATE CASCADE
) ENGINE=INNODB;

mysql>
INSERT INTO parent
VALUES (NULL), (NULL), (NULL), (NULL), (NULL), (NULL);

mysql>
SELECT * FROM parent;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
|  4 |
|  5 |
|  6 |
+----+

b.现在我们需要删除一些行来演示问题:

mysql>
DELETE FROM parent WHERE id IN (3, 5);

c.问题: 当您尝试插入以下子行时会出现问题:

mysql>
INSERT IGNORE INTO child
VALUES
    (NULL, 1)
    , (NULL, 2)
    , (NULL, 3)
    , (NULL, 4)
    , (NULL, 5)
    , (NULL, 6);

ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint f
ails (`test`.`child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERE
NCES `parent` (`id`) ON DELETE CASCADE ON UPDATE CASCADE)

mysql>
SELECT * FROM child;
Empty set (0.00 sec)

尽管使用了IGNORE 关键字,但是MySQL 取消了请求的操作,因为生成的错误没有变成警告(如它应该的那样)。现在问题已经很明显了,让我们看看如何执行最后一个 insert into 语句而不会遇到任何错误。

d.解决方案: 我将通过其他一些既不依赖于插入的记录,也不依赖于它们的数量的常量语句将 insert into 语句包装起来。

mysql>
SET FOREIGN_KEY_CHECKS = 0;

mysql>
INSERT INTO child
VALUES
    (NULL, 1)
    , (NULL, 2)
    , (NULL, 3)
    , (NULL, 4)
    , (NULL, 5)
    , (NULL, 6);

mysql>
DELETE FROM child WHERE parent_id NOT IN (SELECT id FROM parent);

mysql>
SET FOREIGN_KEY_CHECKS = 1;

我知道这不是最佳的,但只要 MySQL 没有解决问题,这是我所知道的最好的。特别是如果你在 PHP 中使用 mysqli 库,所有语句都可以在一个请求中执行。

【讨论】:

  • 另一种选择是使用临时表,从那里删除然后插入。因为我们可以没有一列,所以需要检查,并且在另一列中有错误数据的可能性非零。
  • 或许,加上一个表锁,就完美了。
  • @FandiSusanto 如果您使用 MyISAM 表锁可能会有所帮助。但如果是 Innodb,那么一个简单的事务就足够了。
  • @FandiSusanto,如果他/她使用 MyISAM 表,则没有没有外键 - MyISAM 不可能。
  • 从 MySQL 5.7.22 开始,我使用 INSERT IGNORE 得到了正确的行为。插入 4 行,生成 2 行警告。禁用 FOREIGN_KEY_CHECKS 插入 6 行!这是你想要的吗?
【解决方案2】:

我认为最简单的 mysql 解决方案是加入外键表以验证约束:

INSERT INTO child (parent_id, value2, value3)
SELECT p.id, @new_value2, @new_value3
FROM parent p WHERE p.id = @parent_id

但是你想丢弃基于缺少外键的记录似乎很奇怪。例如,我更喜欢在外键检查时出现 INSERT IGNORE 错误。

【讨论】:

  • 您的解决方案部分正确。如果您只想插入一条记录,它就可以工作,这违背了我想插入任意数量的记录的第一个假设。此外,期望 MySQL 不会为外键错误(使用忽略时)给出错误对我来说听起来很合理,因为如果你想拥有它们,你可以很容易地忽略它们!
  • @Mehran 不正确,您可以使用该语句插入任意数量的行
【解决方案3】:

我相信INSERT IGNORE 旨在忽略来自服务器层的错误,而不是存储引擎层的错误。因此,它将有助于重复键错误(它是主要用例)和某些数据转换,而不是来自存储引擎层的外键错误。

至于您的具体要求:

我正在尝试向表中插入一些记录,我希望 MySQL 忽略那些产生错误,任何错误,并插入 其余的部分。有人有什么建议吗?

为此,我建议使用mysql -f 强制它继续运行,尽管出现任何错误。因此,例如,如果您有这样的文件:

insert into child (parent_id, ...) values (bad_parent_id, ...);
insert into child (parent_id, ...) values (good_parent_id, ...);

然后您可以像这样加载该文件,它将插入好的行并忽略坏行中的错误:

mysql -f < inserts.sql

【讨论】:

  • 我没有理由接受或拒绝这个解释(特别是因为没有报告参考)。但即使是这样,那我必须说,这听起来更像是一个泄漏到实现阶段的分析错误。在我的脑海中,IGNORE 关键字可以轻松设置全局标志,因此任何引擎都可以按照自己的意愿进行检查和运行。我没有研究过 MySQL 的代码,所以我相信这个问题会有更好的解决方案。
  • 手册在描述插入忽略时并没有特别提到外键错误,但这里是:dev.mysql.com/doc/refman/5.1/en/insert.html。我添加了一个示例,说明如何执行多个插入语句并忽略任何错误,包括 FK 错误。
  • 非常感谢您的时间和精力。但事实是,我使用的是 PHP!您知道在使用其他客户端(如 PHP 中的客户端)时如何忽略所有错误吗?我只是希望我这个迟到的问题的答案不会包括编译自定义客户端。
  • 我不使用 PHP,但我认为您可以将插入内容包装在 try/catch 中并吞下错误。
  • 对不起,我不买那个!出于优化的原因,我想在 MySQL 中完成这一切。当然,我可以找到在 PHP 中执行此操作的方法,但我要求的是 MySQL 解决方案。或者换句话说,使用一个 SQL 请求。无论如何,感谢您的宝贵时间。
【解决方案4】:

这个问题似乎在 MySQL 5.7 中得到修复,请参阅 https://bugs.mysql.com/bug.php?id=78853

现在外键约束错误变成了警告:

此问题存在于 5.1、5.5、5.6 版本中,但我看到在 5.7 中进行了一些改进,在 WL#6614 之后错误已转换为警告而不是错误。

【讨论】:

  • 确认,当使用 5.7.22
【解决方案5】:

如果您要向数据库中插入一行,则可以使用附加查询显式运行此检查。假设您有这些表:

用户宠物 用户名 |用户名 | petId petId |宠物名 --------+----------+------ ------+---------- 1 |哈罗德 | 8 1 |菲多 2 |弗雷德 | 3 3 |点 8 |连指手套

并且您想插入一个新用户(George,他有宠物 #1)。你可以做类似的事情

$newUserName = "George"
$petId = "1"
mysql_query("begin")
$result = mysql_query("SELECT petId FROM Pet WHERE petId = $petId LIMIT 1")
if ($result && 1 == mysql_num_rows($result)) {
    mysql_query("INSERT INTO User (userName, petId) VALUES ('$newUserName', $petId)")
}
mysql_query("commit")

请原谅我的示例中过度简化的代码和不良做法 - 这只是为了说明可能的解决方法。

【讨论】:

  • 非常感谢 George,但我一直在寻找纯 MySQL 解决方案。就像您尝试使用 IGNORE 选项将副本插入到唯一键时一样。正如 cmets 中提到的,不应使用任何编程解决方案。否则很简单。
【解决方案6】:

请记住,INSERT IGNORE 不是 ANSI 标准的一部分,这是有原因的。您应该首先避免插入重复记录。主/唯一与外键不同。请记住,IGNORE 也会忽略其他错误和警告(除以零、数据截断),这通常不是一件好事。

在这种情况下,几乎每次都使用 REPLACE 而不是 INSERT IGNORE 更有意义。另一个选项是 ON DUPLICATE KEY UPDATE。这是 Mysql 对 MERGE Ansi SQL 的回答。

另一种选择是使用临时表。

create temporary table tmpTable (col1, col2, ...);
insert into tmpTable ...; -- same insert you already had
alter table tmpTable add key (foreignColumnName); -- only if # of rows is big
insert into table select null /* AI id */, col1, col2 ...
  from tmpTable join parentTable on foreignColumnName = parent.id;

问候, 何塞。

【讨论】:

  • 它确实会忽略其他警告,但如果数据在一个人的应用程序中得到验证,而不是像大多数关心清理的人那样依赖数据库,那么一开始就没有这样的警告。 REPLACE 在不存在父行时仍然失败,并且对于一对多关系并不总是有用。 ON DUPLICATE KEY UPDATE 通过自动增量值燃烧,并且在相关子记录没有主/唯一键的情况下无关紧要。 IGNORE 对于接收您没有的记录的相关数据非常有用,并且避免通过可能创建竞争条件的 SELECT 检查。