Hacker News 中文摘要

RSS订阅

语义层为何重要(以及如何用DuckDB构建) -- Why Semantic Layers Matter (and how to build one with DuckDB)

文章摘要

语义层在数据管理中至关重要,它通过统一定义业务指标(如收入KPI)避免重复实现,提升效率。本文通过YAML文件和Python脚本,结合DuckDB和Ibis工具,构建了一个简单的语义层,并演示了如何查询2000万条纽约出租车记录,帮助读者理解语义层的实际应用场景及其价值。

文章总结

为什么语义层重要——以及如何使用DuckDB构建一个语义层

许多人在问:“为什么要使用语义层?它到底是什么?”在这篇实践指南中,我们将使用一个简单的YAML文件和Python脚本来构建一个最基本的语义层,目的不是为了构建语义层本身,而是为了理解语义层的价值。我们将使用DuckDB和Ibis查询2000万条纽约出租车记录,并执行一致的业务指标。通过本文,你将清楚地知道语义层在何时能解决实际问题,何时又是多余的。

语义层是一个我热衷的话题,因为我在商业智能(BI)工具中使用语义层已有二十多年,而直到最近,我们才拥有了可以独立于BI工具之外的完整语义层,它们结合了逻辑层的优势,并能够在Web应用、笔记本和BI工具之间共享。通过语义层,你的收入KPI或其他复杂的公司指标只需在单一的真实来源中定义一次,无需反复重新实现。

我们将通过一个简单的YAML文件(用于定义语义)和Python脚本来构建一个最基本的语义层,并使用Ibis和DuckDB执行它。在深入代码示例之前,我们先快速回顾一下语义层的概念。

何时不需要语义层?

首先,我们来看看在什么情况下不需要语义层,以及它为何不是最佳选择。最简单直接的原因包括:

  • 你刚刚开始进行数据分析,且只有一个数据消费者,意味着你只有一种展示分析数据的方式,例如BI工具、笔记本或Web应用,而不是多种展示方式。这意味着你不需要在不同的地方应用计算逻辑。
  • 你没有复杂的业务逻辑需要临时查询,只有简单的计数、求和或平均值。
  • 你通过SQL转换将所有指标预处理为物理表,下游分析工具获取的所有指标都已预处理和聚合,过滤速度足够快。

为什么使用语义层?

那么,我们何时真正需要语义层?它到底是什么?关于语义层的历史、兴起、与MVC方法的比较以及其功能,已有大量信息。因此,本文将重点放在“为什么”使用语义层,并通过实际示例展示如何使用它。

使用语义层的主要原因可能包括以下一个或多个需求:

  1. 统一的地方:在一个地方定义临时查询,版本控制并协作,能够将其拉入不同的BI工具、Web应用、笔记本或AI/MCP集成。避免在每个工具中重复定义指标,使可维护性和数据治理更加容易,从而形成一个一致的业务层,封装业务逻辑。

  2. 缓存:对于基于各种源数据库的临时查询,定义能够实现预计算的指标,可以在任何下游分析工具中实现亚秒级查询响应,相比实现自定义数据库连接和不同数据库,能够消除潜在的数据移动成本,通过查询数据所在的位置,使用跨异构源的方言优化SQL下推,减少基础设施开销和云计算成本。

  3. 统一的访问级别安全:通过各种API(REST、GraphQL、SQL、ODBC/JDBC、MDX/Excel)实现统一的访问控制。统一的Analytics API允许用户将Excel连接到清理过的、快速的、统一的API,实现自助BI。

  4. 动态查询重写:自动将简单的、业务友好的查询转换为跨多个数据库的复杂、优化的SQL。这使得用户可以使用业务概念(如“averageordervalue”)编写直观的查询,而无需了解底层数据模型的复杂性、表关系或数据库特定的语法。语义层将复杂的分析(如不同粒度的比率、时间范围(YoY、滚动周期)和自定义日历)抽象为简单的语义查询。

  5. 为LLM提供上下文:语义层可以提供业务上下文,显著提高自然语言查询的准确性,防止AI频繁产生幻觉,因为大部分业务逻辑甚至数据模型都配置和定义在语义层中,帮助LLM更好地理解业务。

语义层的工作原理:一个实际示例

现在,我们通过分析一个最实用的语义层来看它是如何工作的。最简单的语义层是由Julien Hurault最近发布的Boring Semantic Layer (BSL)项目。我们使用DuckDB作为查询引擎,Python和Ibis作为执行层。

我们将构建一个类似于下图所示的语义层——其中YAML定义作为我们的指标,如计算度量和维度,Ibis用于查询翻译以运行任何执行引擎;这里我们使用DuckDB。

如何开始

首先,我们创建一个虚拟环境,安装依赖项和语义层:

bash git clone git@github.com:sspaeti/semantic-layer-duckdb.git uv sync #installs dependencies

这将安装语义层、Ibis和其他依赖项。

现在,我们准备定义我们的指标。为了简化示例并专注于指标而不是数据,我使用了NYC Taxi Dataset,这是一个我们熟悉的数据集。它有一个用于接客的查找表和大量我们可以使用的数据,并且可以通过HTTPS获取。

定义指标

接下来,我们可以开始定义我们的指标。首先,我们设置时间戳及其粒度(BSL要求),然后是维度,如下所示:

```yaml fhvhvtrips: table: tripstbl timedimension: pickupdatetime smallesttimegrain: TIMEGRAINSECOND

dimensions: hvfhslicensenum: .hvfhslicensenum dispatchingbasenum: _.dispatchingbasenum originatingbasenum: _.originatingbasenum requestdatetime: .requestdatetime pickupdatetime: _.pickupdatetime dropoffdatetime: _.dropoffdatetime tripmiles: _.tripmiles triptime: _.triptime basepassengerfare: .basepassengerfare tolls: _.tolls bcf: _.bcf salestax: .salestax congestionsurcharge: _.congestionsurcharge airportfee: _.airportfee tips: .tips driverpay: .driverpay sharedrequestflag: .sharedrequestflag sharedmatchflag: _.sharedmatchflag accessarideflag: .accessarideflag wavrequestflag: .wavrequestflag wavmatchflag: _.wavmatch_flag ```

pickup_datetime是时间列,粒度设置为秒,所有其他列都视为维度。

有趣的部分是当我们设置度量时,这些度量可以变得非常复杂,并且可能依赖于许多现有度量的层次。这是我们定义度量的方式:

yaml measures: trip_count: _.count() avg_trip_miles: _.trip_miles.mean() avg_trip_time: _.trip_time.mean() avg_base_fare: _.base_passenger_fare.mean() total_revenue: _.base_passenger_fare.sum() avg_tips: _.tips.mean() avg_driver_pay: _.driver_pay.mean()

还有一些只聚合标记数据的度量,例如共享行程或轮椅请求:

yaml shared_trip_rate: (_.shared_match_flag == 'Y').mean() wheelchair_request_rate: (_.wav_request_flag == 'Y').mean()

为了创建一个功能性的仪表板并深入分析不同的角度,我们需要维度,这些维度在查询数据时提供更多上下文。例如,如果我们想在纽约市按行政区进行聚合,这些信息不在行程数据中,而是在我们的查找表中,正如我们在上面的DESCRIBE中看到的那样。现在,我们加入这个表并使用这些信息。

首先,我们在YAML中定义额外的数据集如下:

```yaml taxizones: table: taxizonestbl primarykey: LocationID

dimensions: locationid: _.LocationID borough: _.Borough zone: _.Zone servicezone: .servicezone

measures: zone_count: _.count() ```

最后,我们需要连接这两个数据集。这可以像这样指定——添加到fhvhv_trips数据集中:

yaml joins: pickup_zone: model: taxi_zones type: one with: _.PULocationID

通过Python/Ibis和DuckDB查询数据

接下来,我们需要设置我们的执行逻辑——在这种情况下是Python代码——并使用翻译层Ibis来运行DuckDB查询作为我们的本地SQL引擎。

我将在这里解释最重要的步骤,但会跳过一些细节——完整的脚本可以在nyc_taxi.py中找到。首先,我们导入Ibis和我们的SemanticModel类,并通过Ibis定义数据集和执行引擎——再次,这里我们使用DuckDB并从CloudFront直接读取数据集:

```python import ibis from boringsemanticlayer import SemanticModel

con = ibis.duckdb.connect(":memory:") #or use "md:" for MotherDuck engine tables = { "taxizonestbl": con.readcsv("https://d37ci6vzurychx.cloudfront.net/misc/taxi+zonelookup.csv"), "tripstbl": con.readparquet("https://d37ci6vzurychx.cloudfront.net/trip-data/fhvhvtripdata_2025-06.parquet"), } ```

现在,我们已经读取了我们在YAML nyc_taxi.yml文件中创建的指标定义,并将其映射到表数据集,无聊的语义层知道我们有哪些数据集并可以查询它:

```python models = SemanticModel.fromyaml(f"nyctaxi.yml", tables=tables)

taxizonessm = models["taxizones"] #dataset name from the yaml file tripssm = models["fhvhv_trips"] ```

然后,我们将查询定义为带有Ibis和BSL的Python表达式——这里是按接客行政区的行程量

python expr = trips_sm.query( dimensions=["pickup_zone.borough"], measures=["trip_count", "avg_trip_miles", "avg_base_fare"], order_by=[("trip_count", "desc")], limit=5, )

我们可以执行并打印它:

python print(expr.execute())

结果如下:

pickup_zone_borough trip_count avg_trip_miles avg_base_fare 0 Manhattan 7122571 5.296985 33.575738 1 Brooklyn 5433158 4.215820 23.280429 2 Queens 4453220 6.379047 29.778835 3 Bronx 2541614 4.400500 20.313596 4 Staten Island 316533 5.262288 22.200712

刚刚发生了什么?我们定义了要显示度量的维度(pickup_zone.borough),配置了要显示的三个度量,并指定了顺序和返回的行数。

神奇的是,我们现在可以在YAML文件中更改度量,添加CASE WHEN语句,或修复格式错误,而无需触及查询或代码。非技术人员可以通过DSL(领域特定语言)和单独的配置文件获得访问权限,我们可以对其进行版本控制、协作,甚至利用LLM创建新的度量和维度。

Ibis为我们提供了以Pythonic方式实现这一点的灵活性。

在完整的脚本nyc_taxi.py和YAML文件nyc_taxi.yml中,可以找到更多示例,如热门接客区、服务区分析、按行程距离的收入分析和无障碍指标。

结论

希望你喜欢这篇文章,它提供了一个实用的示例,展示了如何使用DuckDB和MotherDuck构建语义层。

语义层的美在于它们赋予了一种处理指标的强大方法,辅以高级功能,但也通过我们在这里实现的简单解决方案。仅凭一个YAML文件和几行Python代码,我们就创建了一个可以在数据堆栈中的任何工具中提供一致指标的系统。无论你是构建仪表板、训练ML模型,还是启用AI助手,你的业务逻辑都保持在一个地方,而你的分析能力却在其他地方增长。

从简单的东西开始,比如Boring Semantic Layer和DuckDB,通过解决你最痛苦的指标不一致问题来证明其价值。然后,从那里扩展。

未来的你和你的同事会感谢你,当“收入”和“利润”在每个工具中始终意味着同一件事时。

评论总结

评论内容主要围绕“语义层”(Semantic Layer)的概念、应用场景及其实现方式展开,观点多样且各有侧重。以下是总结:

  1. 语义层的定义与价值

    • 语义层帮助用户以更直观的方式表达查询和结果,而非依赖复杂的数据库技术。
      引用
      • "a semantic layer helps express queries and their results in terms the end-consumers will care about" (btbuildem)
      • "语义层帮助用户以更直观的方式表达查询和结果" (btbuildem)
    • 语义层类似于BI仪表板的ORM,甚至扩展到机器学习中的特征存储。
      引用
      • "semantic layers are like ORM for but BI dashboards" (mousematrix)
      • "语义层类似于BI仪表板的ORM" (mousematrix)
  2. 语义层的实现方式

    • 使用Common Logic等标准可以为数据库添加语义层。
      引用
      • "Common Logic would be a good addition to any effort trying to add a semantic layer" (whitten)
      • "Common Logic是添加语义层的好选择" (whitten)
    • 语义层需要适当的语言和工具支持,而非仅通过YAML定义。
      引用
      • "Semantic Layer needs proper language and tooling support" (aszen)
      • "语义层需要适当的语言和工具支持" (aszen)
  3. 语义层的挑战与争议

    • 在大型BI团队中推广语义层存在困难,分析师更倾向于直接复制SQL而非创建可重用逻辑。
      引用
      • "Pivoting a decent sized BI shop toward using one is tough" (sschnei8)
      • "在大型BI团队中推广语义层存在困难" (sschnei8)
    • 语义层可能只是NoSQL中SQL视图的“花哨名称”,其实质并未改变。
      引用
      • "Is a 'semantic layer' nothing more than a fancy name for a SQL VIEW in a NoSQL?" (cryptonector)
      • "语义层可能只是NoSQL中SQL视图的‘花哨名称’" (cryptonector)
  4. 语义层的技术背景与趋势

    • DuckDB等工具通过抽象化技术简化了语义层的实现,但同时也存在营销炒作和术语创新的现象。
      引用
      • "DuckDB can abstract away some stuff, but there is also some kind of marketing hype" (Demiurge)
      • "DuckDB等工具简化了语义层的实现,但也存在营销炒作" (Demiurge)
    • 语义层需要更丰富的表达系统,Ibis等工具为构建领域特定语言提供了基础。
      引用
      • "we need better expression systems and Ibis is a great foundation" (mousematrix)
      • "语义层需要更丰富的表达系统,Ibis是很好的基础" (mousematrix)

总结:语义层作为一种技术概念,旨在简化数据查询和结果表达,但其定义、实现方式和推广价值存在争议。尽管其在BI和机器学习中有广泛应用,但其实际效果和必要性仍需进一步验证。