3

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

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对每条(数百万条)记录都有一个单独的记录,那么事情的运行速度似乎要慢得多。所以无论哪种方式我都遇到了麻烦:使用批量插入,重复失败会破坏整个语句,并且没有批量插入,该过程需要更长的时间。

4

2 回答 2

4

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

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

表引擎

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

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

SQL 模式

当您不违反键约束但您尝试插入或更新的数据不符合您要放入的列的定义时,服务器 SQL 模式变得很重要。例如:

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

在这些情况下,如果严格模式生效,MySQL 将抛出错误。但是,如果严格模式不起作用,它通常会“修复”您的错误,这可能会导致各种潜在的有害行为(请参阅MySQL '截断不正确的 INTEGER 值'mysql 字符串转换返回 0仅两个示例)。

危险,威尔罗宾逊!

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

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

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'or 'four',因此它们被转换为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(如果您问我,这似乎不是很“严格”,但它是记录在案的行为而不是这不合理)。

但是等等,还有更多!试试看sql_mode = 'STRICT_TRANS_TABLES'。现在您会发现第一条语句引发了警告而不是错误 - 但第二条语句仍然失败!如果您使用LOAD DATA的是一堆文件并且有些文件失败而有些文件没有失败,这可能会特别令人沮丧(请参阅此关闭的错误报告)。

该怎么办

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

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

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

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

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

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

为避免 [部分更新],请使用单行语句,该语句可以在不更改表的情况下中止。

在我看来,这正是你应该做的。在 Python 中编写循环并不难,只要您正确地将值作为参数插入而不是对它们进行硬编码,您就不必重复代码——您已经在这样做了,对吧?正确的???>:(

替代方案

如果您希望有时会违反您的约束,并且您想在您尝试插入的行已经存在时采取其他措施,那么您可能会对`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 的扩展,并且像往常一样,它具有怪癖,尤其是与外键引用相关的AUTO_INCREMENT字段和操作。ON DELETE

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

  • INSERT IGNORE将导致语句期间发生的任何错误都被忽略,而不仅仅是您认为不关心的任何错误。
  • 文档指出,“忽略的错误可能会生成警告,尽管重复键错误不会。” 因此,您甚至不一定知道使用此关键字时会出现哪些警告!
  • 对我来说,usingINSERT IGNORE说,“我不知道如何以正确的方式做到这一点,所以我只会以错误的方式去做。”

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

于 2014-09-18T21:26:23.190 回答
1

在对 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......我把它留给你作为“家庭作业”。阅读有关insert ... on duplicate key update.

于 2014-09-18T17:33:05.607 回答