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 服务器按以下顺序执行操作:
- 解析
INSERT
语句并开始事务。*
- 插入第一个元组。
- 插入第二个元组(违反键约束)。
- 回滚事务。
- 向 发送错误消息
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
将导致语句期间发生的任何错误都被忽略,而不仅仅是您认为不关心的任何错误。
- 文档指出,“忽略的错误可能会生成警告,尽管重复键错误不会。” 因此,您甚至不一定知道使用此关键字时会出现哪些警告!
- 对我来说,using
INSERT IGNORE
说,“我不知道如何以正确的方式做到这一点,所以我只会以错误的方式去做。”
我INSERT IGNORE
有时会使用,但是当文档明确告诉您做某事的“正确方法”时,请不要自欺欺人。先这样试试;如果您仍然有充分的理由以错误的方式进行操作并冒着破坏数据完整性并永远破坏一切的风险,那么至少您已经做出了明智的决定。