Featured image of post 手写语义层的时代开始松动了:dbt、LookML、Cube、Snowflake 四条老路,和一条想自己「读」出语义的新路!

手写语义层的时代开始松动了:dbt、LookML、Cube、Snowflake 四条老路,和一条想自己「读」出语义的新路!

我做 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 之后,今天是谁、在多久之后、靠什么机制发现语义已经过期了? 这个问题的答案,比选哪家工具重要得多。

By AI博士 万戈