什么是大报表?如何解决大报表的问题?

实际业务中有些报表比较“大”,查询出的报表数据行数可以达到几千万甚至上亿,这类行数很多的报表通常被成为“大报表”。大报表大部分情况下是清单明细报表,少量是分组报表。

大报表查询通常不会采用一次性取出所有记录再交给前端呈现的方式,因为这样要等很久,用户体验极差;而且报表服务器内存也吃不消。

常见的方式是通过分页来呈现大报表,一次只取一小部分数据,取数结束后立刻交给前端呈现,当页码变化时再取出相应页数的数据,这样可以加快报表呈现速度,用户几乎没有等待感。

具体如何实现呢?有几种方式。

1. 数据库分页
业界最常用的做法是使用数据库分页来实现,具体来讲,就是利用数据库提供的返回指定行号范围内记录的语法。界面端根据当前页号计算出行号范围(每页显示固定行数)作为参数拼入 SQL 中,数据库就会只返回当前页的记录,从而实现分页呈现的效果。

主要借助关系数据库自身的能力,每种数据库实现上会有所差异,Oracle 可以使用 rownum,mysql 则可以 limit,具体实现网上有很多资料这里不再赘述。

数据库分页有没有什么不足?任何技术都有其应用范围,数据分页的问题主要集中在以下 4 点。

(1)翻页时效率较差
用这种办法呈现第一页一般都会比较快,但向后翻页时,所使用的取数 SQL 会被再次执行,并且将前面页涉及的记录跳过。对于有些没有 OFFSET 关键字的数据库,就只能由界面端自行跳过这些数据(取出后丢弃),而像 ORACLE 还需要用子查询产生一个序号才能再用序号做过滤。这些动作都会降低效率,浪费时间,前几页还感觉不明显,但如果页号比较大时,翻页就会有等待感了。

(2) 可能出现数据不一致
用这种办法翻页,每次按页取数时都需要独立地发出 SQL。这样,如果在两页取数之间又有了插入、删除动作,那么取的数反映的是最新的数据情况,很可能和原来的页号匹配不上。例如,每页 20 行,在第 1 页取出后,用户还没有翻第 2 页前,第 1 页包含的 20 行记录中被删除了 1 行,那么用户翻页时取出的第 2 页的第 1 行实际上是删除操作前的第 22 行记录,而原来的第 21 行实际上落到第 1 页去了,如果要看,还要翻回第 1 页才能看到。如果还要基于取出的数据做汇总统计,那就会出现错误、不一致的结果。

为了克服这两个问题,有时候我们还会用另一种方法,用 SQL 游标从数据库中取数,在取出一页呈现后,但并不终止这个游标,在翻下一页的时候再继续取数。这种方法能有效地克服上述两个问题,翻页效率较高,而且不会发生不一致的情况。不过,绝大多数的数据库游标只能单向从前往后取数,表现在界面上就只能向后翻页了,这一点很难向业务用户交代,所以很少用这种办法。

当然,我们也可以结合这两种办法,向后翻页时用游标,一旦需要向前翻页,就重新执行取数 SQL。这样会比每次分页都重新取数的体验好一些,但并没有在根本上解决问题。

(3) 无法实现分组报表
除了清单报表,有时我们还要呈现大数据量的分组报表,报表包含分组、分组汇总及分组明细数据。我们知道,按页取数按照翻页每次读取固定条数(一页或几页)记录,这样根本无法保证一次性读取一个完整分组,而分组呈现、分组汇总都要求基于整组数据来操作,否则就会出错。

(4) 无法使用其他数据源
数据库分页是借助关系数据库自身的能力,而对于非关系数据库就不灵了。试想一下,NoSQL 怎么用 SQL 分页,文本怎么做分页?

报表的多样性数据源话题我们曾经讨论过,在数据规模迅速膨胀的今天,基于非关系数据库出报表已经非常普遍。


除了数据库分页,还有其他更好的方式吗?

2. 硬编码实现
我们发现基于数据库的分页方式强依赖数据源,无法满足其他数据源类型的需要,也就是与数据库紧耦合的。我们需要一个低耦合数据源的实现方式以应对多样性数据源场景,还能同时解决效率、准确性和分组呈现问题。

按照主流的解题思路,可以通过编码方式实现这个目标,实现思路大概是这样:

基于数据库时,
把取数和呈现做现两个异步线程,取数线程发出 SQL 后就不断取出数据后缓存到本地存储中,呈现线程根据页数计算出行数到本地缓存中去获取数据显示。这样,只要已经取过的数据就能快速呈现,不会有等待感,还没取到的数据需要等待一下也是正常可理解的;而取数线程只涉及一句 SQL,在数据库中是同一个事务,也不会有不一致的问题。这样,两个问题都能得到解决。不过这需要设计一种可以按行号随机访问记录的存储格式,不然要靠遍历把记录数出来,那反应仍然会很迟钝。

基于非数据库时,
同样设置取数和呈现两个异步线程,取数时通过文件游标(或其他数据源提供的分批取数接口)分批读取数据并缓存到本地,呈现阶段则与上述方式完全一致。

然后再利用报表工具开放的接口和前端报表进行交互,完成报表的分页呈现。

做分组报表时,
就需要一次性取出完整分组,在代码里进行分组汇总,并将汇总结果插入结果集一并返回给前端报表进行呈现,同时还要设置分组记录标志位,以便前端报表在呈现时能够为分组行设置不同的显示效果(如加粗、标红)。

实现后的效果类似下面这样:

取数线程不停地取数缓存,呈现线程从缓存中读取数据呈现,总页数不断变化

通过上面的描述,可以看到自己硬编码虽然可以实现,但复杂度很高。除了多线程编程,还要考虑缓存数据存储形式、文件游标、分组读取与汇总,此外借助其他报表工具进行呈现时还要对方开放足够灵活的接口,…,这些都是挑战。

而且我们只考虑了呈现,如果还要导出 Excel 怎么办?如果还要打印怎么办?毕竟报表既然能查就应该能导出,能打印。这些需求在金融、制造行业都是真实存在的。


(3) 使用支持大报表的报表工具
如果前端使用报表工具开发报表,选用一个直接支持大报表呈现、导出、打印的报表工具则更为直接。工具实现了两个异步线程的大报表机制,同时解决上面我们提到的问题,这些方面都封装好直接使用。

大数据时代要支持大报表,这应该是报表工具必备能力。

你可能感兴趣的:(什么是大报表?如何解决大报表的问题?)