技術分享 | 在MySQL對於批量更新操作的一種優化方式


歡迎來到 GreatSQL社區分享的MySQL技術文章,如有疑問或想學習的內容,可以在下方評論區留言,看到后會進行解答

作者:景雲麗、盧浩、宋源棟

  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯系小編並注明來源。

引言

批量更新數據,不同於這種 update a=a+1 where pk > 500,而是需要對每一行進行單獨更新 update a=1 where pk=1;update a=12 where pk=7;... 這樣連續多行update語句的場景,是少見的。

可以說是偶然也是一種必然,在GreatDB 5.0的開發過程中,我們需要對多語句批量update的場景進行優化。

兩種多行更新操作的耗時對比
在我們對表做多行更新的時候通常會遇到以下兩種情況

1.單語句批量更新(update a=a+1 where pk > 500)

2.多語句批量更新(update a=1 where pk=1;update a=12 where pk=7;...)

下面我們進行實際操作比較兩種場景,在更新相同行數時所消耗的時間。

數據准備

數據庫版本:MySQL 8.0.23

t1表,建表語句以及准備初始數據1000行

create  database if not exists test;
use test
##建表
create table t1(c1 int primary key,c2 int);
##創建存儲過程用於生成初始數據
DROP PROCEDURE IF EXISTS insdata;
DELIMITER $$
CREATE PROCEDURE insdata(IN beg INT, IN end INT) BEGIN
 WHILE beg <= end
 DO
 INSERT INTO test.t1 values (beg, end);
SET beg = beg+1;
END WHILE;
END $$
DELIMITER ;
##插入初始數據1000行
call insdata(1,1000)

1.單語句批量更新

更新語句

update  t1 set c2=10 where c1 <=1000;

執行結果

mysql> update  t1 set c2=10 where c1 <=1000;
Query OK, 1000 rows affected (0.02 sec)
Rows matched: 1000  Changed: 1000  Warnings: 0

2.多語句批量更新

以下腳本用於生成1000行update語句,更新c2的值等於1000以內的隨機數

#!/bin/bash

for i in {1..1000}
do
        echo "update t1 set c2=$((RANDOM%1000+1)) where c1=$i;" >> update.sql
done

生成sql語句如下

update t1 set c2=292 where c1=1;
update t1 set c2=475 where c1=2;
update t1 set c2=470 where c1=3;
update t1 set c2=68 where c1=4;
update t1 set c2=819 where c1=5;

... ....
update t1 set c2=970  where c1=1000;

因為source /ssd/tmp/tmp/1000/update.sql;執行結果如下,執行時間不易統計:

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

所以利用Linux時間戳進行統計:

#!/bin/bash

start_time=`date +%s%3N`
/ssd/tmp/mysql/bin/mysql -h127.0.0.1 -uroot -P3316 -pabc123 -e "use test;source /ssd/tmp/tmp/1000/update.sql;"
end_time=`date +%s%3N`
echo "執行時間為:"$(($end_time-$start_time))"ms"

執行結果:

[root@computer-42 test]# bash update.sh
mysql: [Warning] Using a password on the command line interface can be insecure.
執行時間為:4246ms

執行所用時間為:4246ms=4.246 sec

結果比較

file

總結

由上述例子我們可以看到,同樣是更新1000行數據。單語句批量更新與多語句批量更新的執行效率差距很大。

而產生這種巨大差異的原因,除了1000行sql語句本身的網絡與語句解析開銷外,影響性能的地方主要是以下幾個方面:

1.如果會話是auto_commit=1,每次執行update語句后都要執行commit操作。commit操作耗費時間較久,會產生兩次磁盤同步(寫binlog和寫redo日志)。在進行比對測試時,盡量將多個語句放到一個事務內,保證只提交一次事務。

2.向后端發送多語句時,后端每處理一個語句均會向client返回一個response包,進行一次交互。如果多語句使用一個事務的話,網絡io交互應該是影響性能的主要方面。之前在性能測試時發現網卡驅動占用cpu很高。

我們的目標是希望在更新1000行時,第二種場景的耗時能夠減少到一秒以內。

對第二種場景的優化

接下來我們來探索對更新表中多行為不同值時,如何提高它的執行效率。

簡單分析

從執行的update語句本身來說,兩種場景所用的表結構都進行了最大程度的簡化,update語句也十分簡單,且where條件為主鍵,理論上已經沒有優化的空間。

如果從其他方面來考慮,根據上述原因分析會有這樣三個優化思路:

1.減少執行語句的解析時間來提高執行效率
2.減少commit操作對性能的影響,盡量將多個語句放到一個事務內,保證只提交一次事務。
3.將多條語句合並成一條來提高執行效率

方案一:使用prepare語句,減小解析時間

以下腳本用於生成prepare執行語句

#!/bin/bash

echo "prepare pr1 from 'update test.t1 set c2=? where c1=?';" > prepare.sql
for i in {1..1000}
do
echo "set @a=$((RANDOM%1000+1)),@b=$i;" >>prepare.sql
echo "execute pr1 using @a,@b;" >> prepare.sql
done
echo "deallocate prepare pr1;" >> prepare.sql

生成語句如下

prepare pr1 from 'update test.t1 set c2=? where c1=?';
set @a=276,@b=1;
execute pr1 using @a,@b;
set @a=341,@b=2;
execute pr1 using @a,@b;
set @a=803,@b=3;
execute pr1 using @a,@b;
... ...
set @a=582,@b=1000;
execute pr1 using @a,@b;
deallocate prepare pr1;

執行語句

#!/bin/bash

start_time=`date +%s%3N`
/ssd/tmp/mysql/bin/mysql -h127.0.0.1 -uroot -P3316 -pabc123 -e "use test;source /ssd/tmp/tmp/test/prepare.sql;"
end_time=`date +%s%3N`
echo "執行時間為:"$(($end_time-$start_time))"ms"

執行結果:

[root@computer-42 test]# bash prepare_update_id.sh
mysql: [Warning] Using a password on the command line interface can be insecure.
執行時間為:4518ms

與優化前相比

file

很遺憾,執行總耗時反而增加了。

這里筆者有一點推測是由於原本一條update語句,被拆分成了兩條語句:

set @a=276,@b=1;
execute pr1 using @a,@b;

這樣在MySQL客戶端和MySQL進程之間的通訊次數增加了,所以增加了總耗時。

因為prepare預處理語句執行時只能使用用戶變量傳遞,以下執行語句會報錯

mysql> execute pr1 using 210,5;
ERROR 1064 (42000): You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version
for the right syntax to use near '210,5' at line 1

所以無法在語法方面將兩條語句重新合並,筆者便使用了以下另外一種執行方式

執行語句

#!/bin/bash

start_time=`date +%s%3N`
/ssd/tmp/mysql/bin/mysql -h127.0.0.1 -uroot -P3316 -pabc123  <<EOF
use test;
DROP PROCEDURE IF EXISTS pre_update;
DELIMITER $$
CREATE PROCEDURE pre_update(IN beg INT, IN end INT) BEGIN
 prepare pr1 from 'update test.t1 set c2=? where c1=?';
 WHILE beg <= end
 DO
 set  @a=beg+1,@b=beg;
 execute pr1 using @a,@b;
 SET beg = beg+1;
END WHILE;
deallocate prepare pr1;
END $$
DELIMITER ;
call pre_update(1,1000);
EOF
end_time=`date +%s%3N`
echo "執行時間為:"$(($end_time-$start_time))"ms"

執行結果:

[root@computer-42 test]# bash prepare_update_id.sh
mysql: [Warning] Using a password on the command line interface can be insecure.
執行時間為:3862ms

與優化前相比:

file

這樣的優化幅度符合prepare語句的理論預期,但仍舊不夠理想。

方案二:多個update語句放到一個事務內執行,最終commit一次

以下腳本用於生成1000行update語句在一個事務內,更新c2的值等於1000以內的隨機數

#!/bin/bash
echo "begin;" > update.sql
for i in {1..1000}
do
        echo "update t1 set c2=$((RANDOM%1000+1)) where c1=$i;" >> update.sql
done
echo "commit;" >> update.sql

生成sql語句如下

begin;
update t1 set c2=279 where c1=1;
update t1 set c2=425 where c1=2;
update t1 set c2=72 where c1=3;
update t1 set c2=599 where c1=4;
update t1 set c2=161 where c1=5;

... ....
update t1 set c2=775  where c1=1000;
commit;

執行時間統計的方法,同上

[root@computer-42 test]# bash update.sh 
mysql: [Warning] Using a password on the command line interface can be insecure.
執行時間為:194ms

執行時間為194ms=0.194sec

與優化前相比:

file

可以看出多次commit操作對性能的影響還是很大的。

方案三:使用特殊SQL語法,將多個update語句合並

合並多條update語句
在這里我們引入一種並不常用的MySQL語法:

1)優化前:

update多行執行語句類似“update xxx; update xxx;update xxx;... ...”

2)優化后:

改成先把要更新的語句拼成一個視圖(結果集表),然后用結果集表和源表進行關聯更新。這種更新方式有個隱式限制“按主鍵或唯一索引關聯更新”。

UPDATE t1 m, (
    SELECT 1 AS c1, 2 AS c2
    UNION ALL
    SELECT 2, 2
    UNION ALL
    SELECT 3, 3
    ... ...
    UNION ALL
    SELECT n, 2
  ) r
SET m.c1 = r.c1, m.c2 = r.c2
WHERE m.c1 = r.c1;

3)具體的例子:

###建表
create table t1(c1 int primary key,c2 int);
###插入5行數據
insert into t1 values(1,1),(2,1),(3,1),(4,1),(5,1);
select  * from t1;
###更新c2為c1+1
UPDATE t1 m, (
  SELECT 1 AS c1, 2 AS c2
  UNION ALL
  SELECT 2, 3
  UNION ALL
  SELECT 3, 4
  UNION ALL
  SELECT 4, 5
  UNION ALL
  SELECT 5, 6
 ) r
SET m.c1 = r.c1, m.c2 = r.c2
WHERE m.c1 = r.c1;
###查詢結果
select * from t1;

執行結果:

  mysql> create table t1(c1 int primary key,c2 int);
  Query OK, 0 rows affected (0.03 sec)

  mysql> insert into t1 values(1,1),(2,1),(3,1),(4,1),(5,1);
  Query OK, 5 rows affected (0.00 sec)
  Records: 5  Duplicates: 0  Warnings: 0

  mysql> select * from t1;
  +----+------+
  | c1 | c2   |
  +----+------+
  |  1 |    1 |
  |  2 |    1 |
  |  3 |    1 |
  |  4 |    1 |
  |  5 |    1 |
  +----+------+
  5 rows in set (0.00 sec)

  mysql> update  t1 m,(select 1 as c1,2 as c2 union all select 2,3 union all select 3,4 union all select 4,5 union all select 5,6 ) r set m.c1=r.c1,m.c2=r.c2  where m.c1=r.c1;
Query OK, 5 rows affected (0.01 sec)
  Rows matched: 5  Changed: 5  Warnings: 0

  mysql> select * from t1;
  +----+------+
  | c1 | c2   |
+----+------+
  |  1 |    2 |
  |  2 |    3 |
  |  3 |    4 |
  |  4 |    5 |
  |  5 |    6 |
  +----+------+
  5 rows in set (0.00 sec)

4)更進一步的證明

在這里筆者選擇通過觀察語句執行生成的binlog,來證明優化方式的正確性。

首先是未經優化的語句:

begin;
update t1 set c2=2 where c1=1;
update t1 set c2=3 where c1=2;
update t1 set c2=4 where c1=3;
update t1 set c2=5 where c1=4;
update t1 set c2=6 where c1=5;
commit;
......
### UPDATE `test`.`t1`
### WHERE
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=2 /* INT meta=0 nullable=1 is_null=0 */
......
### UPDATE `test`.`t1`
### WHERE
###   @1=2 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=2 /* INT meta=0 nullable=0 is_null=0 */
###   @2=3 /* INT meta=0 nullable=1 is_null=0 */
......
### UPDATE `test`.`t1`
### WHERE
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=4 /* INT meta=0 nullable=1 is_null=0 */
......
### UPDATE `test`.`t1`
### WHERE
###   @1=4 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=4 /* INT meta=0 nullable=0 is_null=0 */
###   @2=5 /* INT meta=0 nullable=1 is_null=0 */
......
### UPDATE `test`.`t1`
### WHERE
###   @1=5 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=5 /* INT meta=0 nullable=0 is_null=0 */
###   @2=6 /* INT meta=0 nullable=1 is_null=0 */
......

然后是優化后的語句:

UPDATE t1 m, (
  SELECT 1 AS c1, 2 AS c2
  UNION ALL
  SELECT 2, 3
  UNION ALL
  SELECT 3, 4
  UNION ALL
  SELECT 4, 5
  UNION ALL
  SELECT 5, 6
 ) r
SET m.c1 = r.c1, m.c2 = r.c2
WHERE m.c1 = r.c1;
### UPDATE `test`.`t1`
### WHERE
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=1 /* INT meta=0 nullable=0 is_null=0 */
###   @2=2 /* INT meta=0 nullable=1 is_null=0 */
### UPDATE `test`.`t1`
### WHERE
###   @1=2 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=2 /* INT meta=0 nullable=0 is_null=0 */
###   @2=3 /* INT meta=0 nullable=1 is_null=0 */
### UPDATE `test`.`t1`
### WHERE
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=3 /* INT meta=0 nullable=0 is_null=0 */
###   @2=4 /* INT meta=0 nullable=1 is_null=0 */
### UPDATE `test`.`t1`
### WHERE
###   @1=4 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=4 /* INT meta=0 nullable=0 is_null=0 */
###   @2=5 /* INT meta=0 nullable=1 is_null=0 */
### UPDATE `test`.`t1`
### WHERE
###   @1=5 /* INT meta=0 nullable=0 is_null=0 */
###   @2=1 /* INT meta=0 nullable=1 is_null=0 */
### SET
###   @1=5 /* INT meta=0 nullable=0 is_null=0 */
###   @2=6 /* INT meta=0 nullable=1 is_null=0 */

可以看到,優化前后binlog中記錄的SQL語句是一致的。這也說明了我們優化后語句與原執行語句是等效的。

5)從語法角度的分析

UPDATE t1 m, --被更新的t1表設置別名為m
(
  SELECT 1 AS c1, 2 AS c2
  UNION ALL
  SELECT 2, 3
  UNION ALL
  SELECT 3, 4
  UNION ALL
  SELECT 4, 5
  UNION ALL
  SELECT 5, 6
) r --通過子查詢構建的臨時表r
SET m.c1 = r.c1, m.c2 = r.c2
WHERE m.c1 = r.c1

將子查詢臨時表r單獨拿出來,我們看一下執行結果:

mysql> select 1 as c1,2 as c2 union all select 2,3 union all select 3,4 union all select 4,5 union all select 5,6;
+----+----+
| c1 | c2 |
+----+----+
|  1 |  2 |
|  2 |  3 |
|  3 |  4 |
|  4 |  5 |
|  5 |  6 |
+----+----+
5 rows in set (0.00 sec)

可以看到,這就是我們想要更新的那部分數據,在更新之后的樣子。通過t1表與r表進行join update,就可以將t1表中相應的那部分數據,更新成我們想要的樣子,完成了使用一條語句完成多行更新的操作。

6)看一下執行計划

以下為explain執行計划,使用了嵌套循環連接,外循環表t1 as m根據條件m.c1=r.c1過濾出5條數據,每更新一行數據需要掃描一次內循環表r,共循環5次:

file

如果光看執行計划,似乎這條語句的執行效率不是很高,所以我們接下來真正執行一下。

7)實踐檢驗

以下腳本用於生成優化后update語句,更新c2的值等於1000以內的隨機數

#!/bin/bash

echo "update t1 as m,(select 1 as c1,2 as c2 " >> update-union-all.sql

for j in {2..1000}
do
        echo "union all select $j,$((RANDOM%1000+1))" >> update-union-all.sql

done

echo ") as r set m.c2=r.c2 where m.c1=r.c1" >> update-union-all.sql

生成SQL語句如下

update t1 as m,(select 1 as c1,2 as c2
union all select 2,644
union all select 3,322
union all select 4,660
union all select 5,857
union all select 6,752
... ...
union all select 999,225
union all select 1000,77
) as r set m.c2=r.c2 where m.c1=r.c1

執行語句

#!/bin/bash

start_time=`date +%s%3N`
/ssd/tmp/mysql/bin/mysql -h127.0.0.1 -uroot -P3316 -pabc123 -e \
"use test;source /ssd/tmp/tmp/1000/update-union-all.sql;"
end_time=`date +%s%3N`
echo "執行時間為:"$(($end_time-$start_time))"ms"

執行結果:

[root@computer-42 test]# bash update-union-all.sh
mysql: [Warning] Using a password on the command line interface can be insecure.
執行時間為:58ms

與優化前相比:

file

多次測試對比結果如下:

file

總結

根據以上理論分析與實際驗證,我們找到了一種對批量更新場景的優化方式。

Enjoy GreatSQL 😃

文章推薦:

技術分享 | MGR最佳實踐(MGR Best Practice)
https://mp.weixin.qq.com/s/66u5K7a9u8GcE2KPn4kCaA

技術分享 | 萬里數據庫MGR Bug修復之路
https://mp.weixin.qq.com/s/IavpeP93haOKVBt7eO8luQ

Macos系統編譯percona及部分函數在Macos系統上運算差異
https://mp.weixin.qq.com/s/jAbwicbRc1nQ0f2cIa_2nQ

技術分享 | 利用systemd管理MySQL單機多實例
https://mp.weixin.qq.com/s/iJjXwd0z1a6isUJtuAAHtQ

產品 | GreatSQL,打造更好的MGR生態
https://mp.weixin.qq.com/s/ByAjPOwHIwEPFtwC5jA28Q

產品 | GreatSQL MGR優化參考
https://mp.weixin.qq.com/s/5mL_ERRIjpdOuONian8_Ow

關於 GreatSQL

GreatSQL是由萬里數據庫維護的MySQL分支,專注於提升MGR可靠性及性能,支持InnoDB並行查詢特性,是適用於金融級應用的MySQL分支版本。

Gitee:
https://gitee.com/GreatSQL/GreatSQL

GitHub:
https://github.com/GreatSQL/GreatSQL

微信&QQ群:

可搜索添加GreatSQL社區助手微信好友,發送驗證信息“加群”加入GreatSQL/MGR交流微信群

QQ群:533341697
微信小助手:wanlidbc

本文由博客一文多發平台 OpenWrite 發布!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM