本文内容仅在 MariaDB-10.2.15 版本下验证,其它环境下可能略有差异。
当数据库中存量数据较多时,或者是在批量插入操作时,很容易出现插入重复数据的问题。
在 mysql 中,当存在主键冲突或唯一键冲突的情况下,根据插入策略不同,一般有以下三种避免方法:
insert ignore into
:若没有则插入,若存在则忽略replace into
:若没有则正常插入,若存在则先删除后插入insert into ... on duplicate key update
:若没有则正常插入,若存在则更新注意,使用以上方法的前提是表中有一个 PRIMARY KEY
或 UNIQUE
约束/索引,否则,使用以上三个语句没有特殊意义,与使用单纯的 INSERT INTO
效果相同。
在具体介绍三种方法的使用方式之前,均使用如下语句初始化表数据:
"-- 创建表 ljtest --"
CREATE TABLE IF NOT EXISTS ljtest (
id INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, -- 自增主键 id
emp_no INT(11) NOT NULL,
title VARCHAR(50) NOT NULL,
from_date DATE NOT NULL,
to_date DATE DEFAULT NULL)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
"-- 插入三条数据 --"
INSERT INTO ljtest VALUES
('1', '10001', 'Senior Engineer', '1986-06-26', '9999-01-01'),
('2', '10002', 'Staff', '1996-08-03', '9999-01-01'),
('3', '10003', 'Senior Engineer', '1995-12-03', '9999-01-01'),
"-- 查看表数据 --"
select * from ljtest;
+----+--------+-----------------+------------+------------+
| id | emp_no | title | from_date | to_date |
+----+--------+-----------------+------------+------------+
| 1 | 10001 | Senior Engineer | 1986-06-26 | 9999-01-01 |
| 2 | 10002 | Staff | 1996-08-03 | 9999-01-01 |
| 3 | 10003 | Senior Engineer | 1995-12-03 | 9999-01-01 |
+----+--------+-----------------+------------+------------+
3 rows in set (0.00 sec)
"-- 查看此时初始状态下的表结构 --"
show create table ljtest\G
*************************** 1. row ***************************
Table: ljtest
Create Table: CREATE TABLE `ljtest` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`emp_no` int(11) NOT NULL,
`title` varchar(50) NOT NULL,
`from_date` date NOT NULL,
`to_date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
注意: 初始状态下 AUTO_INCREMENT=4
insert ignore into
insert ignore
会根据主键或者唯一键判断,忽略数据库中已经存在的数据insert into
一样1. 同时向表中插入两条包含主键的数据:id = 2(表中已有),id = 4(表中没有)
> insert ignore into ljtest(id, emp_no) values (2, 20002), (4, 40004);
Query OK, 1 row affected, 3 warnings (0.00 sec)
Records: 2 Duplicates: 1 Warnings: 3
注意:
insert ignore into
与普通 insert into
的使用方法几乎完全一样
ERROR 1136 (21S01): Column count doesn't match value count at row 1
ERROR 1364 (HY000): Field 'title' doesn't have a default value
1 row affected
:插入成功一条数据3 warnings
:有三条警告信息Duplicates: 1
:有一条重复数据2. 查看 warning 警告信息
> show warnings;
+---------+------+------------------------------------------------+
| Level | Code | Message |
+---------+------+------------------------------------------------+
| Warning | 1364 | Field 'title' doesn't have a default value |
| Warning | 1364 | Field 'from_date' doesn't have a default value |
| Warning | 1062 | Duplicate entry '2' for key 'PRIMARY' |
+---------+------+------------------------------------------------+
3 rows in set (0.00 sec)
其中的关键信息就是告诉我们主键为 2 的数据重复了。
3. 查看插入后的表数据
> select * from ljtest;
+----+--------+-----------------+------------+------------+
| id | emp_no | title | from_date | to_date |
+----+--------+-----------------+------------+------------+
| 1 | 10001 | Senior Engineer | 1986-06-26 | 9999-01-01 |
| 2 | 10002 | Staff | 1996-08-03 | 9999-01-01 |
| 3 | 10003 | Senior Engineer | 1995-12-03 | 9999-01-01 |
| 4 | 40004 | | 0000-00-00 | NULL |
+----+--------+-----------------+------------+------------+
4 rows in set (0.00 sec)
其中,id = 2 的记录还是之前的数据,id = 4 的数据是新插入的。
即,insert ignore into
会忽略已存在数据(按主键或唯一键判断),只插入新数据。
4. 查看插入后的表结构
> show create table ljtest\G
*************************** 1. row ***************************
Table: ljtest
Create Table: CREATE TABLE `ljtest` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`emp_no` int(11) NOT NULL,
`title` varchar(50) NOT NULL,
`from_date` date NOT NULL,
`to_date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
注意: 插入后 AUTO_INCREMENT=5
(从初始 4 增加为 5),这是正常的。
先重建表数据,对 emp_no 字段增加 unique 约束,并初始化同样的三条数据。
1. 向表中插入一条包含唯一键冲突的数据:emp_no = 10003(表中已有)
insert ignore ljtest(emp_no, title, from_date) values (10003, 'ignore test 3', '0000-00-00');
Query OK, 0 rows affected, 1 warning (0.00 sec)
2. 查看插入后的表数据
跟原数据一样,未发生任何变化。
3. 查看插入后的表结构
> show create table ljtest\G
*************************** 1. row ***************************
Table: ljtest
Create Table: CREATE TABLE `ljtest` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`emp_no` int(11) NOT NULL,
`title` varchar(50) NOT NULL,
`from_date` date NOT NULL,
`to_date` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `emp_no` (`emp_no`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
注意: 虽然未插入成功,但 AUTO_INCREMENT=5
(从初始 4 增加为 5),再次新增数据时,id 将会从 5 开始。
insert into ... on duplicate key update
insert into
语句末尾指定 on duplicate key update
,会根据主键或者唯一键判断:
update
insert into
一样1. 同时向表中插入两条包含主键的数据:id = 2(表中已有),id = 4(表中没有)
> insert into ljtest(id, emp_no, title, from_date) values (2, 20002, 'insert test 2', '0000-00-00'), (4, 40004, 'insert test 4', '0000-00-00') on duplicate key update emp_no=values(emp_no), title=values(title);
Query OK, 3 rows affected (0.00 sec)
Records: 2 Duplicates: 1 Warnings: 0
其中:
insert into
的使用语法差别不大,只是语句结尾有所区别emp_no=values(emp_no), title=values(title)
:
emp_no
和 title
值替换原数据emp_no=emp_no+1, title=title+"test"
3 row affected
:
2. 查看插入后的表数据
> select * from ljtest;
+----+--------+-----------------+------------+------------+
| id | emp_no | title | from_date | to_date |
+----+--------+-----------------+------------+------------+
| 1 | 10001 | Senior Engineer | 1986-06-26 | 9999-01-01 |
| 2 | 20002 | insert test 2 | 1996-08-03 | 9999-01-01 |
| 3 | 10003 | Senior Engineer | 1995-12-03 | 9999-01-01 |
| 4 | 40004 | insert test 4 | 0000-00-00 | NULL |
+----+--------+-----------------+------------+------------+
4 rows in set (0.00 sec)
其中,id = 2 的记录中 emp_no 和 title 字段被替换成新的数据,其它字段还是原来的值。
id = 4 的数据是新插入的。
3. 查看插入后的表结构
> show create table ljtest\G
*************************** 1. row ***************************
Table: ljtest
Create Table: CREATE TABLE `ljtest` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`emp_no` int(11) NOT NULL,
`title` varchar(50) NOT NULL,
`from_date` date NOT NULL,
`to_date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
注意: 插入后 AUTO_INCREMENT=5
(从初始 4 增加为 5),这是正常的。
先重建表数据,对 emp_no 字段增加 unique 约束,并初始化同样的三条数据。
1. 向表中插入一条包含唯一键冲突的数据:emp_no = 10003(表中已有)
insert into ljtest(emp_no, title, from_date) values (10003, 'insert test 3', '0000-00-00') on duplicate key update title=values(title);
Query OK, 2 rows affected (0.00 sec)
2. 查看插入后的表数据
+----+--------+-----------------+------------+------------+
| id | emp_no | title | from_date | to_date |
+----+--------+-----------------+------------+------------+
| 1 | 10001 | Senior Engineer | 1986-06-26 | 9999-01-01 |
| 2 | 10002 | Staff | 1996-08-03 | 9999-01-01 |
| 3 | 10003 | insert test 3 | 1995-12-03 | 9999-01-01 |
+----+--------+-----------------+------------+------------+
3 rows in set (0.00 sec)
其中,emp_no = 10003
记录的 title 被更新了,但主键 id 不变,即更新了原记录的指定列数据。
3. 查看插入后的表结构
> show create table ljtest\G
*************************** 1. row ***************************
Table: ljtest
Create Table: CREATE TABLE `ljtest` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`emp_no` int(11) NOT NULL,
`title` varchar(50) NOT NULL,
`from_date` date NOT NULL,
`to_date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
注意: 插入后 AUTO_INCREMENT=5
(从初始 4 增加为 5),再次新增数据时,id 将会从 5 开始。
replace into
replace into
会根据主键或者唯一键判断:
delete + insert
AUTO_INCREMENT
不一致。 insert into
一样1. 同时向表中插入两条包含主键的数据:id = 2(表中已有),id = 4(表中没有)
> replace into ljtest(id, emp_no, title, from_date) values (2, 20002, 'replace test 2', '0000-00-00'), (4, 40004, 'replace test 4', '0000-00-00');
Query OK, 3 rows affected (0.00 sec)
Records: 2 Duplicates: 1 Warnings: 0
注意:
replace into
与普通 insert into
的使用语法几乎一样3 row affected
:删除一条数据,插入两条数据Duplicates: 1
:有一条重复数据2. 查看插入后的表数据
> select * from ljtest;
+----+--------+-----------------+------------+------------+
| id | emp_no | title | from_date | to_date |
+----+--------+-----------------+------------+------------+
| 1 | 10001 | Senior Engineer | 1986-06-26 | 9999-01-01 |
| 2 | 20002 | replace test 2 | 0000-00-00 | NULL |
| 3 | 10003 | Senior Engineer | 1995-12-03 | 9999-01-01 |
| 4 | 40004 | replace test 4 | 0000-00-00 | NULL |
+----+--------+-----------------+------------+------------+
4 rows in set (0.00 sec)
其中,id = 2 的记录被替换成新的数据、且 to_date 字段原数据丢失,id = 4 的数据是新插入的。
4. 查看插入后的表结构
> show create table ljtest\G
*************************** 1. row ***************************
Table: ljtest
Create Table: CREATE TABLE `ljtest` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`emp_no` int(11) NOT NULL,
`title` varchar(50) NOT NULL,
`from_date` date NOT NULL,
`to_date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
注意:插入后 AUTO_INCREMENT=6
(从初始 4 增加为 5),这是正常的。
先重建表数据,对 emp_no 字段增加 unique 约束,并初始化同样的三条数据。
1. 向表中插入一条包含唯一键冲突的数据:emp_no = 10003(表中已有)
replace into ljtest(emp_no, title, from_date) values (10003, 'replace test 3', '0000-00-00');
Query OK, 2 rows affected (0.00 sec)
2. 查看插入后的表数据
+----+--------+-----------------+------------+------------+
| id | emp_no | title | from_date | to_date |
+----+--------+-----------------+------------+------------+
| 1 | 10001 | Senior Engineer | 1986-06-26 | 9999-01-01 |
| 2 | 10002 | Staff | 1996-08-03 | 9999-01-01 |
| 4 | 10003 | replace test 3 | 0000-00-00 | NULL |
+----+--------+-----------------+------------+------------+
3 rows in set (0.00 sec)
其中,emp_no = 10003
记录的 title 被更新了,同时主键 id 也变成 4 ,即执行了 delete + insert
操作。
3. 查看插入后的表结构
> show create table ljtest\G
*************************** 1. row ***************************
Table: ljtest
Create Table: CREATE TABLE `ljtest` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`emp_no` int(11) NOT NULL,
`title` varchar(50) NOT NULL,
`from_date` date NOT NULL,
`to_date` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
注意: 插入后 AUTO_INCREMENT=5
(从初始 4 增加为 5),再次新增数据时,id 将会从 5 开始。
总结:
AUTO_INCREMENT
不连续问题,且这种不连续不会同步更新到 slave 的 AUTO_INCREMENT
replace into
方法可能会导致部分数据丢失。所以,实际使用时,若是唯一键冲突的情况,一定要谨慎,避免踩坑!