我做 Agent 查数据这块有一阵子了,有个场景反复出现:业务同学问「上个季度总营收多少」,Agent 秒回一个数,SQL 干干净净,没人报错。然后财务那边给的是另一个数。
两个人都没错。错的是「营收」这个词在那套仓库里从来没有被写下来过——取消的订单算不算?用四张订单表里的哪一张?这个决定,平时只活在三个建仓库的工程师脑子里。
要把这份「没写下来的知识」变成机器能读的东西,传统的答案是语义层。但 dbt、LookML、Cube、Snowflake 这四家主流工具,都要求你自己动手把它写出来。
前段时间 Show HN 上冒出来一个叫 semlayer 的项目,口号直接冲着这件事来:skip the quarter of hand-writing dbt YAML——别再用一个季度去手写语义层的 YAML 了。它的主张是,语义不该手写,应该让引擎从你自己的数据里读出来,写进一份任何 Agent 都能读的开放文件。
我把这五条路线摊在一张桌子上看了一遍。有意思的地方不在功能对比,而在于它们对同一个问题的不同回答:语义层里那份「真相」,到底该由谁产出、谁维护、谁背书。
先上总表,后面再一条条拆。
五条路线总览
这张表是我自己整理的,每一格都尽量对应到各家的真实机制,不是官网文案。
| 维度 | dbt Semantic Layer | LookML | Cube | Snowflake Semantic Views | semlayer |
|---|---|---|---|---|---|
| 谁来写 | 人手写 YAML | 人手写 view.lkml | 人手写 cube YAML/JS | 人手写 DDL | 引擎推断 + 人工 review |
| 自动发现业务规则 | 否 | 否 | 否 | 否 | 是(假设检验,scope: measures) |
| 过期 / 漂移检测 | 靠自建调度 | 靠 Git / IDE 校验 | 需自建 | MAX_STALENESS + 物化 | semlayer drift(orphaning 状态机) |
| 消费者契约 | 无 | 无 | 无 | 部分(verified queries) | 有(normative SPEC) |
| 交付方式 | 编译进 dbt 项目 | Looker 平台内 | API / 语义层服务 | schema 级对象,仓库内 | MCP(stdio),可移植 YAML |
| 绑定深度 | 项目级 | Looker 平台 | 商业授权 | 只能 Snowflake | 仓库无关(目前 3 家) |
| 成本量级 | 人力密集 | 人力密集 | 商业授权 | 平台内 | 约 $0.70 / 100 张表 |
看这张表你会发现一件事:前四列在「谁来写」这一行全是「人手写」,只有最后一列不同。这不是巧合。
举个具体点的例子。像「平均订单金额」这种指标,它在任何一张表里都不是一列——它是某个人关于「除以什么」的一个决定,而两种都说得通的算法,结果能差三倍。两份都对、都对得理直气壮,最后在同一个会上报出两个数。语义层要做的,就是把这个决定写下来:哪些行算数、分母是什么、凭什么这么定。区别只在于——这份记录,是有人坐下来手写,还是引擎从数据里读出来。
dbt Semantic Layer:把语义写进 YAML,查询时现拼 SQL
dbt 的路子是把语义直接写进项目里,和你的转换模型放在一起。每个模型挂一个 semantic_model 块,声明它的聚合时间维度、实体(也就是 join key)和维度:
models:
- name: fact_transactions
semantic_model:
enabled: true
agg_time_dimension: transaction_date # 必填
columns:
- name: transaction_id
entity:
type: primary # primary / foreign / unique / natural
name: transaction
- name: customer_id
entity:
type: foreign
name: customer
- name: transaction_date
granularity: day
dimension:
type: time
- name: order_country
dimension:
type: categorical
metrics:
- name: transaction_total
type: simple # simple / ratio / derived / cumulative...
agg: sum
expr: transaction_total
代码来自 dbt 官方文档的 semantic models 页。这里有几个设计点值得记住。
一是 agg_time_dimension 是必填,而且任何要定义 metric 的语义模型,至少得有一个带 granularity 的时间维度列。换句话说,dbt 不是让你「随便」写,它逼你先把时间轴定下来。
二是 MetricFlow(dbt 语义层的执行引擎)在查询时才动态构造 join,而不是预先物化出所有可能的分组。官方原话大意是:你不用提前建一套「把所有分组方式都算好」的系统,问什么维度,它现场拼什么 join。这个思路省事,但也意味着复杂 join 的代价摊到了每一次查询上。
三是时间聚合还依赖一个叫 time spine(时间脊柱)的东西——一张连续的日期表,所有的按时间 join 和聚合都架在它上面。第一次用 dbt 语义层的人经常在这里卡住:模型写完了,跑时间维度就报错,因为它没有时间脊柱。
一句话总结 dbt 这一路:语义是你团队的一项持续工程。写得越细,Agent 回答得越准,但这笔人力账要一直付下去。
LookML:这套东西的祖师爷,也把「写」刻进了基因
Looker 的 LookML 是这类工具的鼻祖。它的基本单位是 view 文件,里面声明维度和度量:
view: orders {
dimension: id {
primary_key: yes
type: number
sql: ${TABLE}.id ;;
}
dimension: customer_id {
sql: ${TABLE}.customer_id ;;
}
dimension_group: created {
type: time
timeframes: [date, week]
sql: ${TABLE}.created_at ;;
}
measure: count {
type: count
sql: ${id} ;;
}
measure: total_amount {
type: sum
sql: ${amount} ;;
}
}
dimension_group 是个挺聪明的设计,一次声明就能自动铺开 date / week / month 等一堆时间维度。查询侧用 explore 加 join 把多个 view 拼起来:
explore: orders {
join: customers {
sql_on: ${orders.customer_id} = ${customers.id} ;;
}
}
如果查询重、想缓存,可以定义 derived_table,写成 PDT(persistent derived table),按 persistence strategy 定期重算,落到数据库的一个 scratch schema 里。另外还有个 manifest.lkml 管项目级配置,比如从别的项目 remote_dependency 导入 LookML 文件。
LookML 的问题也在这里:它的一切都长在 Looker 平台上。模型写得再漂亮,离开 Looker 就编译不了。语义和平台是绑死的。
Cube:语义层顺手把「快」也解决了
Cube 的建模语言是 cube,可以用 YAML 或 JS 写,里面是维度、度量,再加上它最有特色的一块——pre-aggregations(预聚合):
cubes:
- name: orders
sql_table: orders
measures:
- name: count
type: count
dimensions:
- name: status
type: string
sql: status
pre_aggregations:
- name: orders_by_status
measures:
- CUBE.count
dimensions:
- CUBE.status
time_dimension: CUBE.created_at
granularity: day
预聚合本质上是物化好的 rollup 表,Cube 会做 aggregate awareness:一个查询来的时候,它自己判断能不能用现成的 rollup 回答,能就命中,不能才回源。它还有 rollup_join 把不同 cube 的维度和度量拼进同一张 rollup,rollup_lambda 支持把实时数据源和预聚合混在一起。要压规模,还有分区和索引,落进 Cube Store。
所以 Cube 和前三家的差别,不完全在「语义」上,而在它把语义层和性能层绑成了一件事。你写一份模型,顺带拿到一套缓存和物化方案。代价一样:全部手写,而且这是要商业授权的产品。
Snowflake Semantic Views:仓库亲自下场
Snowflake 的做法最不一样——它没做外部工具,而是把语义变成数据库里的一个原生对象,用 DDL 定义,和表、视图平级:
CREATE SEMANTIC VIEW finance_metrics
TABLES (
orders AS orders_tbl PRIMARY KEY (order_id)
)
RELATIONSHIPS (
orders AS customers_id REFERENCES customers (customer_id)
)
FACTS ( ... )
DIMENSIONS ( ... )
METRICS (
orders.total_revenue AS SUM(orders.amount)
)
AI_SQL_GENERATION 'Revenue excludes cancelled orders.'
AI_VERIFIED_QUERIES (
quarterly_revenue AS (
QUESTION 'What was Q3 revenue?'
SQL 'SELECT ...'
)
);
语法来自 Snowflake 的 CREATE SEMANTIC VIEW 文档。这一路有几个细节很能说明它的思路:
- 关系里支持 ASOF join 和 range join(
BETWEEN start_column AND end_column EXCLUSIVE),也就是把「按时间就近匹配」「按区间匹配」这类容易写错的 join 变成了声明式的东西。 - 指标支持 NON ADDITIVE BY 和窗口函数指标,专门处理那种「不能直接求和」的度量。
- 有
WITH SYNONYMS、AI_SQL_GENERATION自定义指令,还有 AI_VERIFIED_QUERIES——人工背书的「金标 SQL」,相当于把资深分析师验过的答案固化成参考。 - 有
MAX_STALENESS和物化,管新鲜度。 - 从旧的 Cortex Analyst YAML 迁移,走
SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML。
语义视图的标准 SQL 查询在 2026 年 3 月已经 GA,等于把「用 SQL 语义化地查数据」这件事搬进了仓库本身。但代价也直接写在设计里:它只能看 Snowflake 自己仓库里的数据。你栈里那另外二三十个系统,它管不到。
semlayer:干脆不写
前面四家,无论包装成什么样,本质都是「你来写」。semlayer 想换个前提:别写,让引擎读出来。
它的流水线分四段,每段都带 confidence 和 provenance:先 profile 每一列(每张表两次宽 SELECT,置信度低于 0.7 才升级到 LLM),再 link 出没人声明的外键(关键约束是「统计永不单独自动纳入」,必须命名信号 + LLM 双证),然后 describe 做两遍上下文传播生成列描述,最后 enrich 用聚合对账——假设检验——从汇总表里反推出业务规则。它自己宣称在 TPC-DS 风格的测试上,104 个未声明外键全部找对,F1 = 1.0,而且 0 个假陷阱被误纳。
它给出的 benchmark 数字是这样的(messy_mart,38 个业务问题,Claude Haiku 档):
| 配置 | 答对率 |
|---|---|
| 裸 schema | 0.42 |
| 语义层 | 0.87 |
| 语义层 + lint | 0.89 |
它修的是静默错误:裸 schema 下「总营收」悄悄把取消订单算进去,报出 $16.3M,而财务那边是 $14.6M;fan-out join 会把总额悄悄翻三倍,只有 linter 看得见。
这里有个很多人没注意的细节——semlayer 同一份报告里,还藏着一个负面结果:在干净、命名规范的 TPC-DS 仓库上,裸 DDL 是 0.67,加了语义层反而降到 0.58。它把这个对自己不利的数字和好数字并排放在仓库里,还写了句「如果你的仓库很整齐,你不需要这个」。
它最有区分度的一块,是 spec/SPEC.md 里的消费者契约。契约规定:置信度低于 0.6 的推断禁止用来回答;deprecated 的表禁新查询;fanout_risk 为真的聚合必须套策略或者直接拒绝,静默求和算 non-conforming;SCD2 必须走 asof 时间窗。它自己的 linter(semlayer lint / MCP 的 check_sql)校验的是语义而不是语法——「跑得通的查询照样可能在求和已取消的订单」。
交付方式是 MCP,把 layer.yaml 通过 stdio 喂给任意 Agent;工具面铺开有 semantic_search、get_domains、get_tables、table_detail、get_metrics、route_intent、check_sql、compile_metric 这一批,连上时还会下发一组 standing instructions。这里有个我觉得比「推断」本身更值得抄的设计——compile, don’t execute:这个 MCP server 不碰你的仓库,没有凭证、不开连接、不读一行数据,它只吐 SQL 文本,执行交给仓库自己那一侧的 MCP server。这么一分,语义层就退成了一个纯粹的「编译层」,风险面小了一大截。另外还有个 --no-llm 全确定性模式,零 API 调用,照样能用。成本大约 $0.70 / 100 张表。
当然,局限也得如实说:beta,star 个位数,只支持 Snowflake / BigQuery / DuckDB(Iceberg 要走 DuckDB 桥),LLM 只接 Anthropic,没有托管服务,校准数据还等第三方复现。
把五家摊在一起,我真实的判断
看功能清单没意思,我想说的是这几个更别扭的点。
第一,前四家把「写」当成用户的责任,这不是设计选择,是商业模式。 你手写出来的 YAML、LookML、语义视图 DDL,是一份粘性资产——只有它家能编译、能跑。写得越细,你越走不了。「自推断」在方向上是反粘性的:语义一旦是一份可搬家的开放文件,平台就少了拿捏你的把手。
第二,大厂两周能不能抄出「推断」?功能上能,动机上未必。 这点很多人会误判。Snowflake 其实已经把 AI 塞进语义视图了——AI_SQL_GENERATION、AI_VERIFIED_QUERIES、从 YAML 迁移都有。但你仔细看,它做的是「让 AI 用你写好的语义去回答问题」,不是「推断出语义」。两个方向之间隔着的,是整个成本模型:前者让你更离不开它写的那份语义,后者让你有一天能带着语义走人。
第三,真正的问题不是「谁写」,是「谁保证它不过期」。 四家的手写层都有一个共同的死穴——ALTER TABLE 那天它就过期了。semlayer 的 drift 命令是冲这个来的。但我想说句可能不讨喜的:漂移检测本身不难,难的是「谁有权改那份 ground truth」。这是治理问题,不是算法问题。一份推断出来的层,第一次跟你的业务规则打架时,谁签字、谁负责、审计怎么留痕,这些没解决之前,「自推断」在生产环境里就还是个 demo。
第四,别急着喊颠覆。 前面那个负面结果——干净仓库上裸 DDL 0.67 > 语义层 0.58——说明这事儿的市场是「存量脏数据」,不是「新建数仓」。如果你的仓库本来就整齐,这整套东西对你的增量很有限。semlayer 自己承认了这一点,我认为这比它那些漂亮数字更能说明它真懂自己的边界。
第五,短期赢家大概率还是 dbt 和 Snowflake,因为它们卖的是分发,不是算法。 dbt 已经把语义层做成了「写了转换逻辑就顺手写一点语义」的自然延伸;Snowflake 直接把它做进仓库,用户的迁移成本接近零。semlayer 最可能的结局也不是被打死,而是——「推断」变成一个开关,被它们抄走。对你来说这不是坏事,对 semlayer 是不是,取决于它能不能在被抄之前先长出自己的分发。
第六,大厂抄不走的那一样东西,是可移植。 dbt 的语义活在 dbt 项目里,Snowflake 的活在它自己的仓库里,semlayer 的活在一份能搬家的 YAML 里。对平台来说,可移植是负资产——它要的恰恰是绑定。所以这既是 semlayer 唯一的护城河,也是最薄的一条。
真要选,我会怎么选
不谈谁强谁弱,只说场景。
- 已经全套在 Snowflake 上:直接用 Snowflake Semantic Views。语义就在仓里,
AI_VERIFIED_QUERIES那套人工背书的金标对你这种「要求口径一致」的场景特别顶用。 - 已经在用 dbt:继续用 dbt 语义层。语义跟着转换逻辑走,链路最短,团队不用学第二套东西。
- 要嵌进产品里做海量查询、性能是硬指标:Cube。预聚合那套东西就是为这个生的。
- 老 Looker 用户:留着。迁移的性价比不高。
- 仓库很脏、又实在没人手写、主要目的是给 Agent 喂上下文:可以上 semlayer,但请把它当成「生成初稿 + 人工签字」,而不是「一把梭」。它的 review 队列和 confidence 分级就是为这个设计的。
如果你已经看过我写的那篇 semlayer 源码级拆解(《AI 写的 SQL 没报错,58% 的答案却是错的:语义层正在成为 Agent 查数据的命门!》),那篇讲的是它内部怎么工作,这篇是把它放回它所在的这张桌子旁边。两篇一起看,你会更清楚它想掀的到底是哪张桌子。类似的横向拆解我还在决策模型那篇里做过一次(《决策模型 + LLM 混合架构:Jev、OpenJev、AnyJev、Laya 四大 System One 方案横评与融合设计》),方法是一样的:把方案摆平,看它们对同一个问题的分歧在哪。
写在最后
语义层的护城河,从来不在「谁来写」上。
手写派把那份 ground truth 的权力留在自己平台里,自推断派想把它变成一份能搬家的文件。这两条路谁赢,不是推断算法决定的,是分发决定的——谁能把「定义正确性」这件事塞进用户已有的工作流,谁就赢。Snowflake 把语义塞进仓库,dbt 把它塞进 CI,都是这个逻辑。
所以我对 semlayer 的真实看法是:它做对了方向,但它赌的不是技术,是「平台愿不愿意放弃绑定」。这个赌注,历史上赢过,也输过很多次。
对手写语义层的团队来说,真正该问的问题也不是「要不要换成自推断」,而是:当有人跑了 ALTER TABLE 之后,今天是谁、在多久之后、靠什么机制发现语义已经过期了? 这个问题的答案,比选哪家工具重要得多。