跨事实对齐(D-CONFORM)¶
位置:Relationship 的 custom_extensions。
载荷键:cardinality,枚举 "many_to_one" | "many_to_many"。
缺省(不存在时):"many_to_one" —— 普通的外键边,连接行走会沿着它走。
忽略时的影响:silent —— 请列进 requires。
两张事实表经常共享维度 —— 日期、平台 —— 却谁也不是谁的多端:一条登录记录并不
属于某条付费记录。严格 OSI 只能通过共享的维表把它们关联起来,于是「按日期算付费
率(付费人数 / 活跃人数)」这样的跨事实指标没有任何日期可以分组
(unconformed_dimension)。many_to_many 直接声明这个配对:每一对
from_columns / to_columns 就是同一个维度在两侧的两种拼写。为什么事实表要对齐而不是直接 JOIN,见粒度:一行代表什么。
relationships:
- name: login_payment_conformed
from: netbar_login
to: netbar_payment
from_columns: [stat_date, platform_id]
to_columns: [pay_date, platform_id]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.8", "requires": ["cardinality"], "cardinality": "many_to_many"}'
一致性边怎么查询¶
一致性边永远不会逐行 join —— 那会把每一个 SUM / COUNT 都扇出。每张事实表
各自聚合,把自己那侧的配对列投影成查询的输出名,然后各分支按这个名字合并,和
S-FAN-3 合并任意两个分支的方式完全一样:
WITH m0 AS (SELECT netbar_payment.pay_date AS stat_date, COUNT(DISTINCT netbar_payment.player_id) AS payers
FROM main.netbar_payment GROUP BY netbar_payment.pay_date),
m1 AS (SELECT netbar_login.stat_date AS stat_date, COUNT(DISTINCT netbar_login.player_id) AS users
FROM main.netbar_login GROUP BY netbar_login.stat_date)
SELECT COALESCE(m0.stat_date, m1.stat_date) AS stat_date, CAST(m0.payers AS DOUBLE) / m1.users AS pay_rate
FROM m0 FULL OUTER JOIN m1 ON m0.stat_date = m1.stat_date OR m0.stat_date IS NULL AND m1.stat_date IS NULL
配对带来的能力¶
- 两种拼写都能用在
group_by、where_sql和time_range.dimension里,对任一侧的 指标都成立 —— 只在登录表上的指标也能按netbar_payment.pay_date分组;输出列名 沿用查询里的那种拼写。 metric_time在两侧的主时间维度配对时按分支解析:每个分支按自己的日期列 过滤和分组。- 粒度上卷发生在各分支内部(
stat_date:month):每张事实表截断自己的列, 合并键是截断后的值。存储粒度不同的事实表也可以配对,较粗一侧的 D-GRAIN 下限生效。 - 可传递:payment ↔ login 加 login ↔ register,就把 payment 和 register 也配上 了 —— 三张事实表,一个日期维度。
- 只有配对列是共享的。跨事实指标按未配对的列分组是
unconformed_dimension; 按它过滤是no_join_path,并提示这个过滤条件没法只落在一侧(改成 D-DERIVE 的filter成员)。一致性边不能出现在关系路径里(invalid_relationship_path)—— 对面没有任何一行可以走到。 - 只列一次。按指标调用
list_dimensions时,一个一致性维度只占一行,用指标 自身数据集上的拼写(任何一种拼写仍然可以写进group_by)。配对列恰好是指标的 时间轴时,这一行就是metric_time。 - S-FAN-6 照旧:比率里面的计数在只有一侧有数据的日期读 NULL;只有纯计数指标 补 0。
- 配对的两侧可以存成不同格式 —— 一侧是原生
DATE,另一侧是 D-FORMAT 的%Y%m%d字符串。配对说的是两列是同一个维度,不是同一种编码,所以规划期 搬过去的每一个边界值都会按目标列自己的编码折算:单个时间范围、每个 tag 的CASE闸门,以及这些闸门 OR 起来的扫描谓词。唯一搬不过去的是where_sql里的 比较 —— D-FORMAT 本来就拒绝编码列出现在那里(字面量不会是它的编码),点名配对 另一侧的原生列同样被拒绝,而不是悄悄改写(unsupported_filter,消息里两种拼写 都点出来)。改用time_range指定这个维度。 - 窗口指标也可以按任一拼写分组,配对的两列日期就是同一条时间轴。落在一张
事实表上的窗口,用另一张事实表的名字问出来,仍然是这张事实表自己的序列、按自己
的列排序,输出名保持查询写的那个。窗口自身的成员跨两张配对事实表时(比如
revenue - refunds的环比),合并后的行按查询写的名字排序,每个分支读自己的 日期;不需要指标级的time_dimension,时间范围写在任一拼写(或metric_time) 上都会落到两张事实表。日期没有配对的事实表没有共同的时间轴 (no_primary_time_dimension,提示去配对)。
校验¶
声明一次、检查一次:配对列必须在 dimension.is_time 上一致,两侧都声明了
datatype 时类型也要一致;一个数据集在一个一致性类里至多贡献一列;many_to_many
边上不允许 join_type(invalid_datus_extension)。模型校验仍然会对这条边发
relationship_target_not_unique 告警 —— 在严格 OSI 下这是对的,也是忽略该键的
消费者能得到的唯一信号。
忽略它为什么是 silent¶
为什么是 silent:丢掉这个键的引擎看到的是一条普通的 many→one 边,会把两张事实表
逐行 join。COUNT DISTINCT 类的 compose 会返回看起来合理的数字(当天没付费的
登录全部落进 NULL 桶);跨 join 的 SUM 只在维度落在对面事实表上时才被 fan_out_risk
拒绝,其余情况就重复计数。把 cardinality 列进 requires,缺少它的 datus 模式
引擎就会在版本闸门处失败。基础模式就是这种降级本身。