通常情況下,我們會使用以下SQL語句來更新字段值:
UPDATE mytable SET myfield='value' WHERE other_field='other_value';
但是,如果你想更新多行數據,並且每行記錄的各字段值都是各不一樣,你會怎麼辦呢?剛開始你可能會想到使用循環執行多條UPDATE語句的方式,就像以下的python程序示例:
for x in xrange(10):
sql = ''' UPDATE mytable SET myfield='value' WHERE other_field='other_value'; '''
這種方法並沒有什麼任何錯誤,並且代碼簡單易懂,但是在循環語句中執行了不止一次SQL查詢,在做系統優化的時候,我們總是想盡可能的減少數據庫查詢的次數,以減少資源佔用,同時可以提高系統速度
1、replace into 批量更新(UNIQUE索引或PRIMARY KEY)
replace into test_tbl (id,dr) values (1,'2'),(2,'3'),...(x,'y');
2、insert into ...on duplicate key update批量更新
- 導致在一個UNIQUE索引或PRIMARY KEY中出現重複值,則在出現重複值的行執行UPDATE。如果不會導致唯一值列重複的問題,則插入新行。
- values(col_name)函數只是取當前插入語句中的插入值,並沒有累加功能。
- 每次更新都會更新該表的自增主鍵ID,如果更新頻率很快,會導致主鍵ID自增的很快,過段時間就超過數字類型的的範圍了,解決方法:innodb_autoinc_lock_mode=0
- 事務隔離級別REPEATABLE-READ下會發生死鎖。
創建案例表 word_count_0626(單詞計數表)
CREATE TABLE IF NOT EXISTS word_count_0626 (
id int(11) NOT NULL AUTO_INCREMENT,
word varchar(64) NOT NULL,
count int(11) DEFAULT 0,
date date NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY word (word, date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1. 執行第一次:(首次數據庫表中沒有數據,正常插入)
insert into word_count_0626 (word, count, date) values
('a',5,curdate())
on duplicate key update count=values(count);
# 結果顯示:
id word count date
1 a 5 2019-06-26
2. 執行第二次:(與第一次的唯一(word,date)衝突,執行更新)
insert into word_count_0626 (word, count, date) values
('a',6,curdate())
on duplicate key update count=values(count);
# 結果顯示:
id word count date
1 a 6 2019-06-26 (更新)
3. 執行第三次:
insert into word_count_0626 (word, count, date) values
('a',6,curdate()-1), // 取前一天,不會衝突
on duplicate key update count=values(count);
# 結果顯示:
id word count date
3 a 6 2019-06-25 (新插入,ID跳躍了)
final public function updateBatch($tableName, array $data): bool
{
$result = false;
$tableName = $tableName ?: $this->tableName;
if (!empty($data) && is_array($data) && count($data) !== count($data, COUNT_RECURSIVE)) {
try {
$keys = array_keys(current($data));
$items = $updates = [];
foreach ($data as $item) {
if (array_keys($item) === $keys) {
$items[] = sprintf("('%s')", implode("','", array_values($item)));
} else {
$items = false;
break;
}
foreach ($keys as $keyName) {
$updates[] = "`$keyName`='{$item[$keyName]}'";
}
}
$sql = sprintf('insert into `%s` (`%s`) values %s ON DUPLICATE KEY UPDATE %s', $tableName, implode('`,`', $keys), implode(',', $items), implode(',', $updates));
if (!empty($items)) {
$this->conn->exec($sql);
$result = true;
}
} catch (\Exception $exception) {
die($exception->getMessage());
}
}
return $result;
}
3、使用mysql 自帶case when構建批量更新
UPDATE book
SET Author = CASE id
WHEN 1 THEN '黃飛鴻'
WHEN 2 THEN '方世玉'
WHEN 3 THEN '洪熙官'
END
WHERE id IN (1,2,3)
程序實現:case when的方式經過測試建議將修改記錄條數控制在1W左右,不要超過2W,否則會耗費的時間也是成倍增加的
<?php
/**
* 批量更新函數
* @param $data array 待更新的數據,二維數組格式
* @param $table string 更新的表名
* @param string $field string 值不同的條件,默認爲id
* @param array $params array 值相同的條件,鍵值對應的一維數組
* @return bool|string
*/
function batchUpdateUseCaseWhen($data, $table, $field, $params = [])
{
if (!is_array($data) || !$field || !is_array($params)) {
return false;
}
$updates = parseUpdate($data, $field);
$where = parseParams($params);
// 獲取所有鍵名爲$field列的值,值兩邊加上單引號,保存在$fields數組中
$fields = array_column($data, $field);
$fields = implode(',', array_map(function ($value) {
return "'" . $value . "'";
}, $fields));
$sql = sprintf("UPDATE `%s` SET %s WHERE `%s` IN (%s) %s", $table, $updates, $field, $fields, $where);
return $sql;
}
/**
* 將二維數組轉換成CASE WHEN THEN的批量更新條件
* @param $data array 二維數組
* @param $field string 列名
* @return string sql語句
*/
function parseUpdate($data, $field)
{
$sql = '';
$keys = array_keys(current($data));
foreach ($keys as $column) {
$sql .= sprintf("`%s` = CASE `%s`", $column, $field);
foreach ($data as $line) {
$sql .= sprintf("WHEN '%s' THEN '%s'", $line[$field], $line[$column]);
}
$sql .= "END,";
}
return rtrim($sql, ',');
}
/**
* 解析where條件
* @param $params
* @return array|string
*/
function parseParams($params)
{
$where = [];
foreach ($params as $key => $value) {
$where[] = sprintf("`%s` = '%s'", $key, $value);
}
return $where ? ' AND ' . implode(' AND ', $where) : '';
}
以上方案大大減少了數據庫的查詢操作次數,大大節約了系統資源 不過這個有個缺點 : 要注意的問題是SQL語句的長度,需要考慮程序運行環境所支持的字符串長度,當然這也可以更新mysql的設置來擴展。
show variables like 'max_allowed_packet';
vim /etc/my.cnf
[mysqld]
max_allowed_packet = 100M
4 創建臨時表,先更新臨時表,然後從臨時表中聯表update
create temporary table tmp(id int(4) primary key,dr varchar(50));
insert into tmp values (0,'gone'), (1,'xx'),...(m,'yy');
update test_tbl, tmp set test_tbl.dr=tmp.dr where test_tbl.id=tmp.id;
注意:這種方法需要用戶有temporary 表的create 權限。臨時表在建立連接時可見,關閉時表結構和表數據都沒了;臨時表可以通過show create table table_name查看。
一般在數據量比較大的查詢中,用in()等查詢條件,會嚴重影響查詢效率。
這時可以用 create temporary table table_name select id,name from table 創建臨時表
5. insert into table select * from table_b