【问题标题】:At what point is MySQL primary key error thrown?什么时候会抛出 MySQL 主键错误?
【发布时间】:2014-11-13 03:12:30
【问题描述】:

如果我有一个批量插入语句,例如:

INSERT INTO TABLE VALUES (x,y,z),(x2,y2,z2),(x3,y3,z3);

x2违反了主键,是在处理x3之前还是之后抛出的错误?

具体来说,我在使用 Python 和 PyMySQL 的 try-catch 块中有一堆批量插入,例如:

conn = myDB.cursor() 
try:
     conn.execute("INSERT INTO TABLE VALUES (x,y,z),(x2,y2,z2),(x3,y3,z3);")
except pymysql.Error as  msg:
     print("MYSQL ERROR!:{0}".format(msg)) #print error

我想确保如果批量插入中的一个元组失败,从而打印错误,同一批次中的其余元组仍然被处理。

我的动机是我在两台服务器之间传输大量数据。在服务器 1 中,数据存储在日志文件中,并且正在插入到服务器 2 上的 MySQL 中。一些数据已经在服务器 2 上的 MySQL 中,因此有很多故障。但是,如果我不使用批量插入,并且对于每条(数百万条)记录我都有一个单独的INSERT INTO,那么事情的运行速度似乎要慢得多。所以无论哪种方式我都遇到了麻烦:使用批量插入,重复失败会破坏整个语句,而没有批量插入,该过程需要更长的时间。

【问题讨论】:

  • AFAIK 值向量按写入顺序进行处理,如果向量违反主键(或唯一索引)规则,则不会插入。因此,按照那条线,我认为可以公平地说,在您的示例中,x2 在尝试插入x3 之前 被检查。
  • 嗯,也就是说如果 x1 和 x3 有效但 x2 无效,则整个语句失败??
  • 在下面查看我的答案
  • 正如 AirThomas 所指出的,这取决于您桌子的引擎。检查他的答案

标签: python mysql pymysql


【解决方案1】:

MySQL 处理多个插入(或更新)语句的方式因表引擎和服务器 SQL 模式而异。

虽然只有表引擎对您在此处询问的关键约束非常重要,但了解大局很重要,因此我将花时间添加一些额外的细节。如果您赶时间,请随时阅读下面的第一部分和最后一部分。

表格引擎

在像 MyISAM 这样的非事务性表引擎的情况下,您很容易最终执行部分更新,因为每个插入或更新都是按顺序执行的,并且在遇到坏行并且语句被中止时无法回滚。

但是,如果您使用 InnoDB 之类的事务表引擎,则在插入或更新语句期间的任何约束违规都将触发回滚到该点所做的任何更改,除了中止语句。

SQL 模式

server SQL mode 在您没有违反键约束但您尝试插入或更新的数据不符合您要放入的列的定义时变得很重要。例如:

  • 插入一行而不为每个NOT NULL 列提供值
  • '123' 插入到使用数字类型(而不是123)定义的列中
  • 更新CHAR(3) 列以保存值'four'

在这些情况下,如果严格模式生效,MySQL 将抛出错误。但是,如果严格模式不起作用,它通常会“修复”您的错误,这可能会导致各种潜在的有害行为(请参阅 MySQL 'Truncated incorrect INTEGER value'mysql string conversion return 0 仅两个示例)。

危险,威尔罗宾逊!

非事务性表和严格模式存在一些潜在的“陷阱”。您还没有告诉我们您正在使用哪个表引擎,但目前所写的 this answer 显然使用的是非事务性表,因此了解它如何影响结果很重要。

例如,考虑以下一组语句:

SET sql_mode = '';  # This will make sure strict mode is not in effect

CREATE TABLE tbl (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  val INT
) ENGINE=MyISAM;  # A nontransactional table engine (this used to be the default)

INSERT INTO tbl (val) VALUES (1), ('two'), (3);

INSERT INTO tbl (val) VALUES ('four'), (5), (6);

INSERT INTO tbl (val) VALUES ('7'), (8), (9);

由于严格模式不起作用,插入所有九个值并将无效字符串强制转换为整数也就不足为奇了。服务器足够聪明,可以将'7' 识别为数字,但无法识别'two''four',因此它们被转换为default value for numeric types in MySQL

mysql> SELECT val FROM tbl;
+------+
| val  |
+------+
|    1 |
|    0 |
|    3 |
|    0 |
|    5 |
|    6 |
|    7 |
|    8 |
|    9 |
+------+
9 rows in set (0.00 sec)

现在,请尝试使用 sql_mode = 'STRICT_ALL_TABLES' 再次执行此操作。长话短说,第一个 INSERT 语句将导致部分插入,第二个将完全失败,第三个将默默地将 '7' 强制转换为 7(如果你问我,但它是documented behavior,并不是那么不合理)。

但是等等,还有更多!试试sql_mode = 'STRICT_TRANS_TABLES'。现在您会发现第一条语句引发了警告而不是错误 - 但第二条语句仍然失败!如果您将 LOAD DATA 与一堆文件一起使用,而其中一些文件失败而另一些文件没有失败,这可能会特别令人沮丧(请参阅 this closed bug report)。

做什么

在密钥违规的情况下,重要的只是表引擎是否是事务性的(例如:InnoDB)(例如:MyISAM)。如果您正在处理事务表,则问题中的 Python 代码将导致 MySQL 服务器按以下顺序执行操作:

  1. 解析INSERT 语句并启动事务。*
  2. 插入第一个元组。
  3. 插入第二个元组(违反键约束)。
  4. 回滚交易。
  5. pymysql发送错误消息。

*在开始事务之前解析语句是有意义的,但我不知道确切的实现,所以我将把它们放在一起作为一个步骤。

在这种情况下,当您的脚本从服务器收到错误消息并进入except 块时,错误元组之前的任何更改都已被撤消。

但是,如果您正在处理非事务表,服务器将跳过第 4 步(以及第 1 步的相关部分),因为表引擎不支持transaction statements。在这种情况下,当您的脚本进入except 块时,第一个元组已插入,第二个已炸毁,您可能无法轻松确定成功插入了多少行,因为the function that normally does that 返回 - 1 如果最后一个插入或更新语句抛出错误。

应严格避免部分更新;它们比简单地确保您的语句完全成功或完全失败更难修复。在这种情况下,the documentation suggests

为避免[部分更新],请使用单行语句,无需更改表即可中止。

在我看来,这正是你应该做的。用 Python 编写循环并不难,只要你是 inserting values properly as parameters,你就不必重复代码,而不是对它们进行硬编码——你已经在这样做了,对吧?正确的??? >:(

替代方案

如果您期望有时会违反您的约束,并且您想在您尝试插入的行已存在时采取其他操作,那么您可能对`INSERT ... ON DUPLICATE KEY UPDATE' 感兴趣。这让您可以完成计算体操的惊人壮举,例如数数

mysql> create table counting_is_fun (
    -> stuff int primary key,
    -> ct int unsigned not null default 1
    -> );
Query OK, 0 rows affected (0.12 sec)

mysql> insert into counting_is_fun (stuff)
    -> values (1), (2), (5), (3), (3)
    -> on duplicate key update count = count + 1;
Query OK, 6 rows affected (0.04 sec)
Records: 5  Duplicates: 1  Warnings: 0

mysql> select * from counting_is_fun;
+-------+-------+
| stuff | count |
+-------+-------+
|     1 |     1 |
|     2 |     1 |
|     3 |     2 |
|     5 |     1 |
+-------+-------+
4 rows in set (0.00 sec)

(注意:将您插入的元组数与查询“受影响”的行数以及之后表中的行数进行比较。计数是不是很有趣?)

或者,如果您认为您现在插入的数据至少与表中当前的数据一样好,您可以查看REPLACE INTO - 但这是 SQL 标准的特定于 MySQL 的扩展,并且像往常一样,it has its quirks,尤其是关于与外键引用关联的AUTO_INCREMENT 字段和ON DELETE 操作。

人们喜欢建议的另一种方法是INSERT IGNORE。这会忽略错误并继续滚动。太好了,对吧?无论如何,谁需要错误?我不喜欢这个解决方案的原因是:

  • INSERT IGNORE 将导致语句期间发生的任何错误被忽略,而不仅仅是您认为您不关心的任何错误。
  • 文档指出,"Ignored errors may generate warnings instead, although duplicate-key errors do not." 因此,您甚至不一定知道使用此关键字时会出现哪些警告
  • 对我来说,使用 INSERT IGNORE 表示:“我不知道如何以正确的方式执行此操作,所以我只会以错误的方式进行操作。”

我有时会使用INSERT IGNORE,但是当文档明确告诉您做某事的“正确方法”时,请不要自欺欺人。先这样试试;如果您仍然有充分的理由以错误的方式进行操作并冒着破坏数据完整性和永远毁掉一切的风险,那么至少您已经做出了明智的决定。

【讨论】:

  • 感谢您的深入回答!我相信未来的读者会发现它很有用。快速提问:假设ON DUPLICATE UPDATEINSERT IGNORE,我假设将处理第三个元组?还;我已经编辑了我的问题,以确定我首先问这个问题的原因!
  • @Tommy 是的;在任何一种情况下,重复键都不会引发错误,并且语句将继续执行。
【解决方案2】:

对 MyISAM 表进行了一些实验后,我发现如果您尝试将两个或多个值元组插入到一个表中并且其中一个(或多个)违反表的约束(例如主键或唯一索引规则),元组之后 违规的元组将不会被插入:

create table test(
  id int unsigned not null primary key, 
  col varchar(100)
) Engine = MyISAM;

insert into test values
  (1, 'The first')
, (2, 'Should work')
, (2, 'Should fail') -- This one won't be inserted, and will be treated as an error
, (3, 'The last')    -- This one won't be inserted either, because of the
                     -- previous tuple "offense".
;
select * from test;
+----+-------------+
| id | col         |
+----+-------------+
|  1 | The first   |
|  2 | Should work |
+----+-------------+

在 InnoDB 表上行为不同(感谢 AirThomas 的评论)插入将完全失败

drop table test;
create table test(
  id int unsigned not null primary key, 
  col varchar(100)
) Engine = InnoDB;

insert into test values
  (1, 'The first')
, (2, 'Should work')
, (2, 'Should fail') -- This will cause the whole insert to fail
, (3, 'The last')
;
select * from test;
    Empty set

但还有其他选择。您可以使用 ignore 关键字(这似乎适用于 MyISAM 和 InnoDB 表):

truncate test; -- Let's work with an empty table
insert IGNORE into test values
  (1, 'The first')
, (2, 'Should work')
, (2, 'Should fail') -- This one won't be inserted, but will not cause the insert 
                     -- to fail (because of the IGNORE keyword)
, (3, 'The last');   -- This one will be inserted, even given the previous 
                     -- tuple "offence"
;
-- In MySQL CLI this will pop out a message like this:
-- Query OK, 3 rows affected
-- Records: 4 Duplicates: 1 Warnings: 0
select * from test;
+----+-------------+
| id | col         |
+----+-------------+
|  1 | The first   |
|  2 | Should work |
|  3 | The last    |
+----+-------------+

你也可以使用on duplicate key...我把它作为“家庭作业”留给你。 Read the documentation about insert ... on duplicate key update.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-25
    • 2015-05-25
    • 1970-01-01
    • 2011-03-22
    • 2011-11-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多