场景:
在日常的开发中,经常会遇到如下的场景:查看某条记录是否存在,不存在的话创建一条心记录,存在的话更新某些字段。一般的处理方式如下:
$result = mysql_query('select * from xxx where id = 1');
$row = mysql_fetch_assoc($result);
if($row){
mysql_query('update ...');
}else{
mysql_query('insert ...');
}
这样的写法可能有如下几点缺陷:
- 少量的性能消耗。执行了两次sql,按照一条sql一去一回两次网络传输的话,那么这就是4次。
- 麻烦。一个很简单的逻辑缺要写十来行代码。
- 在高并发下会出问题。比如当我们获取到了需要的数据,在更新之前,有另外一个请求恰好删除了该条记录,我们的更新操作就没起到作用;再或者,如果我们更新操作的写法有问题,比如更新列a,我们使用a = $row[a] + 1而不是a = a +1这种原子性的操作,有可能别的请求已经修改过了该字段,从而造成数据出错。
不过,MySql考虑到了这点,提供了insert into … on duplicate key update
的语法,该语法在insert
的时候,如果insert的数据会引起唯一索引(包括主键索引)的冲突,即这个唯一值重复了,则不会执行insert操作,而执行后面的update操作。
例如,现在有表test,test表中有字段a,在a上有主键或者唯一索引,并且表中只有一条a=1, b=1
的数据,现在执行如下的sql:
insert into test (a,b) values (1,2) on duplicate key update b = b = 1;
因为a=1的记录已存在了,所以不会执行insert,而会在该条记录上执行update语言b=b+1
,记录会变成a=1,b=2
insert into test (a,b) values (2,2) on duplicate key update b = b + 1;
a=2
的记录不存在,所以执行insert
这样我们就无需在应用程序里面再去判断记录是否存在了,也无需关系高并发下数据出错的情况了。
如果行作为新记录被插入,则受影响的行为1;如果原有记录被更新,则受影响行为2;如果原有记录已存在,但是更新的值和原有值相同,则受影响行为0。
多唯一索引冲突
为了测试方便,我们建了下面的数据表:
create table test(
a int not null primary key,
b int not null UNIQUE key,
c int not null
)
为了测试两个唯一索引都冲突的情况,然后插入下面的数据:
insert into test values(1,1,1), (2,2,2);
然后执行:
insert into test values(1,2,3) on duplicate key update c = c + 1;
因为a和b都是唯一索引,插入的数据在两条记录上产生了冲突,然而执行后只有第一条记录被修改:
mysql> select * from test;
+---+---+---+
| a | b | c |
+---+---+---+
| 1 | 1 | 2 |
| 2 | 2 | 2 |
+---+---+---+
2 rows in set (0.00 sec)
上面的语句等同于:
update test set c=c+1 where a=1 or b = 2 limit 1;
如果a=1 or b =2匹配多条记录,只有第一条记录被更新。所以,一般情况下,我们应该避免在有多个唯一索引的表中使用on duplicate key update
。
使用values()方法
在update中可以使用values()方法引用在insert中的值,如:
insert into test values(1,3,5) on duplicate key update c = values( c )+ 1;
该语句会使a=1的记录中c字段的值更新为6,因为values(c)的值是引用的insert部分的值,在这个例子中就是insert into test values(1,3,5) 中的5,所以最终更新的值为6。
last_insert_id()
如果表含有auto_increment字段,使用insert … on duplicate key update
插入或更新后,last_insert_id()
返回auto_increment
字段的值。
并发控制
在使用例如MyISAM这样的表级锁的分区表上使用insert … on duplicate key update时,会锁住所有分区表,而在例如使用InnoDB这样的行级锁的分区表上则不会锁住所有分区表。
delayed选项
delayed选项会被忽略,当我们使用on duplicate key update时。
INSERT ON DUPLICATE KEY UPDATE 几个要注意的问题
1. rows affected 是多少?
根据官方文档:
For INSERT … ON DUPLICATE KEY UPDATE statements, the affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated, and 0 if an existing row is set to its current values. If you specify the CLIENT_FOUND_ROWS flag, the affected-rows value is 1 (not 0) if an existing row is set to its current values.
总结下,insert 1,update 2,update 的值和原来的值一样 0。但如果通过 JDBC 调用,最后一种情况也会返回1,这是因为客户端连接时如果设置了 CLIENT_FOUND_ROWS 标志,会用 rows found 代替 rows affected 当做返回值,而JDBC默认是会设置该标志的。在 JDBC 连接字符串中指定 useAffectedRows=true
可以取消这个flag。
useAffectedRows
Don’t set the CLIENT_FOUND_ROWS flag when connecting to the server (not JDBC-compliant, will break most applications that rely on “found” rows vs. “affected rows” for DML statements), but does cause “correct” update counts from “INSERT … ON DUPLICATE KEY UPDATE” statements to be returned by the server.
Default: false
Since version: 5.1.7
2. 执行后,select LAST_INSERT_ID()
返回什么?
上官方文档:
If a table contains an AUTO_INCREMENT column and INSERT … UPDATE inserts a row, the LAST_INSERT_ID() function returns the AUTO_INCREMENT value. If the statement updates a row instead, LAST_INSERT_ID() is not meaningful.
insert:返回刚刚生成的自增ID,update:返回值无意义,MyBatis实测返回0而不是null,因此DAO这么写:
@Insert(" INSERT INTO ... ON DUPLICATE KEY UPDATE ...")@SelectKey(statement = "SELECT LAST_INSERT_ID() AS id", keyProperty = "id", before = false, resultType = Long.class)int insert(Entity obj);
然后调用方试图通过观察 id 是否为null,推测是发生了更新还是插入,这是行不通的。
要获得插入或者更新的ID,可以在SQL执行完后,利用唯一索引再把id查出来:
@Insert(" INSERT INTO t(v) VALUES(#{v}) ON DUPLICATE KEY UPDATE v=#{v}")@SelectKey(statement = "SELECT id FROM t WHERE v=#{v}", keyProperty = "id", before = false, resultType = Long.class)int insert(Entity obj);
3. AUTO_INCREMENT 字段的GAP
根据官方文档的描述,INSERT ON DUPLICATE KEY UPDATE
属于 Mixed-mode inserts,只分析SQL无法知道需要几个自增id,在 innodb_autoinc_lock_mode
为 1(默认值)或 2 时,这类 insert 不走 AUTO_INC 表级锁,而是用一个轻量级的 mutex,一次性分配最坏情况下所需要的自增id,至于用不用的完就不管了。上锁的只是分配id的过程,不会锁整个sql语句,这样一来提高了并发度,但代价是和后续insert分配的自增id之间可能存在空洞。具体到INSERT ON DUPLICATE KEY UPDATE
,即使最终执行了 update,自增ID也是会增长的,不过这一般不是问题。