语义视图关系建模与聚合粒度

语义视图通过外键关系自动处理表连接和聚合。关系怎么建,直接决定查询结果对不对——同样的数据,关系定义不同,"订单数""客户数"可能差几倍。本文用一组带边界数据的例子,说明外键关系如何影响 JOIN 和聚合粒度,以及哪些场景会被引擎拦截。

前置准备

全文示例共用以下四张表。数据特意构造了几种边界情况:一笔没有对应客户的孤儿订单、一个没有任何订单的客户、以及一个有多个地址的客户。

CREATE TABLE doc_customers (c_custkey INT, c_name STRING, c_city STRING); CREATE TABLE doc_orders (o_orderkey INT, o_custkey INT, o_totalprice DECIMAL(12,2), o_orderdate DATE); CREATE TABLE doc_line_items (l_orderkey INT, l_linenumber INT, l_qty INT); CREATE TABLE doc_addresses (a_custkey INT, a_type STRING); INSERT INTO doc_customers VALUES (1,'Alice','New York'),(2,'Bob','Boston'),(3,'Carol','New York'),(4,'Dave','LA'); INSERT INTO doc_orders VALUES (101,1,250.00,DATE'2025-01-01'),(102,1,150.00,DATE'2025-02-01'), (103,2,300.00,DATE'2025-01-15'),(104,3,500.00,DATE'2025-03-01'), (105,99,999.00,DATE'2025-04-01'); -- 客户 99 不存在:孤儿订单 INSERT INTO doc_line_items VALUES (101,1,5),(101,2,3),(103,1,7),(104,1,2); INSERT INTO doc_addresses VALUES (1,'home'),(1,'work'),(2,'home'),(3,'home'),(3,'work'),(3,'billing');

数据特征:Dave(客户 4)没有订单;订单 105 的客户号 99 在客户表里不存在;Alice 有 2 笔订单、2 个地址,Carol 有 1 笔订单、3 个地址。

语义视图定义如下。customers 下挂两条一对多分支(orders、addresses),orders 下再挂 line_items:

CREATE SEMANTIC VIEW doc_rel_analysis TABLES ( customers AS doc_customers PRIMARY KEY (c_custkey), orders AS doc_orders PRIMARY KEY (o_orderkey) FOREIGN KEY (o_custkey) REFERENCES customers, line_items AS doc_line_items PRIMARY KEY (l_orderkey, l_linenumber) FOREIGN KEY (l_orderkey) REFERENCES orders, addresses AS doc_addresses PRIMARY KEY (a_custkey, a_type) FOREIGN KEY (a_custkey) REFERENCES customers ) DIMENSIONS ( customers.customer_name AS customers.c_name, customers.customer_city AS customers.c_city ) METRICS ( orders.order_count AS COUNT(orders.o_orderkey), orders.order_total AS SUM(orders.o_totalprice), line_items.qty_total AS SUM(line_items.l_qty), addresses.addr_count AS COUNT(addresses.a_type) );

查询粒度由指标所在表驱动

按客户名分组、查订单指标时,结果不是"每个客户一行",而是以订单表为事实表、把客户属性挂上去:

SELECT * FROM semantic_view( doc_rel_analysis, DIMENSIONS customers.customer_name, METRICS orders.order_count, METRICS orders.order_total ) ORDER BY customer_name;

+---------------+-------------+-------------+ | customer_name | order_count | order_total | +---------------+-------------+-------------+ | NULL | 1 | 999.00 | | Alice | 2 | 400.00 | | Bob | 1 | 300.00 | | Carol | 1 | 500.00 | +---------------+-------------+-------------+

两个关键现象:

  • 孤儿订单出现,但维度为 NULL:订单 105 的客户号 99 在客户表里不存在,它仍出现在结果中,
    customer_name
    customer_name
    NULL
    NULL
    。说明订单不会因为关联不上客户而被丢弃。
  • 无订单的客户不出现:Dave 没有任何订单,结果里没有他。客户属性是挂在订单事实上的,没有订单事实就没有对应行。

记住这个心智模型:查询的粒度由指标所在的表决定,维度表的属性沿外键关系挂上去。如果你需要"列出所有客户(含无订单的)",应直接查客户表,而不是查语义视图的订单指标。

链路内扇出:一对多的正确聚合

当查询涉及一条关系链上的两级(customers→orders→line_items),引擎会在各自的粒度上计算指标,不会因为 JOIN 放大:

SELECT * FROM semantic_view( doc_rel_analysis, DIMENSIONS customers.customer_name, METRICS orders.order_count, METRICS line_items.qty_total ) ORDER BY customer_name;

+---------------+-------------+-----------+ | customer_name | order_count | qty_total | +---------------+-------------+-----------+ | Alice | 2 | 8 | | Bob | 1 | 7 | | Carol | 1 | 2 | +---------------+-------------+-----------+

Alice 有 2 笔订单(101、102),订单 101 含 2 行明细(数量 5、3),订单 102 无明细。

order_count
order_count
是 2 而不是被明细行数放大成 3,
qty_total
qty_total
正确累加为 8。这正是语义层相对手写 JOIN 的价值:手写
orders JOIN line_items
orders JOIN line_items
后对订单做
COUNT
COUNT
会重复计数,语义视图自动按指标的原始粒度聚合。

多分支与 chasm trap

orders 和 addresses 是 customers 下两条独立的一对多分支。把它们的指标放进同一次查询会报错:

SELECT * FROM semantic_view( doc_rel_analysis, DIMENSIONS customers.customer_name, METRICS orders.order_count, METRICS addresses.addr_count );

CZLH-65000: Compiler internal error - generating logical plan failed, error message No relationship found for table addresses

这是经典的 chasm trap(扇出陷阱):如果引擎天真地把 orders 和 addresses 通过 customers 连在一起,Alice 的 2 笔订单会和 2 个地址交叉成 4 行,把订单数和地址数同时放大。引擎选择直接报错,而不是静默返回错数。

对策是分两次查询,每个分支各自单独聚合。地址分支单独查是正确的:

SELECT * FROM semantic_view( doc_rel_analysis, DIMENSIONS customers.customer_name, METRICS addresses.addr_count ) ORDER BY customer_name;

+---------------+------------+ | customer_name | addr_count | +---------------+------------+ | Alice | 2 | | Bob | 1 | | Carol | 3 | +---------------+------------+

多列主键与多跳外键

line_items 用复合主键

(l_orderkey, l_linenumber)
(l_orderkey, l_linenumber)
,并通过 line_items→orders→customers 两跳外键关联到客户。这类多列主键和多跳关系都受支持——前面"链路内扇出"按客户名查明细量,明细量就正确上卷到了客户粒度。

外键约束

建模时注意两条硬约束:

  • 类型一致:外键列与被引用列的数据类型必须相同,否则创建报错。例如订单的
    o_custkey
    o_custkey
    (int)若去引用客户的
    c_name
    c_name
    (string),报
    type int ... does not match type string
    type int ... does not match type string
    。当外键列与被引用表主键列名不同时,需显式指定引用列名,如
    FOREIGN KEY (o_custkey) REFERENCES customers (c_custkey)
    FOREIGN KEY (o_custkey) REFERENCES customers (c_custkey)
  • 定义顺序:被引用的逻辑表必须在
    TABLES
    TABLES
    子句中先于引用方定义。

详见创建语义视图语义视图能力与限制参考

常见问题与对策

现象原因对策
维度值出现 NULL 行子表存在关联不上父表的孤儿行检查外键数据完整性;或在外层
WHERE
WHERE
过滤 NULL
某些维度成员缺失该成员在指标表里没有事实行(如无订单的客户)需要全集时直接查维度表,不查语义视图指标
查询报 No relationship found组合了两个无直接关系路径的分支指标(chasm trap)拆成多次查询,每次只取一条关系链上的指标
跨表指标数值被放大手写 JOIN 才会双重计算用语义视图自动按指标粒度聚合,不要手写 JOIN

相关文档

联系我们
预约咨询
微信咨询
电话咨询
邮件咨询