连接运算 4- 维度对齐
四. 维度对齐
我们再回顾前面的双子表例子的 SQL:
SELECT Orders.id, Orders.customer, A.x, B.y
FROM Orders
LEFT JOIN (SELECT id,SUM(price) x FROM OrderDetail GROUP BY id ) A
ON Orders.id=A.id
LEFT JOIN (SELECT id,SUM(amount) y FROM OrderPayment GROUP BY id ) B
ON Orders.id=B.id
WHERE A.x > B.y
那么问题来了,这显然是个有业务意义的 JOIN,它算是前面所说的哪一类呢?
这个 JOIN 涉及了表 Orders 和子查询 A 与 B,仔细观察会发现,子查询带有 GROUP BY id 的子句,显然,其结果集将以 id 为主键。这样,JOIN 涉及的三个表(子查询也算作是个临时表)的主键是相同的,它们是一对一的同维表,仍然在前述的范围内。
但是,这个同维表 JOIN 却不能用前面说的写法简化,子查询 A,B 都不能省略不写。
可以简化书写的原因在于:我们假定事先知道数据结构中这些表之间的关联关系。用技术术语的说法,就是知道数据库的元数据(metadata)。而对于临时产生的子查询,显然不可能事先定义在元数据中了,这时候就必须明确指定要 JOIN 的表(子查询)。
不过,虽然 JOIN 的表(子查询)不能省略,但关联字段总是主键。子查询的主键总是由 GROUP BY 产生,而 GROUP BY 的字段一定要被选出用于做外层 JOIN;并且这几个子查询涉及的子表是互相独立的,它们之间不会再有关联计算了,我们就可以把 GROUP 动作以及聚合式直接放到主句中,从而消除一层子查询:
SELECT Orders.id, Orders.customer, OrderDetail.SUM(price) x, OrderParyment.SUM(amount) y
FROM Orders
LEFT JOIN OrderDetail GROUP BY id
LEFT JOIN OrderPayment GROUP BY id
WHERE A.x > B.y
这里的 JOIN 和 SQL 定义的 JOIN 运算已经差别很大,完全没有笛卡尔积的意思了。而且,也不同于 SQL 的 JOIN 运算将定义在任何两个表之间,这里的 JOIN,OrderDetail 和 OrderPayment 以及 Orders 都是向一个共同的主键 id 对齐,即所有表都向某一套基准维度对齐。而由于各表的维度(主键)不同,对齐时可能会有 GROUP BY,在引用该表字段时就会相应地出现聚合运算。OrderDetail 和 OrderPayment 甚至 Orders 之间都不直接发生关联,在书写运算时当然就不用关心它们之间的关系,甚至不必关心另一个表是否存在。而 SQL 那种笛卡尔积式的 JOIN 则总要找一个甚至多个表来定义关联,一旦减少或修改表时就要同时考虑关联表,增大理解难度。
我们称这种 JOIN 称为维度对齐,它并不超出我们前面说过的三种 JOIN 范围,但确实在语法描述上会有不同,这里的 JOIN 不象 SQL 中是个动词,却更象个连词。而且,和前面三种基本 JOIN 中不会或很少发生 FULL JOIN 的情况不同,维度对齐的场景下 FULL JOIN 并不是很罕见的情况。
虽然我们从主子表的例子抽象出维度对齐,但这种 JOIN 并不要求 JOIN 的表是主子表(事实上从前面的语法可知,主子表运算还不用写这么麻烦),任何多个表都可以这么关联,而且关联字段也完全不必要是主键或主键的部分。
设有合同表,回款表和发票表:
Contract 合同表
-
id 合同编号
-
date 签订日期
-
customer 客户
-
price 合同金额
-
…
Payment 回款表
-
seq 回款序号
-
date 回款日期
-
source 回款来源
-
amount 金额
-
…
Invoice 发票表
-
code 发票编号
-
date 开票日期
-
customer 客户
-
amount 开票金额
-
…
现在想统计每一天的合同额、回款额以及发票额,就可以写成:
SELECT Contract.SUM(price), Payment.SUM(amount), Invoice.SUM(amount) ON date
FROM Contract GROUP BY date
FULL JOIN Payment GROUP BY date
FULL JOIN Invoice GROUP BY date
这里需要把 date 在 SELECT 后单独列出来表示结果集按日期对齐。
这种写法,不必关心这三个表之间的关联关系,各自写各自有关的部分就行,似乎这几个表就没有关联关系,把它们连到一起的就是那个要共同对齐的维度(这里是 date)。
这几种 JOIN 情况还可能混合出现。
继续举例,延用上面的合同表,再有客户表和销售员表
Customer 客户表
-
id 客户编号
-
name 客户名称
-
area 所在地区
-
…
Sales 销售员表
-
id 员工编号
-
name 姓名
-
area 负责地区
-
…
其中 Contract 表中 customer 字段是指向 Customer 表的外键。
现在我们想统计每个地区的销售员数量及合同额:
SELECT Sales.COUNT(1), Contract.SUM(price) ON area
FROM Sales GROUP BY area
FULL JOIN Contract GROUP BY customer.area
维度对齐可以和外键属性化的写法配合合作。
这些例子中,最终的 JOIN 都是同维表。事实上,维度对齐还有主子表对齐的情况,不过相对罕见,我们这里就不深入讨论了。
另外,目前这些简化语法仍然是示意性 ,需要在严格定义维度概念之后才能相应地形式化,成为可以解释执行的句子。
我们把这种简化的语法称为 DQL(Dimensional Query Languange),DQL 是以维度为核心的查询语言。我们已经将 DQL 在工程上做了实现,并作为润乾报表的 DQL 服务器发布出来,它能将 DQL 语句翻译成 SQL 语句执行,也就是可以在任何关系数据库上运行。
对 DQL 理论和应用感兴趣的读者可以关注乾学院上发布的论文和相关文章。