Apache Ossie 表达式语言指南:聚合函数与可分解性(Distributive / Algebraic / Holistic)一次讲透
【免费下载链接】ossieApache Ossie, industry wide specification effort to standardize how we exchange semantic metadata across analytics, AI and BI platforms, providing a vendor neutral, single source of truth for semantic data项目地址: https://gitcode.com/GitHub_Trending/osi1/ossie
Apache Ossie 是 Apache 旗下的开源规范项目,目标是标准化分析、AI 与 BI 平台之间交换语义元数据的方式,为语义数据提供厂商中立的"单一事实来源"。本文聚焦 Ossie 表达式语言中的两块硬骨头:聚合函数怎么写,以及"可分解性(Decomposability)"为什么决定了指标能否被预聚合和分布式优化——这是写指标之前最该先建立的概念。
Ossie 表达式语言在哪一层运行?三层语义模型里的逻辑层速览
先给结论:表达式语言只服务于逻辑层(Logical Layer),也就是指标、字段、过滤器所在的那一层。
Ossie 把语义模型分成三层,可以把它当成一张地图来定位:
| 层 | 职责 | 表达式语言现状 |
|---|---|---|
| 本体层(Ontological Layer) | 面向更抽象的业务概念建模,接近 OWL 一类建模语言 | 语言尚在另行定义中 |
| 逻辑层(Logical Layer) | 对应传统 BI 语义模型:metrics、fields、filters | ✅ 本文主角,Ossie SQL 表达式语言 |
| 物理层(Physical Layer) | 各数据库的原生 SQL | 由各引擎自行处理 |
逻辑层的表达式语言有四个设计原则:可移植(核心函数在所有合规实现中行为一致)、熟悉(基于 ANSI SQL:2003 核心语法)、分析聚焦(优先覆盖 BI 高频函数)、可扩展(厂商可通过方言扩展核心能力)。
还有一个容易被忽略的边界:这套语言用于声明模型(declaring),而不是查询接口。表达式里允许出现列/指标引用、CASE WHEN、算术与比较运算符、聚合函数、窗口函数等;但SELECT/FROM/JOIN由语义层接管,GROUP BY由粒度规格控制,WHERE用模型的 filter 属性替代。换句话说,你写的是"这个指标是什么",而不是"帮我跑一条查询"。完整定义见 core-spec/expression_language.md。
一张能力地图看懂 Ossie 聚合函数:从 SUM 到 APPROX_PERCENTILE
这一节能拿到的是一张"函数能力地图":哪些是必选(REQUIRED)、哪些是推荐(RECOMMENDED)、各自什么语义、属于哪一类可分解性——一张表看完,后面再按需查手册。
| 类别 | 函数 | 语法 | 含义 | 可分解性 | 要求级别 |
|---|---|---|---|---|---|
| 核心聚合 | SUM | SUM(expr) | 求和 | Distributive | REQUIRED |
| 核心聚合 | COUNT | COUNT(expr) | 非空值计数 | Distributive | REQUIRED |
| 核心聚合 | COUNT(*) | COUNT(*) | 总行数 | Distributive | REQUIRED |
| 核心聚合 | COUNT(DISTINCT expr) | COUNT(DISTINCT expr) | 去重计数 | Holistic | REQUIRED |
| 核心聚合 | AVG | AVG(expr) | 算术平均值 | Algebraic | REQUIRED |
| 核心聚合 | MIN/MAX | MIN(expr)/MAX(expr) | 最小 / 最大值 | Distributive | REQUIRED |
| 统计聚合 | STDDEV | STDDEV(expr) | 样本标准差 | Algebraic | REQUIRED |
| 统计聚合 | STDDEV_POP/STDDEV_SAMP | STDDEV_POP(expr)等 | 总体 / 样本标准差 | Algebraic | REQUIRED |
| 统计聚合 | VARIANCE | VARIANCE(expr) | 样本方差 | Algebraic | REQUIRED |
| 统计聚合 | VAR_POP/VAR_SAMP | VAR_POP(expr)等 | 总体 / 样本方差 | Algebraic | REQUIRED |
| 分位数 | MEDIAN | MEDIAN(expr) | 中位数(第 50 百分位) | Holistic | REQUIRED |
| 分位数 | PERCENTILE_CONT | PERCENTILE_CONT(p) WITHIN GROUP (ORDER BY expr) | 连续分位数(插值) | Holistic | REQUIRED |
| 分位数 | PERCENTILE_DISC | PERCENTILE_DISC(p) WITHIN GROUP (ORDER BY expr) | 离散分位数(实际值) | Holistic | REQUIRED |
| 近似聚合 | APPROX_COUNT_DISTINCT | APPROX_COUNT_DISTINCT(expr) | 概率草图近似去重计数,典型误差约 2% | Sketch-based | RECOMMENDED |
| 近似聚合 | APPROX_PERCENTILE | APPROX_PERCENTILE(expr, p) | 近似分位数(如 t-digest),典型误差约 1% | Sketch-based | RECOMMENDED |
其中p是 0 到 1 之间的数,0.75即第 75 百分位。
除了这张主表,还有两个必选支持的增强用法值得单独留意:
-- DISTINCT 修饰:SUM、COUNT 都支持 SUM(DISTINCT amount) COUNT(DISTINCT customer_id) -- 条件聚合:所有聚合都应支持基于 CASE WHEN 的过滤式聚合 SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) COUNT(CASE WHEN status = 'completed' THEN 1 END)条件聚合意味着"先过滤再聚合"不必依赖WHERE,这在逻辑层里正好契合了"表达式只声明、过滤交给 filter 属性"的设计。
可分解性怎么决定查询优化路径?Distributive、Algebraic、Holistic 与 Sketch-based 四类详解
这一节回答一个具体的工程问题:一个指标能不能"先分片算、再合并结果"?这直接决定了它能否走多阶段聚合、预聚合和分布式执行路径,是语义引擎优化空间的来源。
| 类别 | 包含函数 | 分片合并行为 | 引擎视角的优化空间 |
|---|---|---|---|
| Distributive(分布式) | SUM、COUNT、MIN、MAX | 各分片结果按同形运算直接合并:和相加、极值取更大/更小 | 最省——预聚合表可以完全替代明细扫描 |
| Algebraic(代数式) | AVG、STDDEV、VARIANCE | 分片结果不能直接平均,但保留少量中间量(如总和 + 行数)后即可公式换算合并 | 中等——预聚合存中间量而非最终值,合并时做一次代数运算 |
| Holistic(整体式) | MEDIAN、PERCENTILE_CONT/DISC、COUNT(DISTINCT expr) | 分片结果无法合并,必须回到全量数据 | 小——常规路径只能全量计算 |
| Sketch-based(草图式) | APPROX_COUNT_DISTINCT、APPROX_PERCENTILE | 用可高效合并的概率草图(HyperLogLog、t-digest 等)近似代替前两类 | 大——牺牲约 1%~2% 精度换来分片可合并 |
💡 用三个场景把差异讲透:
- SUM(Distributive):把全国销量拆成华东、华北两片各自求和,两片之和就是全国总和,零误差、可无限分片。
- AVG(Algebraic):两片各自的平均值不能直接再平均——两片行数不同,直接平均会产生偏差。正确做法是每片保留"总和 + 行数",合并时做一次加权换算。
- MEDIAN(Holistic):两片各自的中位数拼不出整体中位数(左半区间可能全是小值、右半全是大值,信息已经丢失),必须回到明细数据。
这就是为什么规范给每个函数都标注了可分解性:它不是分类学兴趣,而是写给执行引擎的"优化说明书"——Distributive 走"预聚合 + 合并",Algebraic 走"中间量 + 公式",Holistic 要么全量扫描、要么降级到 Sketch-based 近似。规范中提到的多阶段聚合扩展(Analytical Context Extension)正是建立在这套分类之上。
在语义模型里写出聚合指标:YAML 写法、多方言 dialects 与 NULLIF 防御性习惯
结论先行:聚合函数主要落在模型的metrics(指标)上,指标定义在模型层级、可以跨多个数据集引用。下面以仓库自带的 TPC-DS 示例 examples/tpcds_semantic_model.yaml 为例,从一个真实指标讲起。
先算一个"人效"指标。示例中的store_productivity(每员工销售额):
- name: store_productivity expression: dialects: - dialect: ANSI_SQL expression: SUM(store_sales.ss_ext_sales_price) / NULLIF(SUM(store.s_number_employees), 0) description: Sales per employee across stores datatype: Decimal ai_context: synonyms: - "sales per employee" - "employee productivity"这一行表达式里藏着两个值得学的写法:
- 防御性除法:
NULLIF(x, 0)在分母为 0 时返回 NULL 而不是报错。凡是写"总量 ÷ 总量"的比率指标,都应该给它加上——分母来自另一个聚合时,"分母恰好为 0"是迟早会发生的事。 - 可分解性在组合指标上会变化:分子分母都是
SUM(Distributive),各自都能预聚合;但"两个可分解量相除"整体不再可分解——你不能把各分片的"人效"平均起来得到总体人效。正确的执行路径是:各分片分别合并分子、分母,最后再除一次。
再看多方言机制。指标的表达式挂在expression.dialects数组下,同一个指标可以为不同引擎并列声明不同方言版本:
expression: dialects: - dialect: ANSI_SQL expression: DATE_TRUNC('month', order_date) - dialect: BIGQUERY expression: DATE_TRUNC(order_date, MONTH)比如DATE_TRUNC在 ANSI 与 BigQuery 里参数顺序相反,BigQuery 的分位数则对应APPROX_QUANTILES(amount, 100)[OFFSET(50)]这类数组取元素写法。方言机制把这些引擎差异收进声明本身,核心函数的语义保持一致——这正是 Ossie"厂商中立单一事实来源"的具体落点。另外注意ai_context.synonyms:给指标配上同义词,AI 检索和自然语言查询才能稳定命中它。
完整函数清单与合规级别(REQUIRED / RECOMMENDED / 方言扩展)见 core-spec/expression_language.md;指标结构的 schema 定义(
ai_context等字段)见 core-spec/spec.md。
从 Tableau / DAX 迁移过来:聚合函数对照速查表
这一节给一张精简速查表,覆盖日常最高频的聚合函数;完整对照(日期、字符串、窗口函数)在 core-spec/expression_language.md 的 Cross-Reference 章节。
| Ossie 标准 | Tableau | Looker Studio | DAX |
|---|---|---|---|
SUM(x) | SUM(x) | SUM(X) | SUM(x) |
COUNT(DISTINCT x) | COUNTD(x) | COUNT_DISTINCT(X) | DISTINCTCOUNTNOBLANK(x) |
AVG(x) | AVG(x) | AVG(X) | AVERAGE(x) |
STDDEV(x) | STDEV(x) | STDDEV(X) | STDEV.S(x) |
VARIANCE(x) | VAR(x) | VARIANCE(X) | VAR.S(x) |
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY x) | PERCENTILE(x, 0.75) | PERCENTILE(X, 75) | PERCENTILE.INC(x, 0.75) |
⚠️ 迁移时留意 DAX 与 SQL 的空值语义差异,规范里专门给出了两条提醒:
- DAX 的
COUNT(x)对应 SQL 语义应选COUNTA(都统计非空值,但 DAXCOUNT不支持布尔列); - DAX
DISTINCTCOUNT会把 BLANK 当作一个取值计入,SQLCOUNT(DISTINCT x)则排除 NULL——可空列上会差 1,建议改用DISTINCTCOUNTNOBLANK对齐。
带走这三件事:要点回顾与从 examples / validation 开始的上手路径
最后收拢为三条可以记住的结论:
- 每个聚合函数都带一个可分解性标签,它决定执行策略。Distributive 分片合并零代价;Algebraic 需要中间量做公式换算;Holistic 必须全量计算或改用 Sketch-based 近似(约 1%~2% 误差)。理解这个分类,才能理解语义引擎的优化空间,也才能判断"这个指标能不能建预聚合表"。
- 核心函数必选、方言吸收差异。
dialects数组让同一模型在 ANSI、Snowflake、BigQuery 等不同引擎上都正确运行;写比率指标时记得NULLIF防除零。 - 规范只声明、不查询。表达式语言描述"模型是什么",
GROUP BY/WHERE等由语义层接管,不要把查询 SQL 写进指标定义。
上手路径建议:
- 克隆仓库开始动手:
git clone https://gitcode.com/GitHub_Trending/osi1/ossie - 通读 examples/tpcds_semantic_model.yaml,它是覆盖聚合、窗口函数指标与多方言声明的完整样本;
- 对照 core-spec/expression_language.md 确认自己用到的函数是否属于 REQUIRED 集合;
- 用 validation/ 目录下的校验工具验证自己的语义模型;
- 若想扩展阅读语义结构,见 core-spec/spec.md 与 docs/。
【免费下载链接】ossieApache Ossie, industry wide specification effort to standardize how we exchange semantic metadata across analytics, AI and BI platforms, providing a vendor neutral, single source of truth for semantic data项目地址: https://gitcode.com/GitHub_Trending/osi1/ossie
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考