Warstwa semantyczna danych - od sensu biznesowego do kodu

W wielu firmach te same dane prowadzą do różnych liczb i interpretacji, bo każdy zespół inaczej rozumie kluczowe pojęcia biznesowe. Brak wspólnej warstwy semantycznej powoduje chaos, utratę zaufania do raportów i paraliż decyzyjny. Ten artykuł pokazuje, jak zapanować nad sensem danych – od źródła problemu po praktyczne rozwiązania.

Problem: kiedy liczby przestają mieć znaczenie

Jeśli pracujesz z danymi dłużej niż tydzień, znasz ten ból doskonale. Ten sam raport, trzy zespoły i… trzy różne wartości “sprzedaży”. Dane źródłowe te same, Zapytania SQL poprawne, dashboardy w Power BI działają jak należy, joiny się zgadzają - a mimo to ktoś kłamie. Albo raczej: każdy ma swoją prawdę. Manager regionu pokazuje 2,3 mln przychodu. CFO mówi o 1,8 mln. Analityk twierdzi, że obie liczby są prawidłowe. Zarząd pyta, kto jest niekompetentny. I co zaraportować na giełdę? Problem nie leży w danych. Baza jest jedna, źródła te same, pipeline działa. Problem leży w tym, że:

  • Analityk A liczy tylko zamówienia ze statusem PAID
  • Analityk B liczy PAID + SHIPPED
  • CFO liczy wszystko oprócz CANCELLED, bo “faktura wystawiona”

Każdy ma rację w swojej interpretacji. I każdy ma inną definicję tego, co znaczy słowo “sprzedaż”.

Konsekwencje biznesowe

To nie jest tylko techniczny detal. To jest poważne biznesowe ryzyko, może nawet problem. Taka sytuacja prowadzi do:

  • paraliżu decyzyjnego - “sprawdźmy jeszcze raz dane” staje się mantrą każdego spotkania zarządu. Decyzje są odkładane, bo nikt nie ufa liczbom. Wszyscy mają więcej roboty (albo dłużej czekają na wynik).
  • utraty zaufania do BI - działy zaczynają robić własne Excele, bo “ten dashboard znowu coś źle pokazuje”. Self-service BI zamienia się w self-destruction. I wcale to nie poprawia sytuacji - teraz manager regionu mówi 2,3 mln, CFO mówi 1,8 mln, zespół ze swoim Excelem mówi 2,1 mln… Czy jest lepiej? No jest “w środku”, ale czy lepiej?
  • marnowania czasu - analitycy spędzają 60% czasu na wyjaśnianiu rozbieżności między raportami zamiast na analizie. IT poprawia “ostatni raz” któryś z kolei dashboard…
  • złych decyzji biznesowych - zarząd optymalizuje niewłaściwe wskaźniki, nakładane są niewłaściwe cele - każdy dział inaczej rozumie wskaźniki, więc dąży do czegoś innego.

Dlaczego to się dzieje?

Przyczyna jest dość banalna: brak jest jednego źródła prawdy - nie w sensie bazy danych, ale w sensie znaczenia. Mamy jedną bazę, ale dziesiątki interpretacji tego, co dane w niej zgromadzone oznaczają. Logika biznesowa i znaczenia są rozproszone:

  • w SQL-ach analityków
  • w miarach Power BI
  • w Excelach handlowców (nie chcesz wiedzieć…)
  • w głowach ludzi

I każdy krok tego łańcuszka może wprowadzić inną definicję. Każdy swoją, każdy prawdziwą.

Warstwa semantyczna - tłumacz między danymi a ludźmi

Warstwa semantyczna powstała dokładnie po to, żeby ten cyrk zakończyć. To jest tłumacz między surowymi danymi a ludźmi, którzy chcą zadawać sensowne pytania i dostawać jedną, wspólną odpowiedź.

Czym właściwie jest warstwa semantyczna?

Najprościej mówiąc: to warstwa, która wie co dane znaczą, a nie tylko jakie są. Baza danych wie, że kolumna order_status zawiera wartości NEW, PAID, SHIPPED, CANCELLED. Wie, bo ta kolumna jest tak zdefiniowana (jako enum) albo inserty wkładające dane do bazy mają tylko takie możliwości. Warstwa semantyczna wie, że: > Sprzedaż = suma kwot zamówień o statusie PAID lub SHIPPED To jest kontrakt semantyczny. Jedna definicja, żadnych dyskusji. Nie dlatego, że ktoś ma rację - tylko dlatego, że wszyscy patrzą na to samo znaczenie.

Pięć kluczowych cech warstwy semantycznej

1. Jedno źródło znaczeń, nie tylko danych Dane możesz mieć w jednej bazie, w jednej hurtowni. Ale jeśli każdy raport je inaczej interpretuje, to masz dziesiątki źródeł prawdy. Warstwa semantyczna to jedno, kanoniczne miejsce, gdzie definiujesz co coś znaczy. 2. Oddzielenie “jak liczymy” od “co oglądamy” Bez warstwy semantycznej logika biznesowa siedzi wszędzie: w dashboardach, SQL-ach, Excelach. Z warstwą semantyczną: logika liczenia jest w jednym miejscu, a raporty tylko z niej korzystają. 3. Wspólny język dla biznesu, analityków i IT Biznes mówi: “Chcę marżę netto na kliencie”. SQL odpowiada: 100 * (sum(revenue) - sum(costs)) / sum(revenue). Warstwa semantyczna tłumaczy to na Marża netto (%) - pojęcie, którym można rozmawiać, które ma opis, właściciela i jasne źródła. 4. Mniej SQL-owego spaghetti, więcej spójności Jeśli zmiana definicji KPI wymaga poprawy 27 dashboardów, każdy ma własne zapytania, a każdy join jest “lekko inny” - to masz klasyczny brak warstwy semantycznej. Z nią: poprawiasz raz, działa wszędzie. 5. Skalowalność: więcej raportów bez chaosu Na początku: 2 analityków, 5 raportów, wszystko “jakoś działa”. Po roku: 20 raportów, self-service BI, każdy klika co chce. Bez warstwy semantycznej to się kończy pytaniem: “Dlaczego ten wykres pokazuje coś innego niż tamten?”

Po co mi to? - korzyści biznesowe

Dla managera

  • Koniec wojny o liczby. Zarząd nie pyta już “która liczba jest prawdziwa”, bo jest tylko jedna definicja każdego KPI. Tylko jedna definicja - to bardzo ważne. Różne działy firmy pokażą tą samą wartość dla sprzedaży, kosztów i przede wszystkim zysku.
  • Szybsze decyzje. Nie tracisz tygodni na wyjaśnianie rozbieżności między raportami. Zaufanie do danych rośnie, bo wszyscy wiedzą, co oznaczają. Teraz można być data driven.
  • Kontrola nad self-service BI. Gdy dajesz ludziom możliwość tworzenia własnych raportów, nie zamienia się to w chaos - bo wszyscy korzystają z tych samych definicji. I teraz każdy może robić swoje przekroje i szukać zależności pod swoim kątem.

Dla analityka

  • Mniej czasu na gaszenie pożarów. Zamiast wyjaśniać rozbieżności między raportami, skupiasz się na analizie - sprawdzasz dlaczego biznes idzie tak albo inaczej, co można zmienić i poprawić.
  • Jeden raz definiujesz, wszędzie działa. Nowe KPI? Dodajesz do warstwy semantycznej. Wszystkie raporty automatycznie mają dostęp. Wszystkie działy widzą te same dane, nie muszą tworzyć KPI po swojemu (i lepiej żeby tego nie robiły).
  • Przejrzysta dokumentacja. Każda miara ma opis, właściciela, logikę liczenia. Nowy analityk nie musi zgadywać, co oznacza revenue_adjusted_v3. Możecie się sprzeczać o definicję i ją wspólnie ustalać czy też zmieniać, ale jest ona jedna - każdy ma tak samo.

Dla technika

  • Mniej duplikacji kodu. Ta sama logika nie jest przepisywana w 15 dashboardach, wszyscy czerpią z jednego źródła.
  • Łatwiejsze utrzymanie. Zmiana definicji w jednym miejscu, a nie refactoring całego BI, w różnych działach.
  • Lepsze testy. Testujesz logikę raz, a nie każdy dashboard osobno.

Przykłady z życia - kiedy brak semantyki boli najbardziej

E-commerce: sprzedaż

Masz kolumnę order_status z wartościami: NEW, PAID, SHIPPED, CANCELLED. Pytanie biznesowe: “Ile było sprzedaży?” Bez warstwy semantycznej:

  • Marketing liczy NEW + PAID + SHIPPED (bo “potencjalna sprzedaż”)
  • Finanse liczą tylko PAID (bo “zaksięgowane”)
  • Logistyka liczy SHIPPED (bo “faktycznie wysłane”)

Zarząd dostaje trzy różne liczby i nikt nie wie, której używać. Z warstwą semantyczną masz jedną definicję: > Sprzedaż = suma kwot zamówień o statusie PAID lub SHIPPED. Koniec dyskusji (dyskusja była wcześniej - zapewne długa - zanim ustalono powyższą definicję). Wszyscy patrzą na to samo.

Bankowość: aktywny klient

“Aktywny klient” to kluczowy KPI. Ale co to właściwie znaczy?

  • Logowanie w ciągu 30 dni?
  • Albo transakcja kartą?
  • Albo saldo > 0?
  • A może wszystkie te warunki jednocześnie?

Bez semantyki każdy dashboard ma własną wersję tej definicji. Jeden pokazuje 100 tysięcy aktywnych klientów, drugi 85 tysięcy… Z semantyką masz jedną miarę: active_customer = true/false z jasną definicją. Raporty są głupie, wyświetlają sumę tych co mają true w tej kolumnie. I bardzo dobrze, tak ma być!

Produkcja: czas przestoju

Dyrektor fabryki nie chce wiedzieć z jakiej tabeli pochodzi downtime_minutes, jaki ma typ danych czy jak wygląda join. Analityk (czy ktokolwiek odpowiadający na to pytanie) nie powinien w ogóle używać słów “baza danych”, “tabela” czy “kolumna”! Dyrektor chce informację: > “Czas przestoju linii produkcyjnej (minuty)” Semantyka robi z technicznego języka pojęcia, którymi da się rozmawiać z biznesem.

Logistyka: terminowość dostaw

“Dostawa na czas” - brzmi prosto. Ale:

  • Czy liczymy względem planu?
  • Czy względem obiecanego terminu klientowi?
  • Co z dostawami w kilku częściach?
  • Co jeśli klient przesunął termin?

Bez semantyki każdy raport to osobna interpretacja. Z semantyką masz miarę on_time_delivery_rate (true albo false) z precyzyjną definicją, poprawiasz ją raz i działa wszędzie.

Jak to wygląda w praktyce (jeszcze bez technologii)

Najpierw obalimy mit:

Warstwa semantyczna to niekoniecznie nowe tabele z kopiami danych.

Czasem tak. Częściej - nie.

Trzy główne podejścia

1. Warstwa semantyczna jako logika (nie dane) Najczęstszy i najzdrowszy wariant.

  • Dane zostają tam, gdzie są (hurtownia, lakehouse)
  • Warstwa semantyczna to:
    • definicje miar
    • definicje wymiarów
    • reguły biznesowe
    • relacje między encjami

Przykłady narzędzi: Power BI semantic model, LookML (Looker), dbt metrics, Cube.dev, AtScale. Nie kopiujesz danych. Tworzysz abstrakcję. SQL jest ukryty, biznes widzi czytelne pojęcia. 2. Warstwa semantyczna jako widoki/modele logiczne Trochę bardziej “oldschool”, ale wciąż skuteczne. Budujesz widoki SQL, modele w dbt (staging → marts), czasem materializowane widoki. Semantyka = to, co pokazujesz, a nie surowe tabele. Minus: logika często się dubluje, trudniej zarządzać zmianami. 3. Warstwa semantyczna jako hybryda Najczęstszy sposób budowania warstwy semantycznej w realnych firmach:

  • dbt: porządkuje dane i nadaje strukturę
  • BI tool / semantic engine: definiuje miary, relacje, KPI
  • dokumentacja: opisuje sens, a nie tylko kolumny

Czy dane są kopiowane? Czasem tak (marty, agregaty), czasem nie (tylko widoki i metadane). Ale tu nie chodzi o kopiowanie, bo chodzi o kontrolę znaczeń. Kopiowanie (agregowanie, widoki) nie jest złe.

Kluczowa prawda o dokumentacji

I teraz coś, co wszyscy olewają, a potem płaczą:

Jeśli semantyka nie jest udokumentowana, to ona nie istnieje.

Tak - bo jak nie znamy znaczeń danych (znamy = mamy spisane; bo jak ktoś ma je w głowie to nie znaczy, że znamy - jak ten ktoś wpadnie pod tramwaj to już nie znamy).

Co dokumentować?

Dla każdej miary i wymiaru:

  • Definicję biznesową (ludzkim językiem, co to jest, do czego używamy - możesz wrzucić do GPT mądry opis i dodać “opisz to jakbyś miał tłumaczyć uczniowi średniej szkoły” - będzie dobre, serio)
  • Logikę liczenia (wzór, warunki, założenia)
  • Źródło danych (skąd pochodzą dane, które służą do wyliczenia danej miary, jak często same dane oraz miary się przeliczają)
  • Właściciela (kto decyduje o definicji?)
  • Przykłady (jak interpretować wartości, na przykładach z danych)

Narzędzia do dokumentacji

  • dbt docs - niedoceniane, ale bardzo skuteczne
  • Data-katalogi: Atlan, DataHub, Alation
  • Confluence / Notion - lepsze to niż nic
  • Looker / Power BI - opisy pól i miar wbudowane w narzędzie

Złota zasada: Dokumentacja obok kodu, a nie w PDF-ie z 2021 roku.

Przykład domenowy: budujemy e-commerce

Tu zaczynają się technikalia i mięsko w SQL (i nie tylko). Od tego momentu będziemy pracować na jednym, spójnym przykładzie. Klasyczny e-commerce:

Mamy dane

Mamy tabelki z kolumnami:

customers (customer_id, signup_date, segment)
orders (order_id, customer_id, order_date, status)
order_items (order_id, product_id, quantity, unit_price)
deliveries (order_id, promised_date, delivery_date)

Chcemy odpowiedzieć na pytania

  • Ile sprzedaliśmy?
  • Ilu mamy aktywnych klientów?
  • Jak terminowe są dostawy?
  • Jaka jest wartość klienta?
  • Jaka jest średnia wartość koszyka?

Definiujemy kluczowe KPI

  • Revenue - Przychód
  • Number of orders - Liczba zamówień
  • Active customers - Aktywni klienci
  • On-time delivery rate - Terminowość dostaw %
  • Average order value - Średnia wartość zamówienia

Każda miara: jedna definicja, jeden właściciel, jeden wzór.

Semantyka w SQL - pierwszy kontakt z kodem

Zanim przejdziemy do zaawansowanych narzędzi, zacznijmy od fundamentów: jak wygląda semantyka w czystym SQL?

Raw data (punkt wyjścia)

-- To NIE jest warstwa semantyczna
-- To jest materiał budowlany

orders (order_id, customer_id, order_date, status)
order_items (order_id, product_id, quantity, unit_price)
customers (customer_id, signup_date, segment)
deliveries (order_id, promised_date, delivery_date)

Staging - czyszczenie, dostosowanie typów, bez interpretacji

CREATE VIEW stg_orders AS
SELECT
  order_id,
  customer_id,
  CAST(order_date AS DATE) AS order_date,
  UPPER(status) AS status  -- normalizacja
FROM orders;

Tu:

  • normalizujesz dane
  • nie liczysz KPI
  • nie interpretujesz biznesu

Po prostu czyścisz i poprawiasz dane.

Fakty - tu zaczyna się semantyka

CREATE VIEW fct_orders AS
SELECT
  o.order_id,
  o.customer_id,
  o.order_date,
  SUM(oi.quantity * oi.unit_price) AS revenue
FROM stg_orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.status IN ('PAID', 'SHIPPED')  -- decyzja semantyczna!
GROUP BY 1, 2, 3;

Decyzja semantyczna: > Sprzedaż = tylko PAID i SHIPPED Jedna decyzja i jedno miejsce.

Wymiary - nudne, stabilne, konieczne

CREATE VIEW dim_customers AS
SELECT
  customer_id,
  signup_date,
  segment
FROM customers;

Im nudniej, tym lepiej. Wymiary nie powinny być miejscem niespodzianek.

Terminowość dostaw - pełny przykład

Ustalamy, że definicja jest taka (już w SQLu) - jeśli dostarczono przed obiecanym czasem to jest “on time”:

CREATE VIEW fct_deliveries AS
SELECT
  order_id,
  promised_date,
  delivery_date,
  CASE 
    WHEN delivery_date <= promised_date THEN 1
    ELSE 0
  END AS is_on_time
FROM deliveries;

Teraz możesz policzyć:

SELECT 
  AVG(is_on_time) AS on_time_delivery_rate
FROM fct_deliveries;

Biznes widzi: “Terminowość dostaw (%)”. Nie widzi techniki: joinów, CASE WHEN, dat i wszystkich innych “surowych” elementów. Mają swoją wartość, którą nawet można na wykresie pokazać!

Semantyka w dbt - porządna inżynieria

SQL to dobry start, ale przy większej skali potrzebujesz struktury. Tu wkracza dbt.

Architektura dbt: staging → intermediate → marts

staging/      <- czyszczenie, normalizacja
intermediate/ <- łączenia, transformacje
marts/        <- to, co wystawiasz do BI

Przykład: fct_orders w dbt

models/marts/fct_orders.sql

{{
  -- zmaterializuj wynik tego SQL-a jako fizyczną tabelę w bazie
  config(
    materialized='table'
  )
}}

-- weź dane z modelu stg_orders
WITH orders AS (
  SELECT * FROM {{ ref('stg_orders') }}
),

-- weź dane z stg_order_items
order_items AS (
  SELECT * FROM {{ ref('stg_order_items') }}
),

-- tu zaczyna się semantyka
-- łączymy zamówienia z pozycjami
-- liczymy revenue (quantity*unit_price),
-- liczymy liczbę unikalnych produktów na zamówienie,
-- filtrujemy tylko opłacone/wysłane (biznesowa definicja "ważnego" zamówienia)
-- GROUP BY - dostajemy revenue per zamówienie.
final AS (
  SELECT
    o.order_id,                    -- Klucz zamówienia
    o.customer_id,                 -- Klient
    o.order_date,                  -- Data (do agregacji czasowych)
    SUM(oi.quantity*oi.unit_price) AS revenue,       -- METRYKA: przychód z zamówienia
    COUNT(DISTINCT oi.product_id) AS product_count   -- METRYKA: ile unikalnych SKU w zamówieniu
  FROM orders o
  JOIN order_items oi ON o.order_id = oi.order_id   -- Standardowy join 1:N (zamówienie → pozycje)
  WHERE o.status IN ('PAID', 'SHIPPED')             -- BIZNESOWY FILTR: tylko "udane" zamówienia
  GROUP BY 1, 2, 3                                  -- Grupowanie po pierwszych 3 kolumnach (skrót SQL)
)

-- zwróć wynik z final CTE. dbt to zapisze jako tabelę
SELECT * FROM final

Miary semantyczne w dbt (YAML)

models/marts/schema.yml

version: 2

# SEKCJA 1: MODELE (tabele/marty)
# Dokumentacja tabeli fct_orders (tej z poprzedniego SQL-a).
# dbt używa tego do generowania docs (dbt docs generate) i autocomplete w narzędziach BI.
models:
  - name: fct_orders                    # Nazwa modelu/tabeli (pasuje do {{ ref('fct_orders') }} w SQLce)
    description: "Tabela faktów zamówień z przychodem"  # Biznesowy opis (widoczny w docs/UI)
    
    # Kolumny z opisami  semantyczny słownik! Pomaga unikać "co to jest revenue?"
    columns:
      - name: order_id
        description: "Unikalny identyfikator zamówienia"     # Klucz (dimension)
      
      - name: revenue
        description: "Przychód z zamówienia (tylko PAID i SHIPPED)"  # METRYKA bazowa


# SEKCJA 2: METRYKI
# Definicja KPI "revenue"  można  ciągnąć po wymiarach, agregować, wersjonować...
metrics:
  - name: revenue                       # Unikalna nazwa metryki (używasz w query: revenue)
    label: "Przychód"                   # Ludzka nazwa (do dashboardów, UI)
    
    # Źródło: tabela z poprzedniego SQL-a. ref() = dependency jak w SQL
    model: ref('fct_orders')            # "Weź dane stąd", dbt obsłuży joiny/dependency
    description: "Przychód z opłaconych i wysłanych zamówień"  # Biznesowa definicja!
    
    # ALGORYTM OBLICZENIA  to jest semantyka jako kod!
    calculation_method: sum              # SUMUJ po expression (inny: count, avg, median...)
    expression: revenue                  # Która kolumna? (może być złożona: revenue * 1.23)
    
    # CZASOWOŚĆ  do time-series (daily/weekly revenue)
    timestamp: order_date                # Kolumna do grupowania czasowego (WHERE date_trunc(...))
    
    # WYMIARY  po czym agregujesz (WHERE/GROUP BY)
    dimensions:
      - customer_id                      # Revenue per customer
      - order_date                       # Revenue per day
      # Możesz dodać: product_category, region, channel...

Dlaczego to jest lepsze niż czysty SQL?

  • Jedno źródło prawdy - miara zdefiniowana raz, używana wszędzie
  • Dokumentacja obok kodu - opis przy definicji
  • Testy - możesz testować semantykę, nie tylko typy
  • Wersjonowanie - Git śledzi zmiany definicji KPI plus masz za darmo cały proces weryfikacji/akceptacji pull requestów
  • Lineage - widzisz, skąd pochodzi każda miara

Terminowość dostaw - kompletny przykład w dbt

models/marts/fct_deliveries.sql

WITH deliveries AS (
  SELECT * FROM {{ ref('stg_deliveries') }}
),

final AS (
  SELECT
    order_id,
    promised_date,
    delivery_date,
    delivery_date <= promised_date AS is_on_time
  FROM deliveries
)

SELECT * FROM final

schema.yml

metrics:
  - name: on_time_delivery_rate
    label: "Terminowość dostaw (%)"
    model: ref('fct_deliveries')
    description: "Odsetek dostaw zrealizowanych w obiecany terminie"
    calculation_method: average
    expression: is_on_time
    timestamp: delivery_date

Efekt:

  • Jedna definicja terminowości
  • Używana w BI, SQL, API, raportach
  • Zmiana w jednym miejscu = zmiana wszędzie

Semantyka poza BI: Python / DuckDB / ML

Najczęstszy błąd: myślenie, że warstwa semantyczna to “rzecz od dashboardów”. To nieprawda.

Ten sam sens w Pythonie

import duckdb

con = duckdb.connect('warehouse.db')

# Ta sama semantyka co w BI
df = con.execute("""
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    SUM(revenue) AS revenue,
    COUNT(DISTINCT customer_id) AS unique_customers
  FROM fct_orders
  GROUP BY 1
  ORDER BY 1
""").df()

print(df.head())

Feature engineering oparty o semantykę

# Zamiast ad-hocowych obliczeń w Pythonie:

# ZŁE
revenue_bad = df[df['status'].isin(['PAID', 'SHIPPED'])].groupby('customer_id')['amount'].sum()

# DOBRE - korzystamy z semantyki
revenue_good = con.execute("""
  SELECT 
    customer_id,
    SUM(revenue) AS total_revenue
  FROM fct_orders
  GROUP BY customer_id
""").df()

Dlaczego to ma znaczenie?

  • Zero duplikacji logiki - ten sam wzór w BI, Pythonie, ML
  • Spójność - model ML trenuje na tych samych definicjach co raporty
  • Łatwiejsze debugowanie - jak zmienisz definicję, zmienia się wszędzie

Tu uwaga - popularne jest też pojęcie feature store szczególnie w projektach machine learingowych. Gdzie różnica? Feature store to system skoncentrowany na cechach używanych w ML: tworzeniu, wersjonowaniu, przechowywaniu i serwowaniu cech (feature’ów) do trenowania modeli. To jest taki “hub cech” dla data scientistów i MLOps, żeby nie odkrywać Ameryki za każdym razem przy budowie modelu. No i żeby wszyscy data scientiści mieli dostęp do już odkrytych cech. Jak te pojęcia się łączą? Te światy się coraz bardziej przenikają - semantic layer bywa źródłem lub wręcz fundamentem feature store’a.​ Typowy sensowny układ:

  • Warstwa semantyczna daje spójny, oczyszczony model biznesowy: tabele typu customer, order, subscription, oraz metryki jak revenue_30d, orders_90d, is_active_customer.​
  • Feature store używa tego modelu do budowy feature’ów: np. customer_ltv_365d, sessions_last_7d, discount_usage_ratio_90d, bazując na definicjach z warstwy semantycznej zamiast SQLa z dupy w każdym projekcie.​

Dzięki temu metryki w dashboardach i featury w modelach mają wspólną logikę; jak zmieniasz definicję “active customer”, to wiesz, co się rozsypie - zarówno w BI, jak i w ML.​ Wróćmy jednak do wykorzystania warstwy semantycznej w ML:

Przykład: predykcja churn

import duckdb
import pandas as pd
from sklearn.ensemble import RandomForestClassifier

con = duckdb.connect('warehouse.db')

# features z warstwy semantycznej
features = con.execute("""
  SELECT
    c.customer_id,
    COUNT(DISTINCT o.order_id) AS total_orders,
    SUM(o.revenue) AS total_revenue,
    AVG(o.revenue) AS avg_order_value,
    MAX(o.order_date) AS last_order_date,
    DATEDIFF('day', MAX(o.order_date), CURRENT_DATE) AS days_since_last_order
  FROM dim_customers c
  LEFT JOIN fct_orders o ON c.customer_id = o.customer_id
  GROUP BY c.customer_id
""").df()

# target: czy klient zrobił zamówienie w ostatnich 90 dniach?
features['is_active'] = features['days_since_last_order'] <= 90

# trenujesz model
X = features[['total_orders', 'total_revenue', 'avg_order_value', 'days_since_last_order']]
y = features['is_active']

model = RandomForestClassifier()
model.fit(X, y)

Kluczowa obserwacja:

  • Definicja revenue ta sama co w BI
  • Definicja active customer ta sama co w raportach
  • Jak zmienisz semantykę, zmienia się model

Testy warstwy semantycznej - jak nie dopuścić, żeby KPI zaczęły kłamać

Testy w warstwie semantycznej to nie jest QA na poziomie aplikacji i nie jest sprawdzanie, czy kolumna ma typ INT. To są testy jednego prostego pytania: > “Czy to, co liczymy, nadal znaczy to samo co wczoraj?”

Dlaczego w ogóle testować semantykę?

Bo zmiany w danych psują sens, a nie schemat. Scenariusze:

  • Ktoś zmieni statusy zamówień (PAIDCOMPLETED)
  • Ktoś doda nowy kanał sprzedaży
  • Ktoś “niewinnie” zmieni join

Pipeline przejdzie. Dashboard się odświeży. A liczby zaczną opowiadać inną historię.

Trzy typy testów, które wystarczą w 90% przypadków

1. Testy sensu biznesowego Najważniejsze, a najczęściej ignorowane. Revenue nigdy nie może być ujemne:

SELECT *
FROM fct_orders
WHERE revenue < 0;

Nie może być zamówień bez klienta:

SELECT *
FROM fct_orders o
LEFT JOIN dim_customers c USING (customer_id)
WHERE c.customer_id IS NULL;

Status musi być znaną wartością:

SELECT DISTINCT status
FROM orders
WHERE status NOT IN ('NEW', 'PAID', 'SHIPPED', 'CANCELLED');

Jeśli te testy się wywalą:

  • Pipeline działa
  • Ale biznesowo masz błąd

2. Testy zakresów i proporcji Czyli: “czy świat nadal wygląda jak świat, a nie jak bug”. Terminowość dostaw musi być w zakresie 0-1:

SELECT *
FROM (
  SELECT AVG(is_on_time) AS rate
  FROM fct_deliveries
) t
WHERE rate < 0 OR rate > 1;

Liczba aktywnych klientów nie może spaść o 90% z dnia na dzień:

WITH today AS (
  SELECT COUNT(*) AS cnt
  FROM dim_customers
  WHERE is_active = true
),
yesterday AS (
  SELECT COUNT(*) AS cnt
  FROM dim_customers_yesterday
  WHERE is_active = true
)
SELECT *
FROM today t, yesterday y
WHERE t.cnt < y.cnt * 0.1;

Nie złapiesz wszystkich błędów ora razu, ale możesz łapać najgłupsze i najdroższe na początek (a potem kolejne). 3. Testy kontraktowe Semantyka zakłada konkretne wartości. Jeśli się zmienią “pod spodem”, chcesz o tym wiedzieć. Czy nie pojawiły się nowe statusy zamówień?

SELECT DISTINCT status
FROM orders
WHERE status NOT IN ('NEW', 'PAID', 'SHIPPED', 'CANCELLED');

Czy struktura tabeli się nie zmieniła?

SELECT column_name
FROM information_schema.columns
WHERE table_name = 'orders'
  AND column_name NOT IN ('order_id', 'customer_id', 'order_date', 'status');

To jest test typu: “Hej, ktoś zmienił rzeczywistość, a my o tym nie wiemy”.

Testy w dbt

# models/schema.yml

models:
  - name: fct_orders
    tests:
      - dbt_utils.expression_is_true:
          expression: "revenue >= 0"
    columns:
      - name: customer_id
        tests:
          - not_null
          - relationships:
              to: ref('dim_customers')
              field: customer_id
      - name: status
        tests:
          - accepted_values:
              values: ['NEW', 'PAID', 'SHIPPED', 'CANCELLED']

Gdzie te testy żyją?

Muszą żyć. Ale nie są po to, żeby chwalić się nimi (że mamy) na slajdach w Power Poincie albo opisywać je na Wiki czy innym Confluence tylko:

  • w dbt tests
  • w SQL-u obok modeli
  • w CI/CD (nawet prostym)

Test semantyczny, który nie jest automatyczny, jest tylko ładnym życzeniem.

Minimalny setup (bez overengineeringu)

Na start wystarczy:

  • 1-2 testy sensu na kluczową miarę
  • 1 test zakresu
  • 1 test kontraktowy

To i tak jest więcej niż ma 80% zespołów…

Najważniejsza myśl

Testy semantyczne nie chronią danych. One chronią decyzje.

Bo błąd w danych to koszt techniczny. Błąd w semantyce to zła decyzja biznesowa. A to już zupełnie inna liga strat.

Pełny przepływ techniczny: SQL → dbt → Python / DuckDB

Połączmy wszystko w jeden, kompletny przykład. Od surowych danych do gotowego systemu semantycznego.

KROK 1: Raw data (DuckDB / hurtownia)

Mamy tabelki, w nich będą dane:

CREATE TABLE orders (
  order_id INT,
  customer_id INT,
  order_date DATE,
  status VARCHAR
);

CREATE TABLE order_items (
  order_id INT,
  product_id INT,
  quantity INT,
  unit_price DECIMAL(10,2)
);

CREATE TABLE customers (
  customer_id INT,
  signup_date DATE,
  segment VARCHAR
);

CREATE TABLE deliveries (
  order_id INT,
  promised_date DATE,
  delivery_date DATE
);

KROK 2: Staging w dbt

Normalizujemy te dane: models/staging/stg_orders.sql

SELECT
  order_id,
  customer_id,
  CAST(order_date AS DATE) AS order_date,
  UPPER(TRIM(status)) AS status
FROM {{ source('raw', 'orders') }}

Oraz czyścimy: models/staging/stg_order_items.sql

SELECT
  order_id,
  product_id,
  quantity,
  unit_price
FROM {{ source('raw', 'order_items') }}
WHERE quantity > 0    -- co najmniej jeden produkt
  AND unit_price > 0  -- cena raczej dodatnia...

KROK 3: Fakty semantyczne

models/marts/fct_orders.sql


WITH orders AS (
  SELECT * FROM {{ ref('stg_orders') }}
),

order_items AS (
  SELECT * FROM {{ ref('stg_order_items') }}
),

aggregated AS (
  SELECT
    o.order_id,
    o.customer_id,
    o.order_date,
    SUM(oi.quantity * oi.unit_price) AS revenue,
    COUNT(DISTINCT oi.product_id) AS product_count
  FROM orders o
  JOIN order_items oi ON o.order_id = oi.order_id
  WHERE o.status IN ('PAID', 'SHIPPED')
  GROUP BY 1, 2, 3
)

SELECT * FROM aggregated;

models/marts/fct_deliveries.sql


WITH deliveries AS (
  SELECT * FROM {{ ref('stg_deliveries') }}
),

with_flag AS (
  SELECT
    order_id,
    promised_date,
    delivery_date,
    delivery_date <= promised_date AS is_on_time
  FROM deliveries
  WHERE delivery_date IS NOT NULL
)
SELECT * FROM with_flag;

KROK 4: Wymiary

models/marts/dim_customers.sql


WITH customers AS (
  SELECT * FROM {{ ref('stg_customers') }}
),

with_activity AS (
  SELECT
    c.customer_id,
    c.signup_date,
    c.segment,
    MAX(o.order_date) AS last_order_date,
    DATEDIFF('day', MAX(o.order_date), CURRENT_DATE) <= 90 AS is_active
  FROM customers c
  LEFT JOIN {{ ref('fctContinue3:03 PM_orders') }} o ON c.customer_id = o.customer_id
GROUP BY 1, 2, 3
)
SELECT * FROM with_activity;

KROK 5: Metryki w dbt

models/marts/schema.yml


metrics:
  - name: revenue
    label: "Przychód"
    model: ref('fct_orders')
    description: "Przychód z opłaconych i wysłanych zamówień"
    calculation_method: sum
    expression: revenue
    timestamp: order_date
    dimensions:
      - customer_id

  - name: on_time_delivery_rate
    label: "Terminowość dostaw (%)"
    model: ref('fct_deliveries')
    description: "Odsetek dostaw zrealizowanych w obiecany terminie"
    calculation_method: average
    expression: is_on_time
    timestamp: delivery_date

  - name: active_customers
    label: "Aktywni klienci"
    model: ref('dim_customers')
    description: "Liczba klientów z zamówieniem w ostatnich 90 dniach"
    calculation_method: count_distinct
    expression: customer_id
    filters:
      - field: is_active
        operator: '='
        value: 'true'

KROK 6: Python + DuckDB - ta sama semantyka


import duckdb
import pandas as pd
import matplotlib.pyplot as plt

# Połączenie z DuckDB
con = duckdb.connect('warehouse.db')

# Korzystamy z tej samej semantyki co BI
monthly_revenue = con.execute("""
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    SUM(revenue) AS revenue,
    COUNT(DISTINCT customer_id) AS unique_customers
  FROM fct_orders
  GROUP BY 1
  ORDER BY 1
""").df()

# Wizualizacja
plt.figure(figsize=(12, 6))
plt.plot(monthly_revenue['month'], monthly_revenue['revenue'])
plt.title('Miesięczny przychód')
plt.xlabel('Miesiąc')
plt.ylabel('Przychód (PLN)')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# Terminowość dostaw
delivery_stats = con.execute("""
  SELECT
    DATE_TRUNC('week', delivery_date) AS week,
    AVG(is_on_time) AS on_time_rate
  FROM fct_deliveries
  GROUP BY 1
  ORDER BY 1
""").df()

print("Terminowość dostaw (ostatnie 4 tygodnie):")
print(delivery_stats.tail(4))

KROK 7: Ta sama logika w API

Tak, nawet w API możemy (powinniśmy!) zastosować semantykę:


from fastapi import FastAPI
import duckdb

app = FastAPI()
con = duckdb.connect('warehouse.db')

@app.get("/metrics/revenue")
def get_revenue(start_date: str, end_date: str):
    result = con.execute("""
        SELECT 
            SUM(revenue) AS total_revenue,
            COUNT(DISTINCT order_id) AS order_count
        FROM fct_orders
        WHERE order_date BETWEEN ? AND ?
    """, [start_date, end_date]).fetchone()
    
    return {
        "total_revenue": result[0],
        "order_count": result[1]
    }

@app.get("/metrics/delivery-performance")
def get_delivery_performance():
    result = con.execute("""
        SELECT
            AVG(is_on_time) AS on_time_rate
        FROM fct_deliveries
        WHERE delivery_date >= CURRENT_DATE - INTERVAL '30 days'
    """).fetchone()
    
    return {"on_time_rate": result[0]}

Kluczowa obserwacja:

  • Ten sam kod w BI, Pythonie, API
  • Zmiana definicji w jednym miejscu
  • Zero duplikacji logiki

Antywzorce - czyli jak wygląda brak semantyki

Rozpoznajesz któryś z tych scenariuszy? To znak, że potrzebujesz warstwy semantycznej.

1. Revenue_final_v7_POPRAWKA2.xlsx

Excel jako “warstwa decyzyjna”. Każdy dział ma swój arkusz z “prawdziwymi danymi”. Dashboard pokazuje jedno, Excel drugie, zarząd otrzymuje trzecie. Na szczęście mamy już narzędzia pracy grupowej online, ale kiedyś… “A na której wersji to zmieniłeś?” Dlaczego to boli:

  • Brak wersjonowania oraz kolejna wersja pliku binarnego (Excel, Word) nie pozwala na porównanie zmian
  • Nie wiadomo, która wersja jest aktualna
  • Logika ukryta w formułach, które rozumie tylko ich autor

2. “Ten raport jest prawdziwy”

Masz 5 raportów ze “sprzedażą”. Wszyscy twierdzą, że ich raport jest prawdziwy. Każdy ma inną liczbę. Dlaczego to boli:

  • Zarząd nie wie, komu wierzyć
  • Decyzje oparte na intuicji, nie danych
  • Analitycy spędzają 60% czasu na wyjaśnianiu rozbieżności

3. Każdy dashboard ma własne CTE


-- Dashboard A
WITH revenue AS (
  SELECT
    SUM(amount)
  FROM orders
  WHERE
    status = 'PAID'
)

-- Dashboard B  
WITH revenue AS (
  SELECT
    SUM(amount)
  FROM orders
  WHERE
    status IN ('PAID', 'SHIPPED')
)

-- Dashboard C
WITH revenue AS (
  SELECT
    SUM(amount)
  FROM orders
  WHERE
    status != 'CANCELLED'
)

Trzy różne definicje tego samego pojęcia. Dlaczego to boli:

  • Zmiana definicji czym jest “przychód” (z przykładu wyżej) oznacza refactoring 20 dashboardów… Chyba, że masz ich więcej - to wtedy więcej niż 20.
  • Łatwo poprawić w jednym miejscu, zapomnieć w drugim
  • Testy? Jakie testy?

4. Brak właściciela definicji

Nikt nie wie, kto decyduje, co oznacza “aktywny klient” czy “terminowa dostawa”. W efekcie każdy interpretuje po swojemu. Dlaczego to boli:

  • Nie ma kogo zapytać o wątpliwości
  • Brak odpowiedzialności za jakość definicji
  • Chaos przy zmianach

5. Logika w głowach ludzi

“Paweł wie, jak liczyć marżę”. Problem: Paweł jest na urlopie. Albo odszedł z firmy. Dlaczego to boli:

  • Knowledge silosing
  • Onboarding nowych pracowników trwa wieki, a i tak nie opowiesz wszystkiego. Nawet Paweł nie opowie (o ile żyje).

6. Self-service zamienia się w self-destruction

Dałeś użytkownikom dostęp do danych. Teraz każdy tworzy własne raporty z własnymi definicjami. Zarząd dostaje 10 różnych wersji tej samej metryki. Dlaczego to boli:

  • Chaos zamiast empowerment
  • Użytkownicy tracą zaufanie do narzędzi
  • IT wraca do robienia wszystkich raportów ręcznie

Checklista wdrożeniowa - od czego zacząć

Faza 1: Zrozumienie

Do zrobienia:

  • Zidentyfikuj 3-5 najważniejszych KPI w firmie
  • Sprawdź, ile różnych definicji tych KPI aktualnie istnieje
  • Znajdź przykład, gdzie rozbieżności w definicjach spowodowały problem biznesowy
  • Ustal, kto jest właścicielem każdego KPI (biznesowo, nie technicznie)

Nie rób:

  • Nie projektuj całej architektury z góry
  • Nie kupuj drogich narzędzi od razu
  • Nie próbuj standaryzować wszystkiego naraz

Faza 2: MVP

Do zrobienia:

  • Wybierz JEDNO kluczowe KPI (np. Revenue)
  • Stwórz jedną, kanoniczną definicję tego KPI
  • Zaimplementuj w najprostszy możliwy sposób (widok SQL wystarczy)
  • Zaktualizuj 2-3 najważniejsze raporty, żeby z niej korzystały
  • Udokumentuj: co liczy, dlaczego tak, kto zdecydował

Sukces = jeden KPI, jedna prawda, zero sporów o jego wartość

Faza 3: Skalowanie

Do zrobienia:

  • Dodaj kolejne 3-5 KPI
  • Wprowadź proces zmiany definicji (review, dokumentacja, komunikacja)
  • Dodaj podstawowe testy (sensu biznesowego, zakresów)
  • Przenieś logikę do dbt lub podobnego narzędzia
  • Przeprowadź szkolenie dla analityków

Mierniki sukcesu:

  • Czas na wyjaśnienie rozbieżności między raportami: <30% poprzedniego
  • Liczba wersji każdego KPI: dokładnie 1. Wszystkie udokumentowane w jednym miejscu!
  • Zaufanie zarządu do dashboardów: rośnie. A zarząd lubi jak rośnie (najlepsze są te wykresy co mają linię w prawo i do góry)

Faza 4: Dojrzałość

Do zrobienia:

  • Pokrycie testami wszystkich kluczowych miar
  • Data catalog z opisami wszystkich definicji
  • Self-service BI oparte o warstwę semantyczną
  • Metryki jakości samej warstwy semantycznej
  • Regularne review definicji (np. kwartalnie, najlepiej z właścicielami KPI)

Czego NIE robić

Overengineering na starcie:

  • Nie buduj hurtowni danych, żeby zacząć z semantyką
  • Nie twórz 50 miar “na zapas”
  • Nie wprowadzaj skomplikowanych procesów governance

Ignorowanie ludzi:

  • Nie narzucaj definicji bez konsultacji z biznesem
  • Nie zapomnij o szkoleniach
  • Nie traktuj tego jako “projektu IT” - to jest ciągła zmiana i ulepszanie produktu

Brak dokumentacji:

  • Nie zostawiaj definicji tylko w kodzie
  • Nie dokumentuj “kiedyś później”
  • Nie zakładaj, że “to oczywiste” (pamiętasz, że kwadrans temu padały słowa “jakbyś miał tłumaczyć uczniowi średniej szkoły”? nadal obowiązują)

Red flags - kiedy przestać i przemyśleć

  • Jeśli implementacja trwa ponad 3 miesiące bez żadnej wartości
  • Jeśli nikt poza Tobą nie rozumie, co robisz
  • Jeśli biznes nie widzi różnicy
  • Jeśli dodajesz kolejną warstwę pośrednią zamiast upraszczać

Finałowa myśl

Jeśli:

  • każdy raport liczy coś “po swojemu”
  • BI jest szybkie, ale nikt mu nie ufa
  • zarząd pyta “która liczba jest prawdziwa?”
  • analitycy spędzają więcej czasu na wyjaśnianiu rozbieżności niż na analizie

…to problemem nie są dane. Problemem jest brak warstwy semantycznej.

Warstwa semantyczna nie służy do przechowywania danych. Ona służy do przechowywania sensu.

To nie jest luksus ani moda. To moment, w którym dane przestają być techniczne, a zaczynają być narzędziem podejmowania decyzji. Jeśli dane mają wspierać decyzje, a nie wieczne dyskusje “dlaczego liczby się nie zgadzają”, to prędzej czy później i tak do tego dojdziesz. Pytanie tylko: czy świadomie - czy po trzecim kryzysie na zarządzie? I nie, nie da się tego naprawić jednym dashboardem final_v7_POPRAWKA2_z_rana2_final.