【ORACLE】关于pljson_util_pkg.sql_to_json转换大量数据的效率优化方案
之前我在这篇文章中说过,pljson库支持将任意的查询sql转换成jsonlist的长字符串,比如
我们先创建一个这样的函数方便使用
create FUNCTION sql_to_json_clob(i_sql VARCHAR2) RETURN CLOB IS
l_clob CLOB;
BEGIN
l_clob := empty_clob();
dbms_lob.createtemporary(l_clob, TRUE);
pljson_list.to_clob(SELF => pljson_util_pkg.sql_to_json(i_sql,
10000,
0),
buf => l_clob,
erase_clob => TRUE);
RETURN l_clob;
END;
然后在plsql中通过以下方式调用
l_clob:=sql_to_json_clob('select 1 col1,2 col2 from dual
union all
select 3,4 from dual');
dbms_output.put_line(l_clob);
可以得到l_clob这个变量的值为
[{"COL1":1,"COL2":2},{"COL1":3,"COL2":4}]
而且如果json的key要做驼峰命名规则,也可以直接在写sql的时候用带双引号的别名确定好,比如
select 1 "appId",2 "phoneNum" from dual
union all
select 3,4 from dual
[{"appId":1,"phoneNum":2},{"appId":3,"phoneNum":4}]
这样我们就不需要在编写程序的时候声明一大堆变量了。
实际项目使用中,可以发现,在数据量比较少的时候,这个转换效率很快,但是当数据量达到某个临界点,这个效率会变得奇慢,多一行数据,耗时长度可增加几千甚至上万倍。而且有时候执行过程中无法终止,kill session也只是把状态变成killed,实际还在不断的消耗内存资源,只能在操作系统上通过orakill来终止。
跟踪这个plsjon的升级,这段代码也一直没有过效率方面的优化,所以我只能自己动手了。
这个程序逻辑大致如下
1.sql_to_json 将sql转换成 sys_refcursor类型
2.然后将这个sys_refcursor传入ref_cursor_to_json
3.通过oracle自带的xml包dbms_xmlgen,将这个sys_refcursor转换成xml格式的数据
4.再通过get_xml_to_json_stylesheet,把xml转换成jsonlist
5.返回jsonlist,完成了
这段代码中没有任何一处有loop,就算有,也只可能在ORACLE的自带的dbms_xmlgen包里,除开ORACLE自带的这个包内部的机制不清楚外,这个段代码的逻辑真是太简单了,几乎无从下手。
回到问题本身,只要传入的SQL的数据量大了,执行就会变慢,其中第一步和第二步,速度是不受多少影响的,那么我们可以尝试,把这个sys_refcursor进行拆分,分多次传入ref_cursor_to_json,最后再把生成的jsonlist进行合并。
所以要对sys_refcursor这种类型进行研究,可惜的是,我翻了很久的资料,都没有找到一个可以拆分sys_refcursor这种类型的案例。我也尝试自己写了很多种拆的方式,都报错。我也不可能把这个游标遍历一次再来拆分。这种游标类型很奇特,它不需要提前创建一个type对象即可直接使用,也无需声明它有哪些字段,具有通用性,要支持任意动态sql的传入,就只能使用这个。
而且之前我另一个项目,也遇到了sys_refcursor这种类型的问题,一直也没想到方案。
但这个问题很紧急,影响到了生产环境的正常业务操作,所以我只能另辟蹊径。
既然不能在游标上下手,那么我直接在sql上下手算了。
我把传入的sql,按特定的结果行数作为条件,直接拆分成多个sql,分多次传入,最后再合并,而且这个行数可以作为输入参数,由使用者自行调试至最高效率。
一个固定的sql容易拆,但是一个不确定的sql就不容易拆了,会遇到几个问题:
1.sql的查询结果是不带行号的,所以要自己动态拼一个rownum字段上去
2.ORACLE的rownum排序,基于某些特性,就算是短时间内查两次同样的sql,也可能会出现两次rownum的排序结果不一样,这个会导致我们生成的json数据重复或者缺失
3.所以我们只能用row_number() over(order by )指定字段强制排序,不论查多少次,数据结果都不变,但是我们传入的是动态sql,每次传入的字段都不一样
4.所以只能把所有字段都作为排序字段,直接字符串拼接所有字段
FUNCTION sql_to_json_clob(i_sql VARCHAR2,
i_batch_rownum NUMBER DEFAULT 100) RETURN CLOB IS
-- Author : DarkAthena
-- Created : 2019-04-25 11:10:41
-- Purpose : sql转json速度优化版
l_clob CLOB; --最终结果
l_sql2 VARCHAR2(30000); --排序SQL
l_col_name_str VARCHAR2(10000); --字段名串
l_sql3 VARCHAR2(30000); --分页SQL
l_row_count NUMBER; --已经处理的行数
l_sum_count NUMBER; --总行数
l_clob_tmp CLOB; --缓存
l_curid INTEGER;
l_cnt NUMBER;
l_desctab dbms_sql.desc_tab;
BEGIN
--统计行数
EXECUTE IMMEDIATE 'select count(1) from (' || i_sql || ')'
INTO l_sum_count;
--如果没数据,返回空LIST
IF l_sum_count = 0
THEN
RETURN '[]';
END IF;
--开始取字段名称
l_curid := dbms_sql.open_cursor();
dbms_sql.parse(l_curid, i_sql, dbms_sql.native);
dbms_sql.describe_columns(l_curid, l_cnt, l_desctab);
FOR i IN 1 .. l_desctab.count LOOP
l_col_name_str := l_col_name_str || CASE
WHEN l_col_name_str IS NULL THEN
NULL
ELSE
','
END || '"' || l_desctab(i).col_name || '"';
END LOOP;
--取字段名称结束
--组装排序SQL
l_sql2 := 'select t.*,row_number() over(order by ' || l_col_name_str ||
') rn from (' || i_sql || ') t order by ' ||
to_char(l_desctab.count + 1);
--关闭游标
dbms_sql.close_cursor(l_curid);
-- dbms_output.put_line(l_sql2);
--初始化处理记录数
l_row_count := 0;
--初始化CLOB对象
l_clob := empty_clob();
dbms_lob.createtemporary(l_clob, TRUE);
--开始进行分页处理
LOOP
--组装分页SQL
l_sql3 := 'select ' || l_col_name_str || ' from (' || l_sql2 ||
') where rn > ' || to_char(l_row_count) || ' and rn<=' ||
to_char(l_row_count + i_batch_rownum);
-- dbms_output.put_line(l_sql3);
--初始化缓存对象
l_clob_tmp := empty_clob();
dbms_lob.createtemporary(l_clob_tmp, TRUE);
--SQL转换成JSONLIST再转换成CLOB,存入缓存
pljson_list.to_clob(SELF => pljson_util_pkg.sql_to_json(l_sql3,
i_batch_rownum,
0),
buf => l_clob_tmp,
erase_clob => TRUE);
--将缓存复制到CLOB对象
dbms_lob.copy(dest_lob => l_clob,
src_lob => l_clob_tmp,
amount => dbms_lob.getlength(l_clob_tmp),
dest_offset => dbms_lob.getlength(l_clob) + 1,
src_offset => CASE
WHEN dbms_lob.getlength(l_clob) = 0 THEN
1
ELSE
2
END);
--已处理行数变大
l_row_count := l_row_count + i_batch_rownum;
--如果已处理行数小于总行数,把CLOB内的最后一个字符,由"]"变成"," ,否则退出循环
IF l_row_count < l_sum_count
THEN
dbms_lob.write(lob_loc => l_clob,
amount => 1,
offset => dbms_lob.getlength(l_clob),
buffer => ',');
ELSE
EXIT;
END IF;
END LOOP;
--返回最终结果
RETURN l_clob;
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20001, SQLERRM);
END;
由于我没有找到两个jsonlist直接合并的方法,所以暂时先用字符串拼接的方式进行合并了,之后想到更好的方法后会在我的github上更新最新的代码
posted on 2021-10-03 20:44 DarkAthena 阅读(85) 评论(0) 编辑 收藏 举报