跨事实查询形状
01

粒度:各自声明一行代表什么

02

聚合:按同一行标题分别汇总

03

对齐:合并聚合结果而非明细

04

扇出:连接放大另一侧度量

05

验收:对齐后合计等于单独聚合

先分别聚合,再按同一行标题对齐

销售事实和库存事实不能先连接再求和。两侧一行所代表的业务事件通常不同,明细连接会把一侧度量按另一侧行数复制,再求和就把复制算进结果。正确形状是每张事实表先按同一组维度属性聚合,再把两个结果集按这些行标题对齐。单独查询销售得到的销售额,对齐之后必须仍然等于该数;库存同理。任一合计变大,就说明连接发生在聚合之前。

这种分别查询再对齐的做法常被称为 drill-across,有的工具叫 stitch 或多次查询。对齐用的是聚合之后的行标题,不是两张事实表的明细键互连。属性为什么要做成可对照的维度,是另一项建模工作;这里只处理对照之后的查询形状,以及形状错误时的扇出。稳定键用于对齐,展示名称只用于显示。名称相同并不保证是同一个成员。

扇出来自聚合前的一对多

错误形状是在明细层把销售和库存按产品、日期连在一起,再同时对金额和数量求和。只要同一键下销售有多行,或库存有多行,连接就不是一对一。求和发生在被放大的行上,两个度量会一起失真,查询却仍然返回一个数字。行数不守恒是检查入口:连接结果的行数大于任一侧在该键上的行数,扇出就已经发生。

正确形状是两条聚合,再做对齐。销售按请求的行标题分组求和,库存按同一组行标题聚合,然后对结果做排序合并;用 SQL 表达时,对应的是聚合结果之间的全外连接,而不是事实表之间的内连接。合并之后不再对原始事实行求和。若行标题还要再汇总到更粗的属性,只允许对已经对齐、且沿该方向可加的度量再求和。这一步不能反过来为明细连接辩护。

用虚构行看放大发生在哪一侧

下面的数字只是例子,不是经营结果。产品 P1 在某日有两行销售,金额 10 和 30,库存快照一行,数量 5。产品 P2 只有库存 7,没有销售。按产品把两张表内连接再求和,P1 的库存变成 10,P2 消失。销售额仍是 40,只因为库存侧只有一行,没有反过来放大销售。

再看产品 P3:一行销售金额 40,两个仓库的库存数量 3 和 2。先连接再求和时,销售额变成 80,库存数量仍是 5。先各自聚合到产品日再全外连接,则 P1 销售 40、库存 5,P2 销售为空、库存 7,P3 销售 40、库存 5。销售合计 80,库存合计 17,与分别聚合相同。P2 的空销售不能在对齐时被内连接删掉,否则库存合计会少掉 P2。

公共粒度必须两侧都支持

分别聚合之前,要声明每张事实表的粒度,以及本次请求的公共行标题。销售明细可以是一行一笔商品销售,库存可以是一行一个仓库、产品和日期的结存。若行标题是产品加日期,库存先按可加规则汇总仓库,再与销售对齐。声明粒度,就是先写清一行代表什么测量事件。

公共粒度比某一侧更细时,不能靠连接把度量拆开。销售没有仓库时,就不能在仓库行标题上与库存对齐,除非已有批准的分摊规则;没有规则就拒绝该粒度,或把销售留在它支持的产品粒度。分摊是另一张事实或桥,不是把连接结果除以行数。更细粒度上的度量如果必须展示,应返回不支持,而不是用平均或复制把数字铺满缺失维度。

外连接、空值和半可加要分开处理

内连接会丢掉只有库存或只有销售的行标题,所以另一侧合计变小。对齐应保留单侧成员。空值表示这一侧没有事实,是否在展示时写成零,要单独声明。把空值默认换成零,会把“没有销售”说成“销售额为零”,这是展示政策,不是聚合结果。全外连接的两侧行标题必须是同一组属性、同一层级。一边是品类、一边是产品,并不是同一组行标题。

库存数量通常不能跨日期相加。先聚合再对齐只避免扇出,不会把半可加度量变成可加度量。一段时间的库存应取期末或指定日快照,再与同期销售对齐,而不是把每日结存加总。销售金额在产品到品类上可加,库存跨日不可加,两条规则同时有效。若业务规定期末库存按仓库可加、按日期不可加,计划里应出现仓库汇总,日期则用于筛选快照,而不是放进求和键。

两条路径都要守住聚合先于对齐

路径一是手写两条聚合,再按相同行标题排序合并。键、属性粒度和空成员要一致,并用单独聚合的合计验收。这条路径透明,但每张报表各自写连接时,有人会为了少扫一次表把连接下推到明细,扇出就回到生产查询里。审查时要看连接的输入是不是已经聚合的结果。也可以把公共粒度预先落入一张宽表,但落表前仍要先分别聚合。否则扇出被保存下来,之后每次读取都在错误行上求和。

路径二由语义层生成分段计划:每个指标在自己的事实和粒度上聚合,再进入只接收聚合结果的对齐节点。dbt 的语义模型把实体、维度和度量分开描述,指标引用度量;计划里应能看到聚合位于事实互连之前。若优化把两段合成明细连接,就和路径一的失败形态相同。路径二便于复用,但必须检查计划节点,不能只看最终一个数字。验收时核对两个聚合节点的分组键和输出行数,再核对对齐节点的输入。对齐输入应等于各聚合的输出,而不是明细行数。

反例与验收

反例一:按产品名称而不是稳定键对齐,同名产品被并成一行或拆成多行。反例二:只用内连接,没有销售的库存日从结果中消失。反例三:对齐之后把每日库存相加,得到无法解释的期间库存。反例四:用去重行数当作通过条件,金额已经翻倍,查询却因为行数看起来正常而放行。

验收至少包括:计划中每张事实表有独立聚合,且聚合在对齐之前;对齐后每个度量等于该事实单独聚合;只有一侧存在的成员保留,并写明空值政策;公共粒度两侧不支持时查询失败,而不是分摊出更细的行。用上面的虚构例子逐键核对 P1、P2、P3 和合计。同一数据快照下重复执行,合计应保持不变。若只比对仪表盘上的一个单元格,一侧的放大可能被另一侧的空值掩盖。

BuildTable.ai 边界

落地时先固定一张销售事实和一张库存快照,写明两侧粒度与公共行标题,用多行样例做扇出测试,再看真实查询计划是否为先聚合后对齐。通过之后再增加第三张事实表。监控对齐前后的行数比,以及每个度量相对单独聚合的合计差。

BuildTable.ai 可作为语义建模入口候选。本文不证明当前版本已经实现跨事实表的先聚合再对齐,也不证明已经实现过滤优先级或已保存查询的语义版本钉住。是否按该查询形状执行,要对照实际语义模型、生成的查询计划和上述验收样例逐项核验。

公开参考资料

开始构建 AI 友好的数据基础

联系我们讨论企业数据建模场景和部署支持。

联系我们