DQL维度查询语言:迈向自助报表的关键一步

自助报表与SQL

让业务人员自己做报表,是报表业界的一贯需求。作为重要的报表工具厂商,润乾当然也被无数用户要求过这一功能。在润乾报表4.x版本中曾引入语义层模块试图实现这个功能,但使用者很少,坦白地说,这是个失败的尝试。而且,不只是我们,整个业界也没有什么好的解决方案让业务人员有够自己做报表,虽然有一些界面很炫丽、操作也很流畅的产品。

自助报表的技术实质是查询
业务人员自己做报表,大多数目的是要看到报表中的数据,而不是关注报表格式。格式要求严格的报表大都是事先就有表样,会作为固定报表由程序员开发好。即使是关注格式的业务用户,也可以把数据倒出来后用Excel再处理。自助报表的着眼点在于“自助”这两个字,而不是“报表”。
我们可以假定要查询的数据已经在数据库中,而从数据库中取出数据就需要用SQL,那么“自助”报表要解决的技术问题就是提供某种界面能让用户“画”出SQL。

查询问题出在SQL
对于针对单表的查询,并不是很难理解和实施,选出字段再配上过滤条件及排序,和用Excel差不太多,分组汇总会稍复杂些,但也不是多难懂。
但是,有业务意义的查询经常涉及多表关联,比如查询存储余额10万元以上的储户中本地人的比例,看看某月回款额与发票额的对比。这些都需要多表关联,也就是要用到SQL的JOIN。
再但是,业务人员很难理解SQL的JOIN,多个表及其关系是个网状形式,要指定关联字段,还会涉及自关联、递归关联还有子查询再关联的复杂情况。三五个关联表之间的数据关系就足够搞晕业务人员了。这时候,界面再炫丽和操作再流畅都没有什么意义了。

究其原因,是因为SQL对JOIN运算的定义过于简单,也就是笛卡尔积后再过滤。
简单有简单的好处,适应面广泛,什么都可以表达,但坏处就是表达起来会很繁琐,没有把更多的运算特征体现出来。我们经常用的一个类比,整数四则运算中,乘法都可以用加法表示,没有乘法也不会出现什么算不了的东西,但可想而如,如果没有乘法只有加法,表达某些运算的困难度会增大多少。
其实发明SQL的初衷是想让业务人员使用的,为什么这么说呢?因为它长得很象英语,其它计算机语言都不那么象了。SQL中有些简单的查询真地可以当英语读,它希望会说英语就会查询。但我们现在知道,这个目标完全没有达到,业务人员能理解和写出的SQL只是极少数简单情况,有些复杂些的SQL甚至连专业程序员写不好。JOIN以及相关的子查询就是导致难度徒增的原因之一。

按需建模

在SQL的理论体系下,没办法做一个界面能让业务人员理解并直接实施JOIN运算。目前采用的变通手段就是建模,当前市场上的自助报表产品,以及BI产品中的OLAP和自助查询功能,都是这么做的。
所谓建模,就是把表间关联运算做成逻辑视图或物理宽表,这样业务人员在查询时相当于只要面向逻辑上的单表。而准备这个模型时需要事先预测可能的关联,发生新的关联需求时又需要再做修改或新建这些模型,所以可以称为按需建模。
有些OLAP产品走得远一些,能根据维度类型自动关联,但解决得仍不彻底,当同一表中有多个同维字段就对不上,比如学生有出生地和入学地都是地区维度。对于自关联处理也很麻烦,要事先指定路径,也还是按需建模。
由于查询有很强的不可预测性,按需建模机制就意味着经常有更新模型的事,而建模需要理解和处理JOIN运算,这超出了业务人员的能力范围,必须借助技术部门完成,这样,自助报表也就不能“自助”了。在某些较大的机构,调动技术部门配合工作的周期很长,等建模完成后很可能已经不需要这个查询了。

DQL解决JOIN

要解决这个问题,需要重新分析研究SQL的JOIN运算。
我们发现,自助报表中需要的JOIN,绝大多数属于这么三种情况:

  1. 外键连接,多对1的LEFT JOIN
  2. 同维表对齐,1对1的LEFT JOIN或FULL JOIN
  3. 汇总后按维对齐,1对1的FULL JOIN或JOIN,LEFT JOIN较少见

我们针对这三种情况,重新定义JOIN运算,改造SQL语法形成另一种类似的查询语言,也就是这里所说的DQL,D是即Dimensional维度的意思。
这里所说的绝大多数,并不是全部,就象不可能把所有的加法都用乘法表示一样,有些关联计算仍然需要原始的JOIN定义来描述,比如做矩阵乘法。不在这三种情况的JOIN在自助报表中相对罕见,我们也不指望解决百分之百的问题,能解决百分之八九十的问题,就已经能让自助报表的“自助”性强很多,相当程度地减少业务部门对技术部门的“骚扰”。

我们来分别看一下DQL在上面几种情况的写法和优势。

外键属性化


这里有两个数据表:员工表和部门表,员工表里有个部门外键字段指向部门表的主键,部门表里又有经理外键字段指回员工表,这是很常见的数据结构设计。
现在,我们想查询中国经理的美国员工,SQL写出来是这样的:
SELECT A.* FROM 员工表 A,部门表 B,员工表 C
WHERE A.国籍='美国' AND C.国籍='中国' AND
A.部门=B.编号 AND B.经理=C.编号
员工表和部门表JOIN,再JOIN回员工表,也就是同表自连接,这需要起个别名。在WHERE中写上JOIN的条件和最终我们希望的条件。整个句子要看一会才能明白。
说句题外话,这曾经是我公司的招聘考题,面向有两三年信息系统工作经验的应聘者,通过率不到一半。

使用DQL会写成这样:
SELECT * FROM 员工表
WHERE 国籍='美国' AND 部门.经理.国籍='中国'
这个句子中,美国员工好理解,中国经理的条件被写成红色部分,稍复杂一点,字段有了子属性,子属性又有子属性,但并不难理解,也就是部门的经理的国籍是中国。

在DQL的语法体系中,外键被看成了属性,外键指向表的字段可直接用子属性的方式引用,也允许多层和递归引用。

同维表等同化


这是两个1比1的表,主键相同,在数据库设计中经常有这种情况,字段的业务分类不同,不适合都放在一个表里,太宽的表在各字段丰满度相差较大时还会造成空间冗余浪费,访问性能也下降,因此常常会分到多个主键相同的表中。
现在我们要查询计算所有员工的收入,SQL中需要做JOIN:
SELECT 员工表.姓名,员工表.工资+经理表.津贴
FROM 员工表 LEFT JOIN 经理表 ON 员工表.编码=经理表.编号
而DQL可以把这两个表看成一个表访问:
SELECT 姓名,工资+津贴 FROM 员工表
红色的部分实际上来自两个物理表,DQL把主键同维的表将视为一个宽表,访问其中任何一个均可引用其它表的字段。

按维对齐汇总


这里有三个表:合同表、回款表和库存表,我们希望按日期统计合同额、回款额和库存金额,用SQL写出来是这样的:
SELECT T1.日期,T1.金额,T2.金额 FROM
(SELECT 日期, SUM(金额) 金额 FROM 合同表 GROUP BY 日期)T1,
(SELECT 日期, SUM(金额) 金额 FROM 回款表 GROUP BY 日期)T2,
(SELECT 日期, SUM(金额) 金额 FROM 库存表 GROUP BY 日期 ) T3
WHERE T1.日期 = T2.日期 AND T2.日期=T3.日期
用子查询把每个表分组汇总后再JOIN起来,如果偷懒不用子查询先JOIN后GROUP,那结果是错误的,统计值会变多。这个问题必须使用子查询。
这里涉及的三个子查询都要连接上,SQL的JOIN关系要写成若干个两表关联,在表比较多时,增删关联表有可能把某个表漏掉而没有连接条件,出现完全叉乘。

用DQL写出来是这样的:
SELECT 合同表.SUM(金额),回款表.SUM(金额),库存表.SUM(金额) ON 日期
FROM 合同表 BY 日期 JOIN 回款表 BY 日期 JOIN 库存表 BY 日期
在DQL中,只要把这几个表分别按日期对齐分别汇总就行了,而不必关心这些表之间的关系,在增删表时也不容易发生遗漏。

如果按维对齐再与外键搅到一起,情况就会更复杂:

我们希望按地区统计销售员人数和合同额,用SQL写出来是这样:
SELECT T1.地区,T1.数量,T2.金额 FROM
(SELECT 地区,COUNT(1) 数量 FROM 销售员 GROUP BY 地区)T1,
(SELECT 客户表.地区 地区,SUM(合同.金额) 金额 FROM 客户表,合同表
WHERE 客户表.编号=合同表.客户 GROUP BY 客户表.地区 ) T2
WHERE T1.地区 = T2.地区
这个子查询中要套着带JOIN的GROUP,很复杂。

         而在DQL中,可以和外键属性化结合,这样查询会写成:
SELECT 销售员.count(1),合同表.sum(金额) ON 地区
FROM 销售员 BY 地区 JOIN 合同表 BY 客户表.地区
红色部分中出现了子属性,但整个句子仍然很简单,DQL允许每个表独立设定统计维度,无须关心表间关联,还可以与属性化的外键配合使用。

数据模型

采用DQL方式看待表间关联还能让数据结构显得更为清晰。

这是我们平时看到的E-R图,它是个网状结构的,表与表之间可能都有关联,表多了就会显得很零乱,增删表的时间很容易遗漏或重复表间的关联。
而在DQL体系下看到的表间关联是总线式的:

表与表之间没有直接的关联,都只处在中间地位的维度关联,增删表的时候不会影响到其它表,数据结构耦合度低。
不过,要说明的是,无论是E-R图还是后面的总线图,其中连线的数量都是相当的,这是数据关系本身决定的,不会因为改变了看待方法而变少,只是总线式看着更清晰些。

超维报表

体系结构

前面讲过的那几个多表JOIN以及带子查询的SQL例子,都是在实际应用中很可能正常发生的业务人员查询需求,可以设想一下需要什么样的界面才能编辑这样的SQL?这些例子可以用来检查自助报表产品的“自助”程度到底有多少,可否不更新模型就由业务人员自行完成这些查询。结果会发现,业内的OLAP和可视化报表工具,无论界面多炫丽、操作多流畅,都经不起这个检验。

不过,有了DQL之后,JOIN问题得到了较好的解决,这时就可以开发出可视化界面,实现有相当自由度的自助报表功能,也就是超维报表。

这是超维报表的体系结构,其中的DQL服务器负责将DQL语法翻译成SQL语法,DQL的JDBC驱动会再将翻译过的SQL交由数据库执行,取和结果返回。DQL服务器本身并不做实质的数据计算,相当于逻辑数据库。
DQL服务器需要使用事先编辑好的数据库元数据信息,也就是数据表的结构以及表之间的关联信息。编辑元数据的过程也就是建模了,但与前面说的按需建模不同,使用DQL时只需要一次性建模,有新的关联查询需求时并不需要更新模型,只有数据库中表及字段以及关联发生了改变才要再次改造模型。这种机制我们称为非按需建模。

界面控件

查询界面

我们假定业务人员不理解数据表这个概念,所以直接看到所有的数据项并可以拖拽选择。这里的数据项稍复杂些,不再是个简单列表,而是个有层次的树形结构,这里的层次关系就隐含体现了表之间的外键关系。
选择数据项时只要把需要的数据项往表里拽就行了。这和普通单表时取数并没有没太大区别,无非就是字段有多层子属性。查询控件会自动把不能和现有已选择数据项匹配的数据项过滤隐藏掉,而且会自动调整已拖入字段的来源。用户只要想象一下目标报表的表头,依次把这个表头对应的数据项拖拽进来就可以了。
做汇总表也很简单,可以直接拖入汇总的数据项。控件会自动建立汇总项与统计维度之间的匹配关系,个别有多种匹配的情况则需要由用户选择,用户不必关心这些数据项来自什么数据表,以及这些数据表之间有什么关联。
这个界面生成DQL语法是很容易的,再经过DQL服务器执行,就可以完成查询工作了。

报表控件
完成数据源之后,就可以进入报表界面选定格式生成可查看打印的报表和统计图:

在报表控件中可以再进行一般的OLAP式操作,如切片、旋转、钻取等动作。可以使用自定义的报表模板呈现出各种风格的表样及图形。

集成性
超维报表虽然提供有面向业务人员的界面控件,但并不是直接可用的应用产品,它和润乾的其它报表产品类似,仍然是个中间件,即需要由程序员集成到应用系统中才能使用,在实施时也需要技术人员事先对数据进行描述和建模。
这个定位与市场上其它有自助报表产品有很大不同,其它产品大多都是一个直接可用的应用产品,有用户管理、资源存储等功能,而集成性相对较差,不适合嵌入到其它应用程序中,一般只能是WEB服务的方式结合。
超维报表则可以作为WEB页面控件无缝集成到应用程序中。相反的,超维报表中没有用户管理与权限、资源组织与存储这类功能,这些需要应用程序在外围自行实现。
说到用户权限,DQL的元数据中提供了表的可见性及角色宏的概念,可以自动根据当前登录角色的宏值在DQL中加入WHERE子句,以保证不同用户看到的数据不同。

汇总表透明化

除了解决JOIN外,DQL还能更方便地实现汇总表透明化。
透明化有利于程序移植,当物理结构在其它场合上发生改变时,只要逻辑结构未发生变化,只要修改DQL服务器用到的元数据即可,上层应用是不用改变的。

有时由于数据量太大,我们需要将一些汇总数据事先计算好存放在数据库中,查询时可以直接访问汇总表而获得更高的性能。但是,因为汇总表的建设有因地制宜的原则,同一个应用的汇总表在不同用户场景下可能不同,有些地方这类数据多需要建汇总表,而换一个地域应用时可能是另一类数据较多,直接访问汇总表会导致数据结构的不透明,程序与汇总表的耦合度变高。
SQL的理论体系中没有维度的概念,可以针对任何表达式实施GROUP BY,从SQL语句中很难拆出维度及相关汇总项的关系信息;DQL是很强化了维度的概念,只能针对维度做汇总,维度与汇总项的关系在DQL中很明确。
这样,只要我们在DQL的元数据中可以定义好基础表和汇总表之间的对应关系,在将DQL翻译SQL时就能很容易地把针对基础的查询自动转换成针对汇总表的查询,而且可以找到可用汇总表中层次最高的那个,也就是涉及量最少的即性能最好的路径。程序员在写查询语句时可以不关心汇总表的存在,只看到逻辑结构中的基础表,汇总表完全由管理人员根据数据量的情况来创建和维护。
类似地,DQL服务器在运行过程中还能记录对汇总数据的请求历史,从而统计对哪些基础表的哪些维度的聚合最常用,并可以此为依据去建立和维护汇总表。DQL语法中天然含有这些维度与汇总项的关系信息,很容易分析统计。