润乾报表优化与性能管理.docx
《润乾报表优化与性能管理.docx》由会员分享,可在线阅读,更多相关《润乾报表优化与性能管理.docx(23页珍藏版)》请在冰豆网上搜索。
![润乾报表优化与性能管理.docx](https://file1.bdocx.com/fileroot1/2023-1/24/b8d518e9-0daa-4973-8cd1-d68486c455ff/b8d518e9-0daa-4973-8cd1-d68486c455ff1.gif)
润乾报表优化与性能管理
润乾报表优化与性能管理
编制者:
吴国邦
第一章、报表优化
1.灵活应用多数据集
1.1.1. 网格式、分组式、交叉式尽量用单数据集
看如下的报表:
图 1.1.
这是一个比较典型的多层交叉报表,其做法有两种:
第一种:
图 1.2.
这种做法设计了五个数据集,每一层的表头都来自一个码表,交叉点的数据汇总时根据条件和表头关联
第二种:
图 1.3.
这种做法只设计了一个数据集,把码表和事实记录表叉乘起来,在报表中进行分组扩展我们试比较一下两种做法的优缺点:
第一种做法:
1.数据集不需要对多表进行叉乘,取数速度快
2.事实记录数据集可以在sql里先进行分组聚集操作,减少取过来的记录书,加快取数的速度和报表运算速度
3.表头扩展直接对码表进行select操作,运算速度快
4.交叉点汇总需要对事实记录表进行检索遍历,查找出和表头关联的记录进行汇总,交叉点的格子数远远大于表头单元格的个数,事实记录表的记录数也一般远远大于码表的记录数,每个交叉点都要对事实记录表遍历检索一次,运算速度非常慢
第二种做法:
1.数据集需要对码表和事实记录表进行叉乘,取数速度慢,但是一般情况下码表的字段数只有一两个,而且数据库有索引,记录数不是非常大的情况下慢不了很多
2.在数据集sql里就可以对表头字段先进行分组聚集,这样取过来的记录数大大减少,加快了取数速度和报表的运算速度
3.表头单元格是对事实记录表进行分组操作,然后再扩展,比第一种做法慢一些,但是由于对数据集只要进行一次的分组操作,因此慢不了太多
4.交叉点汇总不需要再对事实记录表进行检索遍历了,而是直接对当前组集里的记录进行汇总,因此速度大大提高了
总结:
第二种的做法性能优于第一种
1.1.2. 多源分片、主子报表尽量用多数据集
看如下报表:
图 1.4.
这是一个比较典型的主子报表,主表一条记录对应子表多条记录,是个典型的一对多的关系,再引伸一下,一个主表可以对应多个子表,每个子表都是多条记录。
此时做法还是存在两种:
第一种:
1.每个主表和子表分别定义一个数据集,在报表里通过条件把子表和主表关联起来
2.优点:
避免了表间叉乘,减少了数据量,加快了取数速度和数据库的运算速度。
3.原因:
对于主子报表,主表的字段数一般比较多,而子表一般有多条记录和主表的一条记录关联,同时子表间往往没有关联关系,因此如果叉乘会导致数据量大大增加,在多子表的情况下这个现象尤其明显
4.缺点:
子表取数扩展时,需要根据条件和主表记录关联,需要对子表记录进行遍历检索,但是大部分的情况下,主表只有一条记录或者只有十几条记录,因此对子表的遍历检索次数不会很多
第二种:
1.只定义一个数据集,把主表和所有子表叉乘起来
2.缺点:
由于子表之间没有关联关系,且主表的字段数比较多,叉乘往往导致记录数呈几何级数增加
3.优点:
子表取数扩展时,不需要对子表记录进行遍历扩展,运算速度快点
总结:
第一种做法的性能优于第二种
2.写sql的技巧
2.1.1. 尽量在sql里进行group
对于汇总类型的报表,往往需要进行分组聚集运算,如果在数据库中先进行一次分组聚集,能够大大减少取到报表服务器的记录数,加快取数和报表运算的速度。
看如下报表:
图 1.5.
这是一个典型的交叉分组报表,其sql有两种写法:
第一种:
select产品,客户,销量from购买记录表
第二种:
select产品,客户,sum(销量)from购买记录表groupby产品,客户
而报表的做法都一样,如下图所示:
图 1.6.
分析:
采用第一种做法,不仅仅取到报表服务器上记录数多了,取数速度慢,而且在报表端对购买记录表进行分组运算的时候速度也慢了;
采用第二种做法,数据库虽然要进行分组运算,但是数据库中有索引,运算速度快,且取到报表服务器端的记录数大大减少,取数速度大大加快,因此在报表端进行分组运算的时候只要对很少的记录数进行,报表的运算速度大大加快了
总结:
第二种做法的性能远远优于第一种
2.1.2. 尽量不用select*from
对于初学者来说,这是一个很容易犯的错误,例如报表中只需要用到三个字段,但是数据库中实际的表有十个字段,一些初学者习惯性的用select*fromtable1,这样相当于把十个字段的数据都取到报表服务器端,增加了报表服务器端的内存占用以及减慢了运算速度
正确的写法是:
selectcol1,col2,col3fromtable1,即用到哪几个字段就取哪几个,用不着的不要取
2.1.3. 尽量在sql里排序
报表中往往需要对数据进行排序,排序运算可以在数据库中进行,也可以在报表端进行,如果报表中的排序规则是确定的,那么建议排序操作选择在数据库端进行,因为数据库中有索引,且数据库是c语言开发的,数据运算速度快。
2.1.4. 尽量在sql里过滤
这个问题和2.1、2.2是类似的,报表很多时候并不需要对表中的所有记录进行操作,而是对部分满足条件的记录进行操作,因此建议过滤操作在数据库中进行,这样取到报表服务器端的记录数大大减少,既加快了取数的速度,也加快了报表的运算速度,因为报表需要处理的数据少了。
2.1.5. 大数据量可以采用存储过程
有时候,需要用于汇总统计的原始数据量非常大,如果每次生成报表都需要现算,一方面非常慢,另一方面数据库的压力会很大,此时可以采用存储过程对数据预先进行一次压缩,生成中间表,然后再基于中间表生成报表,可以大大提高运算速度并减轻数据库的压力。
3.当前行、当前组的概念
3.1.1. select函数
使用select函数时,相当于从数据集中取出一组符合条件的记录集合,在单元格中进行扩展,此时每个扩展出来的单元格都保留一个指针,指向当前记录,即当前行,因此在这些单元格的附属单元格中,应当直接用“数据集名.列名”来引用同一个数据集同一条记录的值,此时报表引擎不需要对数据集进行检索遍历了,而是直接从当前行中取值。
图示:
图 1.7.
典型的select用法:
图 1.8.
不合理的用法:
图 1.9.
3.1.2. select1()函数
select1的函数和select函数的区别在于,它每次只取出一条记录,但是当前行的概念是一样的,当它从数据集中取出一条记录时,保留了一个指针指向该记录,因此他的附属单元格里只需要利用“数据集名.列名”即可从该记录中取值,而不需要重新检索遍历数据集。
正确的select1的用法:
图 1.10.
它和select函数还有一个区别在于:
当它检索数据集时,检索到第一条满足条件的记录随即把该记录返回,而不继续往下检索;对于select来说,即使已经检索到满足条件的记录了,还会继续往下检索,直到所有记录检索完为止,因为select的任务是检索出一组记录,它还不确定后面是否还有满足条件的记录。
因此,如果你确定只要从数据集中取出一条记录,那么请一定用select1而不要用select
3.1.3. group函数
group函数是对数据集按照某个字段或者表达式进行分组,获得一组组的集合,然后从每组中取出一个指定字段或者表达式的值,放到单元格中进行扩展,扩展出来的每个单元格都保留了一个指针指向当前的组集,该组集称为当前组。
因此在附属单元格中,需要对该组集进行操作时,不需要用任何条件和主单元格关联了,如果加设了条件,反倒画蛇添足,导致报表引擎还对组集中的记录进行遍历检?
鳌?
正确的group用法:
图 1.11.
不合理的group用法:
图 1.12.
group函数的原理图示:
图 1.13.
4.写表达式的技巧
4.1. or/||操作符
使用or操作符时,尽量把值为true的可能性更大的条件表达式放在or的前面,为true可能性更小的条件表达式放在or的后面
原因:
or操作符的左右两个操作数,只要有一个为true,其结果必定为true,因此只要第一个条件表达式算出来是true,后面的条件表达式就没必要算了,这样可以加快运算速度。
其次,虽然润乾报表提供了or和||两种写法,这么做仅仅为了方便习惯写or的用户,事实上表达式中写||可以加快表达式的解析速度。
因此建议:
尽量写||,少写or
4.2. and/&&操作符
使用and操作符时,尽量把值为false的可能性更大的条件表达式放在and的前面,为false可能性更小的条件表达式放在and的后面
原因:
and操作符的左右两个操作数,只要有一个为false,其结果必定为false,因此只要第一个条件表达式算出来是false,后面的条件表达式就没必要算了,这样可以加快运算速度。
其次,虽然润乾报表提供了and和&&两种写法,这么做仅仅为了方便习惯写and的用户,事实上表达式中写&&可以加快表达式的解析速度。
因此建议:
尽量写&&,少写and
4.3. 过滤条件
润乾的内置数据集函数中,有不少函数带有过滤条件参数,例如count(),sum(),avg,max(),min()等等,很多时候可能用户需要把数据集当前记录行集全部选出,而不需要过滤,此时不少用户习惯直接把过滤条件写成true,殊不知,这样导致报表引擎运算时仍旧需要对每条记录进行判断,而如果直接省略该参数,那么引擎就会直接跳过判断,直接进行运算,速度快很多。
图 1.14.
4.4. 二分法查找函数bselect1
本文1.3.2中提到了,select1函数是从数据集中检索出满足条件的第一条记录,然后返回该条记录的选出表达式的值,而且还有当前行的概念,可以保证其附属格中以最快的速度从同一条记录中获取相应字段的值。
Select1函数的检索方法是从第一条记录往下遍历,这种检索算法在记录按照检索条件已经排好序的情况下,比二分法慢,因此润乾报表还提供了二分法检索的函数。
bselect1就是采用二分法检索的函数。
二分法检索算法介绍:
例如存在A-Z按照检索条件排好序的23个数据,二分算法首先找到最中间的那个数M,比较M和检索条件是否相等,如果相等,直接返回M,运算结束;如果不等,那么是大了还是小了,假设大了,那么指针直接指向A和M中间的那个?
鼼,再判断是否相等,如果相等,直接返回G,运算结束;如果不等,就看大了还是小了,假设G小了,那么指针直接指向G和M中间的那个数,继续进行判断,以此类推。
数据集函数:
bselect1()
函数说明:
此函数功能等同select1(),但是算法不同,采用二分法,适用于数据集记录已经按照参考字段排好序的情况,运算速度比select1()快
语法:
datasetName.bselect1(selectExp,"referExp1,referDescExp1,referValueExp1")
参数说明:
selectExp选出字段或表达式
referExp1参考字段表达式
referDescExp1参考字段表达式的数据顺序,true表示降序排列,false表示升序排列
referValueExp1参考字段的值表达式,一旦找到参考字段和该值相同的记录,即返回selectExp的值
......
参考字段及其值可以多个,如果是多个,则找到多个参考字段都和值匹配的记录才返回
rootGroupExp是否root数据集表达式
返回值:
数据类型不定,由selectExp的运算结果决定
示例:
例1:
ds1.bselect1(name,"id,false,value()")采用二分法,找到数据集ds1中id和当前格的值相等的记录,返回其name字段值
例2:
ds1.bselect1(name,"id,false,value();class,false,A1;sex,true,B1")
采用二分法,找到数据集ds1中id和当前格的值相等、class和A1相等且sex和B1相等的记录,返回其name字段值。
注意这三个条件在表达式中的顺序必需和它们在数据集中的排序先后相同,也就是说,在数据集中是先对id升序排序,再对class升序排序,最后对sex进行降序排序的。
4.5. 巧用空值判断nvl
表达式中,经常需要用到空值判断,例如在单元格的显示值属性中,判断当单元格的值为空时,显示为0,否则显示单元格的真实值,等等。
一般这种情况下,用户习惯写的表达式是:
if(value()==null,0,value())。
如果我们把value()换成更加复杂的表达式,例如if(ds1.select1(…)==null,0,ds1.select1(…)),大家可以看出,这种算法明显很慢,需要把ds1.select1(…)这样的复杂表达式运算两次,而如果采用nvl()则可以避免这个问题。
单元格函数:
nvl()
函数说明:
根据第一个表达式的值是否为空,若为空则返回指定值
语法:
nvl(valueExp1,valueExp2)
参数说明:
valueExp1需要计算的表达式,其结果不为空时返回其值
valueExp2需要计算的表达式,当valueExp1结果为空时返回此值
返回值:
valueExp1或valueExp2的结果值
示例:
例1:
nvl(A1,"")表示当A1为空时,返回空串,否则返回A1
例2:
nvl(value(),0)表示当当前格为空时返回0,否则返回当前格的值
应用举例:
图 1.15.
4.6. 数据类型的考虑
数值型的数据,根据其精度不同,可以分成好几种,例如:
整型数据有short(16位),int(32位),long(64位),BigInteger(大于64位);浮点型数据有float(32位),double(64位),BigDecimal(大于64位)。
一般来说,精度越高的数据类型,运算速度越慢,因此,如果您的数据长度没有那么长,那么建议选择精度相对比较低的数据类型,可以加快运算速度。
快逸报表提供的数值型转换函数有:
float()转换成32位的浮点数
double()转换成64位的浮点数
decimal()转换成大于64位的浮点数
integer()转换成32位的整数long()转换成64位的整数
bigint()转换成大于64位的整数
number()转换成相应的32位整数、64位整数、或者64位符点数
请用户在选择以上函数时根据数据的长度慎重选择。
5.枚举分组
5.1. enumgroup函数
请看如下报表:
图 1.16.
这是一个不规则分组的报表,将饮料和点心归入副食品组,肉/家禽和海鲜归入肉类组,日用品归入日用品组,其他所有类别归入其他组。
设计这样的一个报表有多种做法,下面我们列出比较典型的两种:
第一种做法:
图 1.17.
第二种做法:
图 1.18.
分析:
第一种做法是比较常规的做法,首先在第一列中根据分组要求,枚举出三个组,然后分别在第二列、第三列、第四列里对数据进行汇总时,根据分组条件增加过滤条件,如类别ID==1or类别ID==3等等,一般的传统报表工具都是这种做法。
这种做法的弊端在于:
引擎对每一个汇总项进行计算时,都需要对数据进行一次遍历,查找出满足条件的记录进行汇总,因此计算速度很慢。
第二种做法采用了enumgroup函数,这种做法是对按照枚举分组规则数据集先进行分组,之后在第二列、第三列、第四列里对数据进行汇总时直接从当前组里取数,不再需要遍历和过滤了。
因此第二种做法速度非常快,性能比较优。
5.2. plot函数
请看如下的报表:
图 1.19.
这是一个典型的按照时间段来分组的报表,它有如下两种做法:
第一种:
图 1.20.
第二种:
图 1.21.
分析:
第一种做法采用plot函数,plot函数的功能是对数据集按照给定的数据段进行按段分组,有几个数据段就分成几个组,之后按照数据段的个数对单?
因此,第一种做法的优点是:
采用按段分组函数,只需要对数据集进行一次分组,附属格对组集进行汇总运算时不需要对记录进行遍历检索了,大大加快了运算速度
第二种做法,是比较常规的做法,目前传统报表工具基本上只能采用这一种做法,他把数据段挨个列出来,附属单元格中逐个编辑表达式,通过过滤条件和数据段关联,这种做法每次运算都要检索遍历数据集,运算速度非常慢。
总结:
第一种做法的性能远远优于第二种
6.如何减少冗余单元格占用内存
6.1. 空白单元格的应用
请看如下报表:
图 1.22.
这个报表中,粉红色背景的单元格都属于没用的单元格,但是很多时候必须留着占位用。
例如:
c2单元格,其目的是把报表日期和报表编号分隔开,省得连在一起难看,同时当b4横向扩展出很多单元格时,报表日期要靠右对齐,因此中间必须有c2来占位。
由于润乾报表的界面模型是个规整的矩形,不可能在中间或者边上挖去一块,因此你会发现报表中时常会多出一些无谓的占位格。
在内存中,一个单元格就要占用一块内存,因此单元格越少越好,这种情况下可以尽量使用空白单元格。
空白单元格在内存中是个null,基本不占用内存,因此对于报表边上、中间一些占位格,尽量设成空白单元格,这样既达到了占位的效果,又不会占用内存。
6.2. 慎用隐藏行列
报表中为了进行一些复杂的运算,往往需要用到隐藏行列来处理中间的运算,而这些隐藏行列中被用到的单元格往往只有一两个格子,整行整列的单元格的个数往往很多,此时没被用到的单元格会额外浪费内存,因此要记住把没用的单元格设为空白单元格
6.3. 慎用合并单元格
合并单元格的所有属性都存在左上角的格子中,而合并区域中的其他被合并格并不保存任何属性也不占用内存,因此,做报表的过程中,不少用户习惯对于没用的单元格合并起来,减少内存的占用。
殊不知,这种做法虽然减少了内存,但是由于合并格的运算牵扯到主合并格和被合并格的关系,运算比较复杂,会降低运算速度,因此,我们建议:
没用的格子设为空白单元格,尽量不要合并。
举例:
图 1.23.
第二章、性能管理
7.单用户缓存
7.1.1. 概念定义
单用户缓存是指当某个客户端访问某个报表,引擎将其计算出来后,会将运算结果缓存下来。
同一个客户端访问完该报表后,如果需要对同一报表结果进行别的操作,例如打印、导出、翻页等,引擎直接从缓存里取报表结果,而不必重新计算。
7.1.2. 功能背景
由于WEB服务器与浏览器之间无连接的特性,导致B/S方式下服务器端实体(在我们报表系统中主要是指报表模板、报表及报表分页)的生命周期无法完全与浏览器端保持一致。
举例来说,客户端访问了一张报表,报表计算且向客户端输出完毕后,是否应当从内存中清除报表对象呢?
服务器无法判断客户端是否还需要使用这个对象,事实上,客户端往往还需要翻页、打印、导出等操作,此时还需要使用报表对象;而客户端也完全有可能关闭浏览器,不再访问了。
因此,如果服务器端清除了报表对象,则客户端进行翻页、打印、导出等操作时,不得不重新进行计算,浪费cpu;如果客户端不再访问了而服务器端却保留着报表对象,则会浪费内存。
为了解决这个问题,我们对这些实体采取了带时间管理的缓存策略
如果通过tag标签访问,自动会缓存报表。
通过调用API接口计算报表,如果不调用缓存管理器的API,就不会进行报表缓存。
7.1.3. 使用方法
缓存时间和缓存目录的配置在reportConfig.xml中,其中时间以分钟为单位,如下所示:
--配置缓存报表目录-->
cachedReportDir
c:
\runqian\cached
--配置报表最大未访问时长,以分钟为单位-->
cachedReportTimeout
120
--配置参数最大未访问时长,以分钟为单位-->
cachedParamsTimeout
120
说明:
1.如果缓存报表超过最大未访问时长而没有被访问,则会被引擎清除。
2.引擎对用户输入的参数也做了缓存,这个缓存是在内存中的。
当报表缓存被清除时,如果用户请求翻页等操作,引擎会利用参数缓存自动重新计算报表,而不需要用户重新输入参数
3.如果用户请求翻页等操作时,参数缓存也被清除了,就会提示用户重新访问并输入参数
4.一般来说,参数的最大未访问时长应当比报表的最大未访问时长更长,否则没有意义。
5.引擎每隔5分钟扫描一次缓存,清除超时缓存。
因此,如果上一次扫描刚结束时才到达缓存期限的报表,得到下一次扫描时才会被清除,等于延长了4.999.....分钟,这里的9理论上可以无穷多,接近5分钟。
举例来说,缓存期限为2分钟的报表,如果在扫描结束后才到2分钟,那么下一次扫描时相当于已经缓存了6.999....分钟;再举例,缓存期限为6分钟的报表,如果在扫描结束时才到6分钟,那么下一次扫描时可能已经缓存了10.999....分钟了。
7.2. 多用户间共享缓存
7.2.1. 概念定义
当用户A访问报表A时,引擎会把计算结果缓存下来,当用户B(也可能是用户A再次访问)以同一参数再次访问时,引擎直接把缓存报表返回,而不必重新计算。
7.2.2. 功能背景
用户间共享缓存的目的,是为了避免不同用户访问同一张报表时重复运算的问题。
因此可以把A用户访问报表时计算出的结果报表保存下来,当B用户也访问这个报表时,直接把保存下来的结果报表返回给B用户,不再重新计算。
对于带有参数和宏的报表,当用户采用相同的参数和宏进行重复访问时,也可以利用缓存,减少重复的计算。
但是如果参数和宏不一样,报表就只能重新计算,因为不同参数运算出来的结果报表不一样,无法利用缓存。
因此,对于有参数和宏的报表,缓存时还必须识别参数和宏的值。
7.2.3. 控制方式
用户间共享缓存包括三个层面,模板共享、结果报表共享、分页后报表共享,我们可以通过三种方式对其进行控制。
∙授权控制
授权控制主要是用来控制结果报表共享和分页后报表共享的。
一般是由用户所获得的授权来决定。
∙参数控制
参数控制主要是用来控制模板共享的。
若要实现多用户之间共享模板,可以通过配置reportConfig.xml文件中alwaysReloadDefine参数来实现。
设置alwaysReloadDefine的value为yes时,模板不可以被共享;设置alwaysReloadDefine的value为no时,可以实现模板之间的共享。
∙标签控制
标签控制主要是用来控制结果报表共享和分页后报表共享的。
一般是通过tag标签中的useCache与timeout属性来设置的。
当且仅当reportConfig.xml里的alwayReloadDefine属性设置为no时,tag标签中的useCache与timeout属性才会起作用,useCache属性控制是否启用缓存,而timeout为取多少长时间内生成的报表.
7.2.4. 使用方法
∙参数控制
相关配置在reportConfig.xml中,其中时间以分钟为单位,可配置的属性如下所示:
--是否每次重新装载报表模板-->
alwaysReloadDefine