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 服务器按以下顺序执行操作:
- 解析
INSERT 语句并启动事务。*
- 插入第一个元组。
- 插入第二个元组(违反键约束)。
- 回滚交易。
- 向
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,但是当文档明确告诉您做某事的“正确方法”时,请不要自欺欺人。先这样试试;如果您仍然有充分的理由以错误的方式进行操作并冒着破坏数据完整性和永远毁掉一切的风险,那么至少您已经做出了明智的决定。