Oracle SQL

系统介绍

系统架构见下图:
2021-05-15-16-44-19-426510.png
application1和application2是一个分布式系统中的2个应用,application1连接的数据库是database1,application2连接的数据库是database2,application2生产的数据要给application1做跑批使用。
application1要获取database2的数据,并不是通过接口来获取的,而是直连database2来获取,因此application1也具有database2库的读权限。
database2中有1张表table_b,里面保存的数据是application1跑批需要的数据。application1查找到table_b的数据后,先保存到database1的数据库表table_a中,等跑批时取出来用。
table_a和table_b的表结构如下:
2021-05-15-16-44-20-424587.png
2个表的主键都是字段a,application1查询出table_b的数据后,会根据主键a来判断这条数据是否存在,如果数据存在,就更新,否则,就插入。
application1使用的orm框架是Mybatis,为了减少应用和数据库的交互,使用了oracle的merge语句。 :::tips 注意:Mybatis相关的文件有5个:TableAMapper.javaTableBMapper.javaTableAMapper.xmlTableBMapper.xmlTableAEntity.java ::: 熟悉Mybatis的同学应该都知道,前两个java类是sql操作接口类,第3、4两个文件是存放sql的xml文件,跟前两个文件对应,最后一个java文件是do类。

事故现场

TableBMapper中有一个方法selectForPage,用来按页查询table_b中数据,每页1万条数据,之后把这个list结果merge到table_a,看一下代码:

  1. //从table_b按每页1万条来查询数据
  2. List<TableAEntity> list = tableBMapper.selectForPage(startPage, 10000);
  3. //把查到的数据一次性merge到table_a中
  4. tableAMapper.mergeFromTableB(list);

再看一下TableAMapper.xml中的mergeFromTableB方法,代码如下:

  1. <update id="mergeFromTableB" parameterType="list">
  2. <foreach collection="list" item="item" index="index" separator=";" close=";end;" open="begin">
  3. MERGE INTO table_a ta USING(select #{item.a} as a,#{item.b} as b,#{item.c} as c, #{item.d} as d from dual) tb
  4. on (ta.a = tb.a)
  5. WHEN MATCHED THEN UPDATE set
  6. ta.b=tb.b,
  7. ta.c=tb.c,
  8. ta.d=tb.d
  9. WHEN NOT MATCHED THEN insert(
  10. a,
  11. b,
  12. c,
  13. d
  14. )
  15. values (
  16. tb.a,
  17. tb.b,
  18. tb.c,
  19. tb.d
  20. )
  21. </foreach>
  22. </update>

这条sql执行后,截取部分oracle的日志,如下:
2021-05-15-16-44-20-541819.png
图中可以看到oracle报了ORA-07445错误。
分析日志后发现,sql绑定变量达到了了79010个,而oracle是不允许超过65535个的。

解决方案

前面的分析确定了导致oracle挂掉的原因是绑定变量超过了65535个,那对症下药,解决的方案有3个:

业务系统方案

1.循环单条执行merge语句,优点是修改简单,缺点是业务系统跟数据库交互太多,会影响跑批任务执行效率。2.对mergeFromTableB进行分批调用,比如每1000条调用一次merge方法,改造稍微多一点,但是交互会少很多。

DBA方案

给oracle打一个补丁,这个方案需要停服务。
业务方案2明细有优势,用这个方案进行了改造,每次1000条,批量merge,代码如下:

  1. for (int i = 0; i < list.size(); i += 1000) {
  2. if (i + 1000 < list.size()) {
  3. tableAMapper.mergeFromTableB(list.subList(i, i + 1000));
  4. } else {
  5. tableAMapper.mergeFromTableB(list.subList(i, list.size()));
  6. }
  7. }

新的问题

按照上面的方案改造完成后,数据库不会奔溃了,但是新的问题出现了。测试的同学发现,每次处理超过1000条数据,非常耗时,有时竟然达到了4分钟,惊呆。
看打印的批量sql,类似于下面的语句:

  1. begin
  2. merge into table_a ta USING(...;
  3. merge into table_a ta USING(...;
  4. end;

分析了一下,虽然放在了一个SQL块中,但还是单条执行,最后一起提交。
再做一次优化,把上面多条merge语句合成1条。
优化思路是创建一张临时表,先把list中的数据插入到临时表中,然后用一次merge把临时表的数据merge进table_a这张表。
oracle的临时表有2种,一种是会话级别,一种是事务级别:

  1. 会话级别的临时表,数据会在整个会话的生命周期中,会话结束,临时表数据清空;
  2. 事务级别的临时表,数据会在整个事务执行过程中,事务结束,临时表数据清空。

下面看具体实施过程。

  1. 创建一张会话临时表,SQL如下:

    1. create global temporary table_a_temp on commit delete rows as select * from table_a;
    2. comment on table_a_temp is 'table_a表临时表';
  2. 把table_b查询到的数据list插入临时表,需要在 TableAMapper.xml 增加一个方法:

    1. <insert id="batchInsertTemp" parameterType="list">
    2. insert all
    3. <foreach collection="list" index="index" item="item">
    4. into table_a_temp
    5. <trim prefix="(" suffix=")" suffixOverrides="," >
    6. a,
    7. <if test="item.b != null" >
    8. b,
    9. </if>
    10. <if test="item.c != null" >
    11. c,
    12. </if>
    13. <if test="item.d != null" >
    14. d,
    15. </if>
    16. </trim>
    17. <trim prefix="values (" suffix=")" suffixOverrides="," >
    18. #{item.a},
    19. <if test="item.b != null" >
    20. #{item.b,jdbcType=VARCHAR},
    21. </if>
    22. <if test="item.c != null" >
    23. #{item.c,jdbcType=VARCHAR},
    24. </if>
    25. <if test="item.d != null" >
    26. #{item.d,jdbcType=VARCHAR},
    27. </if>
    28. </trim>
    29. </foreach>
    30. select 1 from dual
    31. </insert>

    :::danger 注意:oracle的insert all语句单次插入不能超过1000条。 :::

  3. 把临时表的数据merge到table_a中,需要在 TableAMapper.xml 增加一个方法:

    1. <update id="mergeFromTempData">
    2. MERGE INTO table_a ta
    3. USING (select * from table_a_temp) tb
    4. on (ta.a = tb.a)
    5. WHEN MATCHED THEN UPDATE set
    6. ta.b = tb.b,
    7. ta.c = tb.c,
    8. ta.d = tb.d
    9. WHEN NOT MATCHED THEN
    10. insert
    11. (a, b, c, d)
    12. values
    13. (tb.a, tb.b, tb.c, tb.d)
    14. </update>

    4.最终业务代码修改如下:

    1. //从table_b查询
    2. List<TableAEntity> list = tableBMapper.selectForPage(startPage, 10000);
    3. //批量插入table_a_temp临时表
    4. for (int i = 0; i < list.size(); i += 1000) {
    5. if (i + 1000 < list.size()) {
    6. tableAMapper.batchInsertTemp(list.subList(i, i + 1000));
    7. } else {
    8. tableAMapper.batchInsertTemp(list.subList(i, list.size()));
    9. }
    10. }
    11. //从table_a_temp把数据merge到table_a
    12. tableAMapper.mergeFromTempData();

    总结

    在oracle上执行SQL时,如果绑定变量的数量超过了65535,会引发ORA-07445。当然,引发ORA-07445的原因还有其他。解决这个问题最好的方式是从业务代码层面进行修改。也可以让DBA可以给oracle打一个补丁,但是oracle必须要停服务。