[轉] MySQL "replace into" 的坑 (5.5 ROW格式)

MySQL 對 SQL 有不少擴展,有些用起來很方便,但有一些被誤用以後會有性能問題,還會有一些意料以外的反作用,好比 REPLACE INTO。mysql

好比有這樣一張表:sql

1
2 3 4 5 6 7 8 
CREATE TABLE `auto` (  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  `k` int(10) unsigned NOT NULL,  `v` varchar(100) DEFAULT NULL,  `extra` varchar(200) DEFAULT NULL,  PRIMARY KEY (`id`),  UNIQUE KEY `uk_k` (`k`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1

auto 表有一個自增的 id 字段做爲主鍵,字段 k 有 UNIQUE KEY 作惟一性約束。寫入幾條記錄以後會是這樣:性能

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 
xupeng@diggle7:3600(dba_m) [dba] mysql> INSERT INTO auto (k, v, extra) VALUES (1, '1', 'extra 1'), (2, '2', 'extra 2'), (3, '3', 'extra 3'); Query OK, 3 rows affected (0.01 sec) Records: 3 Duplicates: 0 Warnings: 0  xupeng@diggle7:3600(dba_m) [dba] mysql> SHOW CREATE TABLE auto\G *************************** 1. row ***************************  Table: auto Create Table: CREATE TABLE `auto` (  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  `k` int(10) unsigned NOT NULL,  `v` varchar(100) DEFAULT NULL,  `extra` varchar(200) DEFAULT NULL,  PRIMARY KEY (`id`),  UNIQUE KEY `uk_k` (`k`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1 1 row in set (0.01 sec)  xupeng@diggle7:3600(dba_m) [dba] mysql> SELECT * FROM auto; +----+---+------+---------+ | id | k | v | extra | +----+---+------+---------+ | 1 | 1 | 1 | extra 1 | | 2 | 2 | 2 | extra 2 | | 3 | 3 | 3 | extra 3 | +----+---+------+---------+ 3 rows in set (0.00 sec)

在 slave 節點上是和 master 一致的:spa

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 
xupeng@diggle8:3600(dba_s) [dba] mysql> SELECT * FROM auto; +----+---+------+---------+ | id | k | v | extra | +----+---+------+---------+ | 1 | 1 | 1 | extra 1 | | 2 | 2 | 2 | extra 2 | | 3 | 3 | 3 | extra 3 | +----+---+------+---------+ 3 rows in set (0.00 sec)  xupeng@diggle8:3600(dba_s) [dba] mysql> SHOW CREATE TABLE auto\G *************************** 1. row ***************************  Table: auto Create Table: CREATE TABLE `auto` (  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  `k` int(10) unsigned NOT NULL,  `v` varchar(100) DEFAULT NULL,  `extra` varchar(200) DEFAULT NULL,  PRIMARY KEY (`id`),  UNIQUE KEY `uk_k` (`k`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1 1 row in set (0.00 sec)

能夠看到,寫入三條記錄以後,auto 表的 AUTO_INCREMENT 增加爲 4,也就是說下一條不手工爲 id 指定值的記錄,id 字段的值會是 4。code

接下來使用 REPLACE INTO 來寫入一條記錄:blog

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 
xupeng@diggle7:3600(dba_m) [dba] mysql> REPLACE INTO auto (k, v) VALUES (1, '1-1'); Query OK, 2 rows affected (0.01 sec)  xupeng@diggle7:3600(dba_m) [dba] mysql> SELECT * FROM auto; +----+---+------+---------+ | id | k | v | extra | +----+---+------+---------+ | 2 | 2 | 2 | extra 2 | | 3 | 3 | 3 | extra 3 | | 4 | 1 | 1-1 | NULL | +----+---+------+---------+ 3 rows in set (0.00 sec)  xupeng@diggle7:3600(dba_m) [dba] mysql> SHOW CREATE TABLE auto\G *************************** 1. row ***************************  Table: auto Create Table: CREATE TABLE `auto` (  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  `k` int(10) unsigned NOT NULL,  `v` varchar(100) DEFAULT NULL,  `extra` varchar(200) DEFAULT NULL,  PRIMARY KEY (`id`),  UNIQUE KEY `uk_k` (`k`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1 1 row in set (0.00 sec)

能夠看到 MySQL 說 「2 rows affected」,但是明明是隻寫一條記錄,爲何呢?這是由於 MySQL 在執行 REPLACE INTO auto (k) VALUES (1) 時首先嚐試 INSERT INTO auto (k) VALUES (1),但因爲已經存在一條 k=1 的記錄,發生了 duplicate key error,因而 MySQL 會先刪除已有的那條 k=1 即 id=1 的記錄,而後從新寫入一條新的記錄。table

這時候 slave 上出現了詭異的問題:ast

1
2 3 4 5 6 7 8 9 10 11 
xupeng@diggle8:3600(dba_s) [dba] mysql> SHOW CREATE TABLE auto\G *************************** 1. row ***************************  Table: auto Create Table: CREATE TABLE `auto` (  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,  `k` int(10) unsigned NOT NULL,  `v` varchar(100) DEFAULT NULL,  `extra` varchar(200) DEFAULT NULL,  PRIMARY KEY (`id`),  UNIQUE KEY `uk_k` (`k`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1

能夠知道,當前表內數據 id 字段的最大值是 4,AUTO_INCREMENT 應該爲 5,但在 slave 上 AUTO_INCREMENT 卻並未更新,這會有什麼問題呢?把這個 slave 提高爲 master 以後,因爲 AUTO_INCREMENT 比實際的 next id 還要小,寫入新記錄時就會發生 duplicate key error,每次衝突以後 AUTO_INCREMENT += 1,直到增加爲 max(id) + 1 以後才能恢復正常:class

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 
xupeng@diggle8:3600(dba_s) [dba] mysql> REPLACE INTO auto (k, v) VALUES (4, '4'); ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY' xupeng@diggle8:3600(dba_s) [dba] mysql> REPLACE INTO auto (k, v) VALUES (5, '5'); Query OK, 1 row affected (0.00 sec)  xupeng@diggle8:3600(dba_s) [dba] mysql> SELECT * FROM auto; +----+---+------+---------+ | id | k | v | extra | +----+---+------+---------+ | 2 | 2 | 2 | extra 2 | | 3 | 3 | 3 | extra 3 | | 4 | 1 | 1-1 | NULL | | 5 | 5 | 5 | NULL | +----+---+------+---------+ 4 rows in set (0.00 sec)

沒有預料到 MySQL 在數據衝突時其實是刪掉了舊記錄,再寫入新記錄,這是使用 REPLACE INTO 時最大的一個誤區,拿以前的例子來講,執行完 REPLACE INTO auto (k, v) VALUES (1, ‘1-1’) 以後,因爲新寫入記錄時並未給 extra 字段指定值,原記錄 extra 字段的值就「丟失」了,而一般這並不是是業務上所預期的,更常見的需求其實是,當存在 k=1 的記錄時,就把 v 字段的值更新爲 ‘1-1’,其餘未指定的字段則保持原狀,而知足這一需求的 MySQL 方言是 INSERT INTO auto (k, v) VALUES (1, ‘1-1’) ON DUPLICATE KEY UPDATE v=VALUES(v);擴展

鑑於此,不少使用 REPLACE INTO 的場景,實際上須要的是 INSERT INTO … ON DUPLICATE KEY UPDATE,在正確理解 REPLACE INTO 行爲和反作用的前提下,謹慎使用 REPLACE INTO。

 

原文地址:

https://blog.xupeng.me/2013/10/11/mysql-replace-into-trap/

相關文章
相關標籤/搜索