语义视图关系建模与聚合粒度
语义视图通过外键关系自动处理表连接和聚合。关系怎么建,直接决定查询结果对不对 ——同样的数据,关系定义不同,"订单数""客户数"可能差几倍。本文用一组带边界数据的例子,说明外键关系如何影响 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, 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_namecustomer_name
为 NULLNULL
。说明订单不会因为关联不上客户而被丢弃。
无订单的客户不出现 :Dave 没有任何订单,结果里没有他。客户属性是挂在订单事实上的,没有订单事实就没有对应行。
记住这个心智模型:查询的粒度由指标所在的表决定,维度表的属性沿外键关系挂上去 。如果你需要"列出所有客户(含无订单的)",应直接查客户表,而不是查语义视图的订单指标。
链路内扇出:一对多的正确聚合
当查询涉及一条关系链上的两级(customers→orders→line_items),引擎会在各自的粒度上计算指标,不会因为 JOIN 放大:
SELECT * FROM semantic_view(
doc_rel_analysis
DIMENSIONS customers.customer_name
METRICS orders.order_count, line_items.qty_total
) ORDER BY customer_name;
+---------------+-------------+-----------+
| customer_name | order_count | qty_total |
+---------------+-------------+-----------+
| NULL | 1 | NULL |
| Alice | 2 | 8 |
| Bob | 1 | 7 |
| Carol | 1 | 2 |
+---------------+-------------+-----------+
Alice 有 2 笔订单(101、102),订单 101 含 2 行明细(数量 5、3),订单 102 无明细。
order_countorder_count
是 2 而不是被明细行数放大成 3,
qty_totalqty_total
正确累加为 8。这正是语义层相对手写 JOIN 的价值:手写
orders JOIN line_itemsorders JOIN line_items
后对订单做
COUNTCOUNT
会重复计数,语义视图自动按指标的原始粒度聚合。
孤儿订单 105(客户号 99 不存在)仍以
customer_name = NULLcustomer_name = NULL
出现:它有 1 笔订单,但没有任何明细行,所以
qty_totalqty_total
为
NULLNULL
——
子表指标对没有子行的父行返回 NULLNULL
,不是 0 。需要 0 时在外层用
COALESCE(qty_total, 0)COALESCE(qty_total, 0)
。
多分支扇出(chasm trap)的自动处理
orders 和 addresses 是 customers 下两条独立的一对多分支。经典的 chasm trap(扇出陷阱)问题是:如果天真地把两个分支通过 customers 连成一张宽表,Alice 的 2 笔订单会和 2 个地址交叉成 4 行,订单数和地址数被同时放大。语义视图引擎会在各自粒度上分别聚合再对齐维度 ,把两个分支的指标放进同一次查询也能得到正确结果,不会放大:
SELECT * FROM semantic_view(
doc_rel_analysis
DIMENSIONS customers.customer_name
METRICS orders.order_count, addresses.addr_count
) ORDER BY customer_name;
+---------------+-------------+------------+
| customer_name | order_count | addr_count |
+---------------+-------------+------------+
| NULL | 1 | NULL |
| Alice | 2 | 2 |
| Bob | 1 | 1 |
| Carol | 1 | 3 |
+---------------+-------------+------------+
Alice 的
order_countorder_count
是 2、
addr_countaddr_count
是 2,都没有被交叉放大成 4——引擎分别在订单粒度和地址粒度算好各自的聚合,再按
customer_namecustomer_name
对齐。孤儿订单 105 以
NULLNULL
出现,它没有对应地址,
addr_countaddr_count
为
NULLNULL
。
多分支能力对更深的组合同样成立。三条分支的指标一起取,也各自正确聚合:
SELECT * FROM semantic_view(
doc_rel_analysis
DIMENSIONS customers.customer_name
METRICS orders.order_count, line_items.qty_total, addresses.addr_count
) ORDER BY customer_name;
+---------------+-------------+-----------+------------+
| customer_name | order_count | qty_total | addr_count |
+---------------+-------------+-----------+------------+
| NULL | 1 | NULL | NULL |
| Alice | 2 | 8 | 2 |
| Bob | 1 | 7 | 1 |
| Carol | 1 | 2 | 3 |
+---------------+-------------+-----------+------------+
按非唯一维度分组时,跨分支聚合也在分组粒度上分别汇总。按城市分组(Alice、Carol 同在 New York):
SELECT * FROM semantic_view(
doc_rel_analysis
DIMENSIONS customers.customer_city
METRICS orders.order_count, addresses.addr_count
) ORDER BY customer_city;
+---------------+-------------+------------+
| customer_city | order_count | addr_count |
+---------------+-------------+------------+
| NULL | 1 | NULL |
| Boston | 1 | 1 |
| New York | 3 | 5 |
+---------------+-------------+------------+
New York 的
order_countorder_count
是 3(Alice 2 + Carol 1)、
addr_countaddr_count
是 5(Alice 2 + Carol 3),两个分支各自在城市粒度汇总后对齐,互不干扰。
💡 提示 :早期版本遇到跨分支组合会直接报
No relationship found for tableNo relationship found for table
,要求拆成多次查询。当前版本已能自动处理扇出,可以放心把多个分支的指标写在同一次查询里。仍建议查询后核对行数与量级是否符合预期。
多列主键与多跳外键
line_items 用复合主键
(l_orderkey, l_linenumber)(l_orderkey, l_linenumber)
,并通过 line_items→orders→customers 两跳外键关联到客户。这类多列主键和多跳关系都受支持——前面"链路内扇出"按客户名查明细量,明细量就正确上卷到了客户粒度。
聚合粒度规则:上卷合法,下钻非法
外键把逻辑表串成一条粒度阶梯:父表粗、子表细 (customers → orders → line_items,客户比订单粗、订单比明细粗)。指标能按哪些维度分组,由这条阶梯决定:
上卷(roll-up)合法 :定义在细粒度表的指标,可以按任意更粗的父层级分组,引擎自动沿外键 JOIN 汇总。明细指标 qty_totalqty_total
(定义在最细的 line_items)既能按 customer 分组,也能上卷到更粗的 city:
SELECT * FROM semantic_view(
doc_rel_analysis
DIMENSIONS customers.customer_city
METRICS line_items.qty_total
) ORDER BY customer_city;
+---------------+-----------+
| customer_city | qty_total |
+---------------+-----------+
| Boston | 7 |
| New York | 10 |
+---------------+-----------+
New York 汇总了 Alice(8)和 Carol(2)的明细量得 10,Boston 是 Bob 的 7。同一个
qty_totalqty_total
在 customer、city 层级返回不同聚合,全局合计恒为 17。
下钻(drill-down)非法 :父表指标不能直接聚合子表的列。给 customers 表定义 SUM(orders.o_totalprice)SUM(orders.o_totalprice)
这样的指标,创建时就报错:
CZLH-42000: Semantic analysis exception - cannot resolve column 'o_totalprice'
父表要基于子表列做聚合,必须先用
FACTSFACTS
把子表列恒等透传上来,指标再引用这个事实(见
创建语义视图 的"带 FACTS 的语义视图")。透传后还能在父粒度做双层聚合,如
AVG(SUM(子表列))AVG(SUM(子表列))
求每个父行的子表均值。
无子行的父行 :有父行但没有对应子行时(如孤儿订单没有明细),子表指标返回 NULLNULL
而非 0;需要 0 时外层用 COALESCECOALESCE
。前面"链路内扇出"里孤儿订单的 qty_total = NULLqty_total = NULL
就是这条规则。
外键约束
建模时注意两条硬约束:
类型一致 :外键列与被引用列的数据类型必须相同,否则创建报错。例如订单的 o_custkeyo_custkey
(int)若去引用客户的 c_namec_name
(string),报 type int ... does not match type stringtype int ... does not match type string
。当外键列与被引用表主键列名不同时,需显式指定引用列名,如 FOREIGN KEY (o_custkey) REFERENCES customers (c_custkey)FOREIGN KEY (o_custkey) REFERENCES customers (c_custkey)
。
定义顺序 :被引用的逻辑表必须在 TABLESTABLES
子句中先于引用方定义。
详见创建语义视图 和语义视图能力与限制参考 。
常见问题与对策
现象 原因 对策 维度值出现 NULL 行 子表存在关联不上父表的孤儿行 检查外键数据完整性;或在外层 WHEREWHERE
过滤 NULL 某些维度成员缺失 该成员在指标表里没有事实行(如无订单的客户) 需要全集时直接查维度表,不查语义视图指标 子表指标为 NULL(不是 0) 父行没有对应子行(如孤儿订单没有明细) 需要 0 时外层用 COALESCE(指标, 0)COALESCE(指标, 0)
父表指标报 cannot resolve column 父表指标直接聚合了子表的列 先用 FACTSFACTS
把子表列透传到父表,指标再引用透传的事实 跨表指标数值被放大 手写 JOIN 才会双重计算 用语义视图自动按指标粒度聚合,不要手写 JOIN
相关文档