
技术分享 | 在MySQL对于批量更新操作的一种优化方式
引言
批量更新数据,不同于这种 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行
1.单语句批量更新
更新语句
执行结果
2.多语句批量更新
以下脚本用于生成1000行update语句,更新c2的值等于1000以内的随机数
生成sql语句如下
因为source /ssd/tmp/tmp/1000/update.sql;执行结果如下,执行时间不易统计:
所以利用Linux时间戳进行统计:
执行结果:
执行所用时间为:4246ms=4.246 sec
结果比较
总结
由上述例子我们可以看到,同样是更新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执行语句
生成语句如下
执行语句
执行结果:
与优化前相比
很遗憾,执行总耗时反而增加了。
这里笔者有一点推测是由于原本一条update语句,被拆分成了两条语句:
这样在MySQL客户端和MySQL进程之间的通讯次数增加了,所以增加了总耗时。
因为prepare预处理语句执行时只能使用用户变量传递,以下执行语句会报错
所以无法在语法方面将两条语句重新合并,笔者便使用了以下另外一种执行方式
执行语句
执行结果:
与优化前相比:
这样的优化幅度符合prepare语句的理论预期,但仍旧不够理想。
方案二:多个update语句放到一个事务内执行,最终commit一次
以下脚本用于生成1000行update语句在一个事务内,更新c2的值等于1000以内的随机数
生成sql语句如下
执行时间统计的方法,同上
执行时间为194ms=0.194sec
与优化前相比:
可以看出多次commit操作对性能的影响还是很大的。
方案三:使用特殊SQL语法,将多个update语句合并
合并多条update语句
在这里我们引入一种并不常用的MySQL语法:
1)优化前:
update多行执行语句类似“update xxx; update xxx;update xxx;... ...”
2)优化后:
改成先把要更新的语句拼成一个视图(结果集表),然后用结果集表和源表进行关联更新。这种更新方式有个隐式限制“按主键或唯一索引关联更新”。
3)具体的例子:
执行结果:
4)更进一步的证明
在这里笔者选择通过观察语句执行生成的binlog,来证明优化方式的正确性。
首先是未经优化的语句:
然后是优化后的语句:
可以看到,优化前后binlog中记录的SQL语句是一致的。这也说明了我们优化后语句与原执行语句是等效的。
5)从语法角度的分析
将子查询临时表r单独拿出来,我们看一下执行结果:
可以看到,这就是我们想要更新的那部分数据,在更新之后的样子。通过t1表与r表进行join update,就可以将t1表中相应的那部分数据,更新成我们想要的样子,完成了使用一条语句完成多行更新的操作。
6)看一下执行计划
以下为explain执行计划,使用了嵌套循环连接,外循环表t1 as m根据条件m.c1=r.c1过滤出5条数据,每更新一行数据需要扫描一次内循环表r,共循环5次:
如果光看执行计划,似乎这条语句的执行效率不是很高,所以我们接下来真正执行一下。
7)实践检验
以下脚本用于生成优化后update语句,更新c2的值等于1000以内的随机数
生成SQL语句如下
执行语句
执行结果:
与优化前相比:
多次测试对比结果如下:
总结
根据以上理论分析与实际验证,我们找到了一种对批量更新场景的优化方式。
