@eternity
2016-05-22T09:34:30.000000Z
字数 7092
阅读 7298
work
在平常的开发中,经常碰到这种更新数据的场景:先判断某一数据在库表中是否存在,存在则 update,不存在则 insert。
如果使用Hibernate,它自带saverOrUpdate方法,用起来很方便,但如使用 SQL 语句呢?
最常见的写法是,先通过 select 语句查询记录是否存在,存在则使用 update 语句更新,不存在则使用 insert 语句插入。
但是这样做明显不够优雅,存在几个问题:
Oracle下有 merge 的语法, MySQL 针对这种场景,在标准 SQL 下也有自己的拓展语法。
数据不存在则插入,存在则无操作。
在 INSERT 语句中使用 IGNORE 关键字实现数据不存在则插入,存在则无操作。它的实现逻辑是,当插入语句出现主键冲突,或者唯一键冲突时,不抛出错误,直接忽略这条插入语句。官网上的相关介绍如下:
If you use the IGNORE keyword, errors that occur while executing the INSERT statement are ignored. For example, without IGNORE, a row that duplicates an existing UNIQUE index or PRIMARY KEY value in the table causes a duplicate-key error and the statement is aborted. With IGNORE, the row is discarded and no error occurs. Ignored errors may generate warnings instead, although duplicate-key errors do not.
MySQL 官方文档提供的标准语法:
INSERT IGNORE
INTO tbl_name
[PARTITION (partition_name,...)]
[(col_name,...)]
{VALUES | VALUE} ({expr | DEFAULT},...),(...),...
或者
INSERT IGNORE
[INTO] tbl_name
[PARTITION (partition_name,...)]
[(col_name,...)]
SELECT ...
可见除了多了个 IGNORE 关键字以外,跟一般 INSERT 语句并无区别。
1.建一张测试用的表
CREATE TABLE `test_tab` (
`name` varchar(64) NOT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.插入一条数据
INSERT INTO `test_tab` (`name`,`age`) values ('zhangsan',24)
当前test_tab表的数据为:
name | age |
---|---|
zhangsan | 24 |
3.再执行一次步骤2的插入语句,则会报异常:
[Err] 1062 - Duplicate entry 'zhangsan' for key 'PRIMARY'
4.对步骤2的 INSERT 语句增加 IGNORE 关键字,则不会报异常,已存在的数据也不会被更新。
INSERT IGNORE INTO `test_tab` (`name`,`age`) values ('zhangsan',24) ;
------
语句执行情况:
受影响的行: 0
时间: 0.000s
当前 test_tab 表的数据为:
name | age |
---|---|
zhangsan | 24 |
在 INSERT 语句中使用 ON DUPLICATE KEY UPDATE 关键字实现数据不存在则插入,存在则更新的操作。判断数据重复的逻辑依然是主键冲突或者唯一键冲突。
官网上的相关介绍如下:
if you specify ON DUPLICATE KEY UPDATE, and a row is inserted that would cause a duplicate value in a UNIQUE index or PRIMARY KEY, an UPDATE of the old row is performed. 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.
MySQL 官方文档中提供标准的语法:
INSERT
[INTO] tbl_name
[PARTITION (partition_name,...)]
[(col_name,...)]
{VALUES | VALUE} ({expr | DEFAULT},...),(...),...
[ ON DUPLICATE KEY UPDATE
col_name=expr
[, col_name=expr] ... ]
或者:
INSERT
[INTO] tbl_name
[PARTITION (partition_name,...)]
SET col_name={expr | DEFAULT}, ...
[ ON DUPLICATE KEY UPDATE
col_name=expr
[, col_name=expr] ... ]
或者:
INSERT
[INTO] tbl_name
[PARTITION (partition_name,...)]
[(col_name,...)]
SELECT ...
[ ON DUPLICATE KEY UPDATE
col_name=expr
[, col_name=expr] ... ]
可见,还是原来 INSERT 语句的写法。
1.使用刚才新建的 test_tab 表,此时表中的数据如下:
name | age |
---|---|
zhangsan | 24 |
2.使用主键相同的 insert 语句,仍然会 duplicate key 错误
INSERT INTO `test_tab` (`name`,`age`) values ('zhangsan',50) ;
------------
[Err] 1062 - Duplicate entry 'zhangsan' for key 'PRIMARY'
3.对刚才的 INSERT 语句添加 ON DUPLICATE KEY UPDATE
... 关键字:
INSERT INTO `test_tab` (`name`,`age`) values ('zhangsan',50)
ON DUPLICATE KEY UPDATE `age`=50 ;
------------
受影响的行: 2
时间: 0.025s
4.此时主键为'zhangsan'的数据,age字段已被更新:
name | age |
---|---|
zhangsan | 50 |
5.当然,如果主键不冲突,效果跟一般插入语句是一样的:
INSERT INTO `test_tab` (`name`,`age`) values ('lisi',30)
ON DUPLICATE KEY UPDATE `age`=30 ;
------------
受影响的行: 1
时间: 0.009s
name | age |
---|---|
zhangsan | 50 |
lisi | 30 |
saveOrUpdate 在 MySQL 中还有另一种实现,即 REPLACE INTO 语句,它用起来有点像 Oracle 的 Merge。判断数据重复的逻辑依然是主键或者唯一键冲突。 MySQL 官方文档中提供标准的语法:
REPLACE [LOW_PRIORITY | DELAYED]
[INTO] tbl_name
[PARTITION (partition_name,...)]
[(col_name,...)]
{VALUES | VALUE} ({expr | DEFAULT},...),(...),...
或:
REPLACE [LOW_PRIORITY | DELAYED]
[INTO] tbl_name
[PARTITION (partition_name,...)]
SET col_name={expr | DEFAULT}, ...
或:
REPLACE [LOW_PRIORITY | DELAYED]
[INTO] tbl_name
[PARTITION (partition_name,...)]
[(col_name,...)]
SELECT ...
1.仍然使用上面的 test_tab 表的数据,此时数据如下
name | age |
---|---|
zhangsan | 50 |
lisi | 30 |
2.使用一般的insert语句插入 name=zhangsan 的数据,报主键冲突。但是换成 REPLACE INTO ... 语句则没问题:
REPLACE INTO `test_tab` (`name`,`age`) VALUES ('zhangsan',30) ;
------------
受影响的行: 2
时间: 0.009s
3.结果如下:
name | age |
---|---|
zhangsan | 30 |
lisi | 30 |
对于操作结果来说,很像是 SAVE OR UPDATE,但是实现方式与 INSERT 的“DUPLICATE KEY UPDATE”关键字不同。当使用 REPLACE INTO 语句时,对于重复的数据,是直接删除,然后再插入新数据的。所以它的更新其实不是 UPDATE,而是 DELETE->INSERT 。大多数情况下,使用 REPLACE INTO 完成更新操作并无问题,但是有一种场景必须特别注意:
所以当被更新表存在触发器的场景时,使用 INSERT 的“DUPLICATE KEY UPDATE”关键字更合适。
select * from pet;
+--------+---------+
| owner | species |
+--------+---------+
| Benny | bird |
| Diane | bird |
| Gwen | cat |
| Harold | cat |
| Adrian | dog |
| NULL | dog |
+--------+---------+
SELECT species, COUNT(*) FROM pet GROUP BY species;
+---------+----------+
| species | COUNT(*) |
+---------+----------+
| bird | 2 |
| cat | 2 |
| dog | 2 |
+---------+----------+
SELECT species, COUNT(owner) FROM pet GROUP BY species;
+---------+--------------+
| species | COUNT(owner) |
+---------+--------------+
| bird | 2 |
| cat | 2 |
| dog | 1 |
+---------+--------------+
COUNT(*) vs COUNT(VAL):
他们是等价的,INNODB 中都需要进行全表扫描,并使用合适的索引。INNODB 下看下该表是否有辅助索引,如果有辅助索引的话, count(*)操作会走覆盖索引,走覆盖索引速度会比较快,使用 EXPLAIN可以看到 Using index 。
COUNT(*) 与 SELECT(*) 中的*在语义上是不同的,SELECT(*) 因为不使用覆盖索引,所以不推荐使用。
MySQL 查询分析器对于 count(*) 优化在 5.6版本以后
COUNT(*) vs COUNT(COL) vs COUNT(VAL):
当两张表 inner join ,两张表记录是一对多的关系时,需要把多的表的记录合并成一行,例如:考试与考试标签 的关系,需要通过一次查询,获得考试信息,以及每个考试对应的所有标签信息,所有标签合并成一个字段,逗号分隔。
语法:
GROUP_CONCAT([DISTINCT] expr [,expr ...]
[ORDER BY {unsigned_integer | col_name | expr}
[ASC | DESC] [,col_name ...]]
[SEPARATOR str_val])
举个例子:
SELECT t.id as exam_id ,t.`name`
, GROUP_CONCAT(t2.label_id order by t2.label_id SEPARATOR ',') as label_ids
from center_exam.examination t ,center_exam.exam_label t2
where t.id = t2.biz_id
and t2.type = 'examination'
AND t.tenant_id = 1
GROUP BY t.id
MySQL GROUP_CONCAT 函数 官方手册
我们知道使用 WHERE ID IN (...) 语句时,如果 ID 有加索引,SQL 执行时是会使用索引的。
有一种说法是,当 IN 中查询的数量非常大时,MySQL 无法使用索引,需要使用一张临时表保存 id,再 join 临时表查询。
经过实际测试,发现MySQL 中 in 记录数超过总记录数的一定比例不会使用索引
另外,使用 id in (……)条件需要担心的是,MySQL 对单条语句的长度是有限制的,由 sysvar_max_allowed_packet 参数控制,默认为 1M 。
所以,一般情况下使用 id in (……) 是没有问题的,对于查询数据较大的情况,建议保留使用临时表的方式。
关于使用 in 条件与临时表性能差异的讨论
sysvar_max_allowed_packet说明
目前项目中使用的 jdbcTemplate.batchUpdate(),并没有做到真正的批处理,通过抓包分析,发现仍然是分多次 SQL 请求到数据库,要做到真正的批处理请求,需要在 jdbc 驱动的连接串中加上rewriteBatchedStatements=true
参数,这样batchUpdate()的多条语句才能合并成一个请求发送到数据库。