TL;DR

Семантический слой — это не «магический прослоенный модуль», а логический уровень, где один раз задаются бизнес-метрики и измерения, а дальше переиспользуются в BI, веб-приложениях, ноутбуках и интеграциях с LLM.

Статья сначала честно разбирает случаи, когда без него можно обойтись, а затем показывает простой, но рабочий пример: YAML для описания метрик и измерений, Python + Ibis для исполнения запросов и DuckDB как движок. На примере ~20 млн поездок такси демонстрируются определения мер, joins с зонами, построение запросов, материализация куба и базовая визуализация.

В финале разбираются ограничения такого подхода, практические сценарии применения и то, как семантический слой помогает привести метрики к единому смыслу во всём аналитическом стеке.

Многие задаются вопросом: «Зачем мне семантический слой? Что это вообще такое?» В этом практическом руководстве мы соберём самый простой семантический слой — всего лишь из файла YAML и Python-скрипта. Цель не в том, чтобы построить слой ради самого слоя, а в том, чтобы понять его ценность. Затем мы выполним запросы к данным о 20 миллионах поездок нью-йоркских такси, используя согласованные бизнес-метрики, при этом запросы выполняются через DuckDB и Ibis. К концу вы точно будете понимать, когда семантический слой действительно решает проблемы, а когда это избыточно.

Это тема, которой я увлечён: я использую семантические слои внутри BI-инструментов уже более двадцати лет, и только недавно появились полнофункциональные семантические слои, существующие вне BI-инструмента. Они сочетают преимущества логического слоя с возможностью делиться им между веб-приложениями, Jupyter-ноутбуками и BI-инструментами. В семантическом слое ваш KPI по выручке или другие сложные показатели компании определяются один раз — в едином источнике истины; больше не нужно переопределять их снова и снова.

Мы посмотрим на самый простой вариант семантического слоя, который использует простой YAML-файл (для описания семантики) и Python-скрипт для выполнения через Ibis на DuckDB. Прежде чем перейти к практическому примеру с кодом, кратко напомним, что такое семантический слой.

Примечание: код ко всем примерам из статьи доступен в репозитории semantic-layer-duckdb на GitHub.

Когда семантический слой не нужен

Начнём со случаев, когда он вам не нужен и будет неправильным выбором. Самые простые и очевидные причины:

  • Вы только начинаете работать с аналитикой и у вас есть лишь один потребитель данных, то есть один способ показывать аналитические данные — например, BI-инструмент, Jupyter-ноутбуки или веб-приложение, но не несколько способов одновременно. Это означает, что вы не применяете расчётную логику в разных местах.

  • У вас нет обширной бизнес-логики для ad hoc-запросов; вам достаточно простых подсчётов, операций SUM или средних значений.

  • Вы предварительно рассчитываете все метрики как SQL-трансформации в физические таблицы, то есть дальнейшие аналитические инструменты получают все метрики уже предобработанными и агрегированными, а фильтрация работает достаточно быстро.

Зачем использовать семантический слой?

В этой статье я сосредоточусь на случаях применения, а в следующей главе покажу практический пример использования.

Чтобы лучше понять причины применения семантического слоя, начнём с полезного определения от Джулиана Хайда:

«Семантический слой, также известный как метрический слой, находится между бизнес-пользователями и базой данных и позволяет этим пользователям составлять запросы в терминах, которые они понимают. Он также регулирует доступ к данным, управляет преобразованиями данных и может настраивать базу данных, определяя материализации».

Как и многие новые идеи, семантический слой — это квинтэссенция и эволюция многих прежних идей, таких как языки запросов, многомерный OLAP и федерация запросов.

Основные причины использовать семантический слой могут включать одну или несколько из следующих потребностей:

1. Единое место, где ad hoc-запросы определяются один раз, совместно и под управлением системы контроля версий, с возможностью подключать их к разным BI-инструментам, веб-приложениям, Jupyter-ноутбукам или интеграции с AI/MCP. Это позволяет избежать дублирования метрик в каждом инструменте, упрощает сопровождение и управление данными, формируя согласованный бизнес-слой с инкапсулированной бизнес-логикой.

Пример: В большинстве организаций довольно быстро начинают одновременно использовать несколько BI-инструментов, а вдобавок — Excel или Google Таблицы. Вместо того чтобы поддерживать отдельные вычисляемые поля и бизнес-логику в каждом инструменте в его закрытом формате, семантический слой даёт одно определение, которое работает на всех платформах.

2. Кэширование необходимо для ad hoc-запросов, опирающихся на разные источники данных. Определение метрик, позволяющее выполнять предварительные расчёты для отклика менее секунды, может принести пользу любым нижестоящим аналитическим инструментам — по сравнению с реализацией кастомных подключений и использованием разных баз данных. При этом устраняются затраты на перемещение данных, поскольку запросы выполняются «на месте» — с использованием SQL pushdown, оптимизированного под диалекты, в разнородных источниках данных (проталкивания вычислений). Это снижает накладные расходы на инфраструктуру и облачные издержки.

Пример: для непроизводственной или высоконагруженной OLTP-системы семантический слой может напрямую опрашивать разные источники данных (например, IoT-потоки, логи и прочие данные) вместо их перемещения в дата-лейк или хранилище. Благодаря кэшу семантического слоя скорость оказывается достаточной и без переноса данных.

3. Единая модель контроля доступа обеспечивается через различные API (REST, GraphQL, SQL, ODBC/JDBC, MDX/Excel). Единый аналитический API позволяет реализовать self-service BI (самообслуживаемую аналитику): пользователи могут подключать Excel к очищенному, быстрому и унифицированному API.

Пример: Централизованная безопасность на уровне строк и столбцов, которая работает единообразно во всех нижестоящих аналитических инструментах, вместо попыток управлять доступом отдельно в каждом BI- или аналитическом инструменте, имеющем доступ к данным. Пользователи могут напрямую подключаться из Excel и «из коробки» получать корректные права доступа и вычисленные бизнес-метрики.

4. Динамическое переписывание запросов автоматически переводит простые, «человекопонятные» запросы в сложный, оптимизированный SQL для выполнения в нескольких базах данных. Это позволяет пользователям формулировать интуитивные запросы в бизнес-терминах (например, «average_order_value») без знания сложности базовой модели данных, связей между таблицами или специфики синтаксиса конкретной СУБД. Семантический слой сводит сложную аналитику — такие вещи, как отношения (ratio) на разных уровнях детализации, времовые срезы (год-к-году, скользящие периоды) и нестандартные календари — к простым семантическим запросам.

Пример: Упрощение сложной аналитики за счёт обработки нетривиальных вычислений, которые в «чистом» SQL получаются громоздкими: отношения на разных уровнях детализации (например, «на одного участника в месяц» в страховании — PMPM), времовой анализ (year-to-date (YTD), trailing 12 months, сравнение период к периоду), а также логика нестандартных календарей. Всё это сводится к простым семантическим запросам, а не к нагромождению подзапросов и подсчётов уникальных значений.

5. Контекст для LLM, повышающий точность и качество запросов на естественном языке, может быть существенно усилен с помощью семантического слоя, который задаёт бизнес-контекст и предотвращает частые «галлюцинации» ИИ, поскольку большая часть бизнес-логики (иногда и модели данных) настроена и определена именно в семантическом слое, помогая LLM лучше понимать предметную область.

Пример: Внутренним большим языковым моделям (LLM) или системам с генерацией, дополненной извлечёнными данными (RAG, Retrieval-Augmented Generation) необходим бизнес-контекст, чтобы понимать задачи. Связи измерений и фактов в семантическом слое вместе с определениями метрик помогают модели понимать и предлагать более корректные SQL-запросы или ответы на естественном языке.

В более широком смысле семантические слои организованно и под управлением регламентов закрывают разрыв между потребностями бизнеса и интеграцией источников данных. Лучше всего они подходят крупным предприятиям с множеством разрозненных KPI, которые могут позволить себе добавить ещё один слой в свой дата-стек. Однако пример ниже использует самый простой и минимальный семантический слой — даже при небольшом объёме данных.

ПРИМЕР: если хотите углубиться

Брайан Биккел выступил с отличным докладом в сообществе Practical Data Community о семантических слоях и проблемах, которые они решают. Очень рекомендую посмотреть "Semantic Layer Deep Dive". Если вы уже следите за шоу Practical Data Джо Риса, обратите внимание и на презентацию Гамильтона Улмера об Instant SQL с DuckDB/MotherDuck — это не совсем про семантические слои, но связано с историей SQL и CTE и показывает, как помогает „мгновенный SQL“.»

Наборы данных и агрегаты

Важное различие — нужны ли нам постоянные (персистентные) датасеты или ad hoc-запросы. Это обычно очень разные вещи. Ad hoc-запросы обязаны быть гибкими и менять уровень детализации по мере добавления измерений. То есть человек, выполняющий запрос, может переключиться с дневного вида на недельный или месячный, добавить регион, а затем решить «свернуть» до уровня страны — и всё это за пару секунд. Поэтому времени на обновление или переработку данных попросту нет.

Вычисляемые меры нужно добавлять «на лету», без повторного прогона ETL-процесса. Распространённый обходной путь — создавать несколько персистентных физических датасетов в dbt, каждый с теми же данными, но с разной гранулярностью, чтобы в BI-инструменте можно было показывать разные графики с разными акцентами. Семантический слой (или ad hoc-запросы) делает это «на лету».

Понятия можно развести так:

  • набор данных ≠ агрегаты

  • столбцы таблицы ≠ метрики

  • физическая таблица ≠ логическое определение

Если вам начинают требоваться сущности из правой колонки, значит, вам нужен семантический слой — будь то встроенный в BI-инструмент или реализованный отдельно по причинам, указанным выше.

Семантический слой не отменяет необходимость аккуратно спроектированной физической модели: на курсе «PostgreSQL для администраторов баз данных и разработчиков» подробно разбираются схемы, индексация и планировщик запросов — то, на чём держится производительность таких логических абстракций.


Как работает семантический слой: практический пример

Теперь посмотрим, как это работает, на примере максимально практичного семантического слоя. Самый простой вариант, который я нашёл, предложил Жюльен Хюро, недавно анонсировавший проект Boring Semantic Layer (BSL). В качестве движка запросов мы используем DuckDB, а в качестве слоя трансляции — Ibis (в Python), при выполнении в DuckDB.

ПРИМЕЧАНИЕ: Чего нет в этом семантическом слое

Это сравнительно простой семантический слой. Более продвинутые решения включают дополнительные возможности: несколько полноценных API (REST, GraphQL, SQL, ODBC, Excel), расширенные механизмы безопасности и мощный слой кэширования. В этом примере акцент на логической модели данных (Logical Data Model): мы задаём бизнес-требования в YAML — это абстракция над физическим слоем. Хотя она довольно близка к физическому слою, именно здесь инструменты уровня предприятия (Cube, dbt SL, GoodData, AtScale) дают больше преимуществ.

Мы собираемся построить примерно то, что показано ниже: определения в YAML выступают как описания наших метрик (вычисляемых мер и измерений), а Ibis отвечает за трансляцию запросов для выполнения на любом движке; здесь мы используем DuckDB.

Для справки: Каталоги данных

Если вы задаётесь вопросом, как в эту картину вписываются DuckLake или другие каталоги открытых форматов таблиц (Iceberg, Unity Catalog, Polaris), ответ такой: они связываются с определениями метрик. Соответственно, каталог содержит полный список доступных дата-активов. Открытые каталоги данных можно рассматривать как сервис поиска/справочник по наборам данных в вашем «озере данных» (data lake). Если вы используете семантический слой в том виде, как мы реализуем его здесь, отдельный каталог не потребуется, потому что все ваши метрики и измерения определены внутри самого семантического слоя.

Начало работы

Создадим виртуальное окружение, установим зависимости и сам семантический слой:

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

Это установит не только семантический слой, но и Ibis и прочие необходимые зависимости.

Теперь мы готовы определять метрики. Чтобы упростить пример и сфокусироваться именно на метриках, а не на данных, я использовал набор данных NYC Taxi, с которым многие знакомы. В нём есть таблица соответствий для мест посадки (pickups) и множество полезных данных; доступ к ним осуществляется по HTTPS.

ПРИМЕЧАНИЕ: можно использовать общие наборы данных MotherDuck

Если вы хотите воспользоваться общими наборами данных MotherDuck, посмотрите раздел Example Datasets — там, например, загружен набор NYC 311 Service Requests, а также Foursquare и другие полезные датасеты. Эти данные можно запрашивать мгновенно: запустите duckdb и подключитесь к MotherDuck, как показано ниже.

MotherDuck DuckDB Query Example

Как мы уже выяснили, семантические слои удобны для централизованного и настраиваемого определения метрик, поэтому для этого мы используем YAML. У YAML минимальные накладные расходы, он легко читается — именно поэтому большинство семантических слоёв его используют. Альтернативой мог бы быть SQL, но в нём не хватает важных возможностей, например переменных; к тому же определения склонны разрастаться в чрезмерно вложенные конструкции и становятся трудны в сопровождении. На практике наиболее эффективно сочетание YAML с эпизодическим встраиванием SQL-фрагментов там, где это уместно.

Для начала посмотрим, с какими данными мы работаем — быстро посчитаем строки и опишем таблицы:

D select count(*) FROM read_parquet("https://d37ci6vzurychx.cloudfront.net/trip-data/fhvhv_tripdata_2025-06.parquet");
┌─────────────────┐
│  count_star()   │
│      int64      │
├─────────────────┤
│    19868009     │
│ (19.87 million) │
└─────────────────┘
D DESCRIBE FROM read_parquet("https://d37ci6vzurychx.cloudfront.net/trip-data/fhvhv_tripdata_2025-06.parquet");
┌──────────────────────┬─────────────┬─────────┬─────────┬─────────┬─────────┐
│     column_name      │ column_type │  null   │   key   │ default │  extra  │
│       varchar        │   varchar   │ varchar │ varchar │ varchar │ varchar │
├──────────────────────┼─────────────┼─────────┼─────────┼─────────┼─────────┤
│ hvfhs_license_num    │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ dispatching_base_num │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ originating_base_num │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ request_datetime     │ TIMESTAMP   │ YES     │ NULL    │ NULL    │ NULL    │
│ on_scene_datetime    │ TIMESTAMP   │ YES     │ NULL    │ NULL    │ NULL    │
│ pickup_datetime      │ TIMESTAMP   │ YES     │ NULL    │ NULL    │ NULL    │
│ dropoff_datetime     │ TIMESTAMP   │ YES     │ NULL    │ NULL    │ NULL    │
│ PULocationID         │ INTEGER     │ YES     │ NULL    │ NULL    │ NULL    │
│ DOLocationID         │ INTEGER     │ YES     │ NULL    │ NULL    │ NULL    │
│ trip_miles           │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ trip_time            │ BIGINT      │ YES     │ NULL    │ NULL    │ NULL    │
│ base_passenger_fare  │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ tolls                │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ bcf                  │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ sales_tax            │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ congestion_surcharge │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ airport_fee          │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ tips                 │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ driver_pay           │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
│ shared_request_flag  │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ shared_match_flag    │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ access_a_ride_flag   │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ wav_request_flag     │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ wav_match_flag       │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ cbd_congestion_fee   │ DOUBLE      │ YES     │ NULL    │ NULL    │ NULL    │
├──────────────────────┴─────────────┴─────────┴─────────┴─────────┴─────────┤
│ 25 rows                                                          6 columns │
└────────────────────────────────────────────────────────────────────────────┘

А также поиск в CSV:

D select count(*) from read_csv("https://d37ci6vzurychx.cloudfront.net/misc/taxi+_zone_lookup.csv");
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     265      │
└──────────────┘
D describe from read_csv("https://d37ci6vzurychx.cloudfront.net/misc/taxi+_zone_lookup.csv");
┌──────────────┬─────────────┬─────────┬─────────┬─────────┬─────────┐
│ column_name  │ column_type │  null   │   key   │ default │  extra  │
│   varchar    │   varchar   │ varchar │ varchar │ varchar │ varchar │
├──────────────┼─────────────┼─────────┼─────────┼─────────┼─────────┤
│ LocationID   │ BIGINT      │ YES     │ NULL    │ NULL    │ NULL    │
│ Borough      │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ Zone         │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
│ service_zone │ VARCHAR     │ YES     │ NULL    │ NULL    │ NULL    │
└──────────────┴─────────────┴─────────┴─────────┴─────────┴─────────┘

Это даёт нам хорошее представление о том, с чем мы имеем дело. Из словаря данных видно, что PULocationID и DOLocationID представляют зоны такси, которые следует соединять с приведённой выше таблицей соответствий зон по столбцу LocationID.

Обычно дальше я использую команду SUMMARIZE — это специфичная для DuckDB команда (оператор), которая возвращает статистики по данным, такие как min, max, approx_unique, avg, std, q25, q50, q75, count. Это даёт быстрый и удобный обзор того, с чем мы работаем.

Определение метрик в скучном семантическом слое

Теперь можно переходить к определению метрик. Начнём с задания временной метки и её гранулярности (требование BSL), а затем перечислим измерения — это будет выглядеть примерно так:

fhvhv_trips:
  table: trips_tbl
  time_dimension: pickup_datetime
  smallest_time_grain: TIME_GRAIN_SECOND
  
  dimensions:
    hvfhs_license_num: _.hvfhs_license_num
    dispatching_base_num: _.dispatching_base_num
    originating_base_num: _.originating_base_num
    request_datetime: _.request_datetime
    pickup_datetime: _.pickup_datetime
    dropoff_datetime: _.dropoff_datetime
    trip_miles: _.trip_miles
    trip_time: _.trip_time
    base_passenger_fare: _.base_passenger_fare
    tolls: _.tolls
    bcf: _.bcf
    sales_tax: _.sales_tax
    congestion_surcharge: _.congestion_surcharge
    airport_fee: _.airport_fee
    tips: _.tips
    driver_pay: _.driver_pay
    shared_request_flag: _.shared_request_flag
    shared_match_flag: _.shared_match_flag
    access_a_ride_flag: _.access_a_ride_flag
    wav_request_flag: _.wav_request_flag
    wav_match_flag: _.wav_match_flag

Столбец pickup_datetime — это временной столбец, причём уровень детализации установлен до секунды; все остальные столбцы рассматриваются как измерения.

Самое интересное начинается при определении мер — расчётных показателей, которые могут быть весьма сложными и потенциально зависеть от нескольких слоёв уже существующих мер. Определяются они так:

  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()

И ещё несколько мер, которые агрегируют только помеченные записи, например совместные поездки или запрос на машину, приспособленную для инвалидных колясок:

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

Чтобы собрать функциональный дашборд и глубже «проваливаться» в разрезы, нам нужны измерения, добавляющие контекст при выполнении запросов. Например, если мы хотим агрегировать по боро Нью-Йорка, этой информации нет в данных о поездках, она содержится в нашей таблице соответствий, как мы видели выше в DESCRIBE. Давайте присоединим эту таблицу и используем её.

Сначала определим дополнительный набор данных в YAML следующим образом:

taxi_zones:
  table: taxi_zones_tbl
  primary_key: LocationID
  
  dimensions:
    location_id: _.LocationID
    borough: _.Borough
    zone: _.Zone
    service_zone: _.service_zone
    
  measures:
    zone_count: _.count()

И, наконец, нам нужно соединить два набора данных. Это можно указать так — добавив в набор данных fhvhv_trips:

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

Выполнение запросов через Python/Ibis и DuckDB

Далее нам нужно настроить логику исполнения — в нашем случае это код на Python — и использовать прослойку трансляции Ibis, чтобы локально выполнять запросы в DuckDB, который выступает нашим SQL-движком.

Я объясню самые важные шаги и опущу некоторые детали — полный скрипт вы найдёте в nyc_taxi.py на github. Сначала импортируем Ibis и наш класс SemanticModel из Boring Semantic Layer и определяем наборы данных; в качестве движка выполнения используем DuckDB, обращаясь к нему через Ibis — здесь мы снова используем DuckDB и читаем датасет напрямую из CloudFront:

import ibis
from boring_semantic_layer import SemanticModel

con = ibis.duckdb.connect(":memory:") #or use `"md:"` for MotherDuck engine
tables = {
    "taxi_zones_tbl": con.read_csv("https://d37ci6vzurychx.cloudfront.net/misc/taxi+_zone_lookup.csv"),
    "trips_tbl": con.read_parquet("https://d37ci6vzurychx.cloudfront.net/trip-data/fhvhv_tripdata_2025-06.parquet"),
}
ПРИМЕЧАНИЕ: Масштабирование с MotherDuck

Одним простым изменением можно переключиться на MotherDuck как бэкенд DuckDB (через Ibis). Вместо con = ibis.duckdb.connect(":memory:") используйте con = ibis.duckdb.connect("md:")

Теперь, когда мы прочитали определения метрик из YAML-файла nyc_taxi.yml и сопоставили их с таблицами набора данных, Boring Semantic Layer «понимает», какие датасеты у нас есть, и может выполнять по ним запросы:

models = SemanticModel.from_yaml(f"nyc_taxi.yml", tables=tables)

taxi_zones_sm = models["taxi_zones"] #dataset name from the yaml file
trips_sm = models["fhvhv_trips"] 

Далее мы задаём запрос как выражение на Python с Ibis и BSL — здесь пример: объём поездок по боро (по месту посадки):

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

И выполняем его, выводя результат:

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»), в разрезе которого хотим показывать меры, выбрали три меры для вывода и указали сортировку и количество строк через LIMIT.

Вся «магия» в том, что теперь мы можем изменить метрику в YAML-файле, добавлять выражение CASE WHEN или править форматирование — и всё это без изменения самого запроса или кода. Менее технические специалисты получают доступ через предметно-ориентированный язык (DSL) и отдельный конфигурационный файл, который можно вести в системе контроля версий, редактировать совместно, а при желании — привлекать LLM для добавления новых мер и измерений.

Ibis даёт нам гибкость делать это «по-питоновски».

Больше примеров — таких, как популярные зоны посадки, анализ сервисных зон, анализ выручки по расстоянию поездки и метрики доступности, — ищите в полном скрипте nyc_taxi.py и в YAML-файле nyc_taxi.yml.

ПРЕДУПРЕЖДЕНИЕ: Ограничения

Мне не удалось выполнить двойное соединение (join) одного и того же набора данных: отдельно для мест посадки и для мест высадки. Поэтому в этом примере я присоединяю таблицу только один раз.

Материализация

Если вы хотите ускорить работу и создать материализованный куб, можно воспользоваться Xorq — см. пример в example_materialize.py.

import pandas as pd
import xorq as xo

from boring_semantic_layer import SemanticModel

df = pd.DataFrame(
    {
        "date": pd.date_range("2025-01-01", periods=5, freq="D"),
        "region": ["north", "south", "north", "east", "south"],
        "sales": [100, 200, 150, 300, 250],
    }
)

con = xo.connect()
tbl = con.create_table("sales", df)

sales_model = SemanticModel(
    table=tbl,
    dimensions={"region": lambda t: t.region, "date": lambda t: t.date},
    measures={
        "total_sales": lambda t: t.sales.sum(),
        "order_count": lambda t: t.sales.count(),
    },
    time_dimension="date",
    smallest_time_grain="TIME_GRAIN_DAY",
)

cube = sales_model.materialize(
    time_grain="TIME_GRAIN_DAY",
    cutoff="2025-01-04",
    dimensions=["region", "date"],
    storage=None,
)

print("Cube model definition:", cube.json_definition)

df_cube = cube.query(
    dimensions=["date", "region"], measures=["total_sales", "order_count"]
).execute()

Более сложные меры

Этот пример относительно прост, но показывает, как можно использовать простой семантический слой поверх озера данных с DuckDB.

Если вам нужны более продвинутые меры, зависящие друг от друга, легко представить, насколько это будет полезно. Сила семантических слоёв в том, что они позволяют просто задавать зависимости сложных мер, устраняя необходимость повторять сотни строк SQL-кода в вашем CTE-запросе.

Разумеется, можно использовать dbt для управления зависимостями, но тогда у вас не будет возможностей для ad hoc-запросов, «фильтрации на лету» и аккуратно оформленных YAML-файлов, которые описывают ваши динамические запросы.

Визуализация

Любопытно, что BSL также включает некоторые возможности визуализации благодаря встроенной обёртке над Vega-Lite (декларативный язык для интерактивных визуализаций на основе JSON) и её Python-библиотеке Altair.

Достаточно установить пакеты командой uv add 'boring-semantic-layer[visualization]' altair[all], после чего можно строить простые визуализации. Ниже пример чуть развёрнут, чтобы получить приятную картинку, но вы легко можете представить более краткую версию — например, только с заголовком:

# Пример графика
png_bytes = expr.chart(
  format="png",  # Добавьте параметр формата здесь
  spec={
	"title": {
	    "text": "NYC Taxi Trip Volume by Borough",
	    "fontSize": 16,
	    "fontWeight": "bold",
	    "anchor": "start"
	},
	"mark": {
	    "type": "bar",
	    "color": "#2E86AB",
	    "cornerRadiusEnd": 4
	},
	"encoding": {
	    "x": {
		  "field": "pickup_zone_borough",
		  "type": "nominal",
		  "sort": "-y",
		  "title": "Borough",
		  "axis": {
			"labelAngle": -45,
			"titleFontSize": 12,
			"labelFontSize": 10
		  }
	    },
	    "y": {
		  "field": "trip_count",
		  "type": "quantitative",
		  "title": "Number of Trips",
		  "axis": {
			"format": ".2s",
			"titleFontSize": 12,
			"labelFontSize": 10
		  }
	    }
	},
	"width": 500,
	"height": 350,
	"background": "#FAFAFA"
  }
)

# Сохранить как файл
with open("trip-volume-by-pickup-borough-styled.png", "wb") as f:
  f.write(png_bytes)

Сгенерированный PNG выглядит так:

image
FAQ

Здесь показано, как прагматично реализовать семантический слой с DuckDB и простыми инструментами. Кроме того, я надеюсь, что теперь стало понятнее, что такое семантический слой и когда его уместно применять.

Прежде чем завершить, давайте пройдёмся по самым распространённым вопросам о семантическом слое.

Почему нельзя просто использовать базу данных?

Ключ — в семантическом логическом слое, который абстрагирует «физический мир» от мира моделирования. Это даёт большую гибкость: вы реализуете то, что нужно бизнесу, а не то, что диктует физическая модель данных. Попробуйте реализовать «выручку на клиента по кварталам со сравнением год к году» сразу в пяти разных BI-инструментах, имея лишь представления в базе данных, — скорее всего, получите пять разных реализаций, которые со временем начнут расходиться.

А если у нас сотни метрик — нужен ли семантический слой?

Как раз в таком случае он нужен больше всего. Управление сотнями метрик в нескольких инструментах без единого согласованного представления превращается в кошмар с точки зрения управляемости. В каждом инструменте формулы чуть отличаются, и никто не уверен, какая версия правильная. Семантический слой даёт единый источник истины.

Не добавляет ли семантический слой лишнюю сложность к уже и так сложному ландшафту данных?

Современный дата-стек обычно включает целый набор инструментов. В крупной организации семантический слой чаще всего снижает сложность, устраняя дублирование метрик между этими инструментами. Начальные затраты на внедрение окупаются, когда вам больше не приходится выяснять, почему значения выручки расходятся между Tableau и вашим веб-приложением.

Что если мои данные часто меняются? Не станет ли семантический слой «узким местом» для обновлений?

Это как раз сильная сторона семантических слоёв. В отличие от предварительно посчитанных таблиц-агрегатов, которые приходится перерабатывать при изменении исходных данных, семантические слои формируют запросы по требованию. Ваши метрики автоматически отражают актуальные данные, потому что вычисляются в реальном времени из источника. Вам нужно обновлять YAML-определения только при изменении бизнес-логики, а не при каждом обновлении данных.

И это может сделать процесс более гибким, чем поддержание десятков моделей dbt под разные уровни детализации.

Что если я хочу использовать MCP вместе с ним?

Если вы хотите добавить Model Context Protocol (MCP), например, с Claude Code, «скучный» семантический слой (Boring Semantic Layer) из коробки поддерживает эту связку вместе с xorq. Посмотрите короткое демо в LinkedIn от Свена Гоншорека.

Также можно заглянуть в репозиторий для подробностей и установить поддержку командой uv add 'boring-semantic-layer[mcp]'. Но в этой статье я сосредотачиваюсь прежде всего на возможностях семантического слоя и важности его применения.

Какие ещё популярные инструменты семантического слоя существуют?

Cube, AtScale, dbt Semantic Layer, GoodData. Некоторые из них мощнее других; не все поддерживают расширенные механизмы безопасности, разграничение доступа на уровне строк и столбцов, а также мощные API, интеграцию с Excel или кэширование. Я веду небольшой список таких инструментов в подборке «Semantic Layer Tools».

Как использовать семантический слой с MotherDuck?

Вот несколько интеграций, которые работают «из коробки»:

Заключение

Надеюсь, эта статья была вам полезна как практическая иллюстрация использования семантического слоя с DuckDB и MotherDuck.

Сила семантических слоёв — в том, что они позволяют управлять метриками в одном месте, дополняясь продвинутыми возможностями, но при этом могут быть и вполне простыми, как в нашем примере. Всего один YAML-файл и несколько строк Python — и у вас есть система, которая выдаёт согласованные метрики для любого инструмента в вашем дата-стеке. Хотите вы строить дашборды, обучать ML-модели или подключать AI-ассистентов — бизнес-логика остаётся в одном месте, а аналитические возможности растут повсюду.

Начните с простого — с Boring Semantic Layer и DuckDB — и докажите ценность, устранив самые болезненные рассогласования метрик. Затем масштабируйтесь.

Будущий вы и ваши коллеги скажут спасибо, когда «выручка» и «прибыль» будут означать одно и то же во всех инструментах, всегда.


Когда начинаешь жить с семантическим слоем, очень быстро упираешься в классические боли: где на самом деле живут метрики, как не утопить PostgreSQL под AI-нагрузкой, чем нормализовать хаос в ClickHouse и почему качество данных ломает любую красивую модель. Для тех, кто хочет разруливать это не на уровне «ещё одного костыля в BI», а в самом основании стека, есть несколько демо-уроков, которые преподаватели OTUS проведут бесплатно в рамках набора на курсы:

  • 19 ноября, 20:00 — PostgreSQL как векторная БД: строим AI-приложения с pgvector. Записаться

  • 24 ноября, 20:00 — Метрики качества данных и стратегия внедрения. Записаться

  • 26 ноября, 20:00 — Интеграция ClickHouse и dbt. Записаться

Комментарии (0)