MySQL什么时候抛出主键错误?

Tom*_*mmy 4 python mysql pymysql

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

INSERT INTO TABLE VALUES (x,y,z),(x2,y2,z2),(x3,y3,z3);
Run Code Online (Sandbox Code Playgroud)

并且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
Run Code Online (Sandbox Code Playgroud)

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

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

Air*_*Air 5

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

虽然只有表引擎对于您在这里询问的关键约束确实很重要,但了解更大的图景很重要,因此我将花时间添加一些额外的细节。如果您很着急,请随时阅读下面的第一部分和最后一部分。

表引擎

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

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

SQL模式

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

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

在这些情况下,如果严格模式有效,MySQL 将抛出错误。但是,如果严格模式未生效,它通常会“修复”您的错误,这可能会导致各种潜在的有害行为(请参阅MySQL '截断不正确的整数值'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);
Run Code Online (Sandbox Code Playgroud)

由于严格模式未生效,因此插入所有九个值并且将无效字符串强制为整数也就不足为奇了。服务器足够聪明,可以识别'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)
Run Code Online (Sandbox Code Playgroud)

现在,尝试使用 再次执行此操作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)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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