工作中,需要把Oracle数据库改造成MySQL数据库,系统中的报表SQL语句,既要支持Oracle,又要支持MySQL,且改造成MySQL后要保证其执行效率。MySQL使用8.0+版本,支持Oracle中的窗口函数、WITH AS等用法。
由于系统业务和表结构比较复杂,也没有怎么做数据落地,导致查询的表数据量大且做了大量的关联查询。
以其中一张需要改造的报表为例,Java+Oracle的实现方式,没有用到第三方报表工具,该报表SQL的XML文件大致有以下特点:
现需要改造此报表SQL的XML文件,让其在Oracle和MySQL8.0.25版本中兼容,要求在MySQL环境下,查询一个月的时间不能超过30s。
其他部门已提前完成的内容:
MySQL数据库中已导入了Oracle中的表结构和数据、视图、函数,且视图、函数已修改成MySQL支持的写法,但没有做优化,只能保证执行不报语法错误,有一些视图因数据量大或关联没用到索引,甚至执行不出结果。
将在Oracle环境中执行出来的几段SQL语句抓取出来,修改成MySQL支持的写法,保证在MySQL上执行不报语法错误。
1、SQL中使用到的那个视图,SELECT不出结果,LIMIT 1都执行不出。
2、Oracle的临时表和MySQL的有所不同:Oracle的临时表,可以提前创建,结束事务或会话时会自动清空数据;MySQL的临时表为会话级别,只能在当前会话中创建使用,结束会话时会自动DROP临时表。且MySQL一句SQL中不能多次使用同一张临时表,以下两种情况都会报错:
情况一:
SELECT * FROM 临时表1 A, 临时表1 B ;
情况二:
WITH A AS
(SELECT * FROM 临时表1),
B AS
(SELECT * FROM 临时表1)
SELECT * FROM A,B ;
针对上述第一个的问题,视图执行不出结果。主要原因是在MySQL中,视图里使用了UNION ALL,会导致使用此视图时外部过滤条件注不进去,视图内部无法先过滤,且用到了几张几百万行的大表,所以执行不出来。
例子:
实体表1:100W行数据,DATE_列上加了索引
视图1:
SELECT * FROM 实体表1 ;
视图2:
SELECT * FROM 实体表1
UNION ALL
SELECT * FROM 实体表1 ;
情况1:执行很快,可以用到索引
SELECT * FROM 视图1 WHERE DATE_=‘2000-01-01’;
情况2:执行很慢,无法用到索引
SELECT * FROM 视图2 WHERE DATE_=‘2000-01-01’;
情况3:执行很快
SELECT * FROM 视图1 LIMIT 1;
情况4:执行很慢
SELECT * FROM 视图2 LIMIT 1;
情况5:执行很慢
WITH A AS
(
SELECT * FROM 实体表1
UNION ALL
SELECT * FROM 实体表1
)
SELECT * FROM A LIMIT 1 ;
结论:
(1)、MySQL中,视图中用到UNION ALL,则使用此视图时外部过滤条件注不进去,得先执行视图内部的SQL,对其结果再做外部过滤。
(2)、MySQL中,视图中用到UNION ALL,对于语句”SELECT * FROM 视图 LIMIT 1;”并不是视图内部查询出一条结果立马返回,应该是查询出所有的结果以后再返回一条。
(3)、MySQL中,WITH AS 后的虚拟视图,执行计划应该跟视图类似。
将Oracle中对应的临时表和视图,均创建成临时表到MySQL存储过程中,视图部分的SQL先根据查询区间过滤,再插入临时表。
针对MySQL一句SQL中不能多次使用同一张临时表的问题,可以复制多张临时表,当成不同的临时表来实现。
一些客户不允许使用存储过程,那么把存储过程的SQL代码放到单独的XML文件中,程序直接传入参数调用执行。
复制多张临时表来使用,会有以下问题:
1、 增加不必要的执行时间
2、公共视图,其他地方也会使用到。如果将视图改成临时表,重复用几次插入几次,那么此SQL中重复使用了3次,其他SQL中重复使用了5次,就不现实了。
视图改造成临时表的方式行不通,更复杂且维护成本高,考虑是否可以用实体表来实现。
将Oracle中对应的临时表和视图,在MySQL中均创建成实体表,插入后使用,有以下问题:
1、同一张表不停地DELETE/INSERT,会产生很多碎片,索引失效;而且需要一个字段来记录每次查询的唯一标识,程序获取唯一值的问题也会搞得更复杂。
2、如果设置成手动提交,报表SQL在一个事务里,DELETE-INSERT-SELECT-DELETE,那么就会导致该报表的其他查询界面长时间的锁等待。
综合考虑以上方案,暂定方案4:视图和临时表全部加在WITH AS后面来实现。
确定使用方案4来改造,原Oracle环境下用到的视图和临时表,在MySQL中均改成WITH AS 后的虚拟视图。视图部分的改造,写到单独的XML文件,加入区间的过滤条件,方便其他地方统一调用拼接。
(先不考虑性能问题,只让SQL语句在MySQL中执行出相同的结果,改造时尽量用两种数据库均支持的写法)
1、连Oracle环境,抓取报表执行的SQL语句
2、确认SQL语句中用到的视图和函数,在MySQL中已建立,且执行不报错、执行结果相同
3、SQL语句改造,让其在MySQL中执行出相同的结果,以下是修改频率较高的几个地方:
3.1、NVL --> COALESCE
3.2、DECODE --> CASE WHEN
3.3、TO_CHAR、TO_DATE --> MySQL中比较时一般会自动转换类型
3.4、TRUNC --> TRUNCATE
3.5、ZH_CONCAT --> GROUP_CONCAT
3.6、LISTAGG --> GROUP_CONCAT
3.7、ROWNUM --> ROW_NUMBER() OVER() AS ROWNUM
注意几点:
(1)、尽量改成MySQL和Oracle通用的写法,比如Oracle中的NVL改成COALESCE(MySQL、Oracle均支持),不要改成IFNULL(Oracle不支持)。
(2)、‘||’是Oracle中的拼接函数,可以拼接多个参数。另一个拼接函数‘CONCAT’在MySQL中可以拼接多个参数,而Oracle中只能拼接两个。可以修改MySQL的配置文件,sql-mode中加一个‘PIPES_AS_CONCAT’,如此‘||’在MySQL中也表拼接,‘||’就不用再做转换。
(3)、Oracle中空值’’和NULL不做区分,都是NULL的意思。而MySQL中有区分,且MySQL拼接函数中,有一个参数为NULL则整体为NULL,Oracle中则不是,需要特别注意,根据实际情况修改,保证执行结果一致。
(4)、在与日期、数值、字符串字段进行比较时,MySQL一般会自动转换成相同类型进行比较,Oracle中会加一个类型转换函数,特别是日期函数的使用需要特别留意。但MySQL中不是所有的情况都让它自动转换类型,为了使用到索引有些地方也需要强制类型转换。
(5)、其他
MySQL中如果视图里有大表的UNION ALL,则外部过滤条件无法注入,会先整表查询出视图,对其结果再做过滤,效率极低。所以改造成WITH AS后,先根据查询的日期区间做过滤,大大减少了虚拟视图执行出来的数据量。也可按实际情况将UNION ALL拆分,特别是视图里子查询中的UNION ALL,可以拆分成单独的虚拟视图后再做UNION ALL。
一步步执行WITH AS 后的虚拟视图,记录好每个虚拟视图的执行时间、执行出来的数据量,以及每次修改后的整体执行时间,逐步优化。
优化时发现加了一个索引,虚拟视图的执行效率大大提高,但是整体的SQL语句执行效率反而下降,是因为增加了索引从而改变了整体的执行计划,这种情况就需要根据修改记录倒退回去,看看问题出在哪个修改上,所以备份好修改记录特别重要。
大致的优化方向:
1、MySQL的性能主要靠索引,所以索引都用起来,一些大表看看能否用覆盖索引,减少回表的时间。
2、能用内置函数实现就不要用自定义函数,MySQL的自定义函数,数据量一多,效率降得特别明显。
3、做关联时,如果实在无法用到索引,那么看看能否把关联的数据量减少,比如我遇到一个左连接,把右表金额为0的过滤掉以后再做关联,效果相同(因为只要金额字段),具体情况具体分析。
4、将最终的SELECT输出语句使用到的虚拟视图,提前插入到实体表,建好索引,用实体表来关联执行一下,比较累计执行时间的差异。如果差很多,再对比执行计划进行分析。优化时遇到过一个问题,WITH AS 后有一个虚拟视图A,单独执行时间是2s,数据量2000多行,最终的SELECT输出语句,将此虚拟视图A做了左连接的右表,最终的执行计划,是将虚拟视图A中的语句拆出来跟左边的一张大表做了关联,而不是先将虚拟视图A执行出来,对其2000多行的结果集做关联,导致整体的执行时间多了20多秒。最终在虚拟视图A中UNION ALL 了空行,改变了整体的执行计划,从而提升了性能。所以视图里的UNION ALL不全是累赘,主要看执行计划怎么走。
以上优化思路,不考虑数据落地,也不改变取数逻辑。
在同一个数据库中,原始SQL和改造后的SQL,均查询一个月。对比执行出来的总行数、数值字段的合计值,再找几行数据看看每个字段是否一致。
1、MySQL中改造优化好的SQL,在Oracle中执行(一些函数用法等需要改回Oracle版本),对比一下,“原视图+INSERT临时表”、“全部改成WITH AS”,这两种写法哪个执行效率更高,再决定Oracle用哪种方式。
2、改造该报表SQL的XML文件,Oracle和MySQL写法有差异的地方,根据连接的数据库类型判断区分。 WITH AS后面的公共虚拟视图,单独放在一个XML文件里,传入日期区间,方便一起调用拼接用。
3、分别测试在Oracle和MySQL下起起来的程序,报表查询是否正常。