I team di marketing non utilizzano più i report predefiniti di Google Analytics, ma pipeline dati che scrivono e controllano direttamente. Nel 2026, il data stack moderno per il marketing si compone di tre livelli: sorgenti raw, modeling layer, semantic layer. Questo articolo spiega come costruire questi tre livelli con dbt + BigQuery, quali errori si commettono a ogni stadio e come strutturare un'architettura sostenibile in produzione.
Source mapping: caricare dati in BigQuery non basta
Hai caricato GA4, Meta Ads, sGTM event in BigQuery — ma è solo l'inizio. Source mapping significa trasformare le tabelle raw in contract significativi. In dbt, le definizioni di source vivono in file .yml:
sources:
- name: raw_ga4
database: roibase-prod
schema: analytics_123456789
tables:
- name: events_*
identifier: events_*
loaded_at_field: event_timestamp
freshness:
warn_after: {count: 12, period: hour}
Questa definizione realizza tre cose: (1) Data lineage — quale modello utilizza quale tabella raw, (2) Freshness check — se l'ultimo event è più vecchio di 12 ore, un alert viene generato, (3) Contract — se manca la colonna event_timestamp, il build fallisce.
L'errore più comune: usare lo schema raw così com'è. Scrivere SQL senza flatten l'array event_params di GA4, ogni query diventa 200+ righe. La logica di unnest deve vivere in un'unica posizione durante il source mapping:
-- models/staging/stg_ga4_events.sql
with source as (
select * from {{ source('raw_ga4', 'events_*') }}
),
flattened as (
select
event_date,
event_timestamp,
user_pseudo_id,
(select value.string_value from unnest(event_params) where key = 'session_id') as session_id,
(select value.int_value from unnest(event_params) where key = 'ga_session_number') as session_number
from source
)
select * from flattened
Questo modello viene ora richiamato downstream come ref('stg_ga4_events') — la sintassi raw di event_params è isolata upstream. Il freshness check gira ogni giorno, errori di schema modification vengono catturati automaticamente.
Modeling layer: definisci la metrica una volta, usala cento volte
Dopo il layer di staging, viene il modeling layer. Qui si separano i modelli intermedi (business logic) dai mart models (aggregazione). Nel data stack di marketing, il modello critico è il join session → transaction:
-- models/marts/mrt_session_metrics.sql
with sessions as (
select * from {{ ref('int_sessions') }}
),
transactions as (
select * from {{ ref('int_transactions') }}
),
joined as (
select
s.session_id,
s.session_date,
s.traffic_source,
s.medium,
s.campaign,
t.transaction_id,
t.revenue,
t.transaction_timestamp
from sessions s
left join transactions t
on s.session_id = t.session_id
)
select
session_date,
traffic_source,
medium,
campaign,
count(distinct session_id) as sessions,
count(distinct transaction_id) as transactions,
sum(revenue) as total_revenue,
safe_divide(count(distinct transaction_id), count(distinct session_id)) as conversion_rate
from joined
group by 1, 2, 3, 4
Questo modello gira ogni giorno alle 03:00 (dbt Cloud scheduler), Looker Studio si connette direttamente a questa tabella. Quando è necessario fare modifiche, cambi l'SQL in un'unica posizione e tutti i dashboard si aggiornano automaticamente.
Dettaglio importante: l'uso di safe_divide — se sessions = 0, non genera un errore di divisione per zero, ma ritorna null. La gestione delle eccezioni in una production pipeline avviene a questo livello.
dbt tests: verifica della qualità dati automatica
Mentre definisci le metriche nel modeling layer, scrivi anche i test:
# models/marts/schema.yml
models:
- name: mrt_session_metrics
columns:
- name: session_date
tests:
- not_null
- name: sessions
tests:
- not_null
- dbt_utils.expression_is_true:
expression: ">= 0"
- name: conversion_rate
tests:
- dbt_utils.expression_is_true:
expression: "<= 1"
Il comando dbt test esegue queste regole. Se il conversion rate > 1 (significa che c'è un errore SQL), il build fallisce e un alert viene inviato a Slack. Invece di QA manuale, ottieni controllo di qualità dei dati automatizzato — il resto dello stack dati è costruito su questa base solida.
Semantic layer: definisci la metrica, non la query
Con dbt v1.6+, il semantic layer è uscito dalla fase beta. Ora definisci la metrica in un file .yml, non in SQL:
# models/semantic/metrics.yml
metrics:
- name: total_revenue
label: Total Revenue
model: ref('mrt_session_metrics')
type: sum
sql: total_revenue
timestamp: session_date
time_grains: [day, week, month]
- name: roas
label: Return on Ad Spend
type: ratio
numerator: total_revenue
denominator: total_ad_spend
Questa definizione viene utilizzata in tre contesti: (1) Looker Studio, (2) Slack bot che interroga la dbt Cloud discovery API per metriche, (3) DAG Airflow con input a pipeline ML downstream.
Il vantaggio: le metriche sono consumabili senza scrivere SQL. L'analyst di marketing non conosce il SQL e scrive "Show me ROAS by campaign, last 7 days" — il semantic layer di dbt compila automaticamente la query. La logica SQL sta nel modeling layer, la definizione della metrica nel semantic layer — sono separate e i cambiamenti sono isolati.
Attenzione: il semantic layer è ancora nuovo — non tutti gli strumenti BI hanno integrazione nativa. In Roibase usiamo un approccio ibrido: le metriche critiche nel semantic layer, gli analytics personalizzati tramite SQL exposure.
Exposures: documenta le dipendenze downstream
Gli exposure mostrano dove un modello dbt viene utilizzato al di fuori:
# models/exposures.yml
exposures:
- name: looker_studio_performance_dashboard
type: dashboard
url: https://lookerstudio.google.com/...
depends_on:
- ref('mrt_session_metrics')
- ref('mrt_campaign_performance')
owner:
name: Marketing Analytics Team
email: [email protected]
Questa definizione è visualizzata nella documentazione dbt — quale dashboard dipende da quale modello, chi va notificato se il modello cambia. In produzione, quando fai un breaking change nello schema, esegui dbt run --select +mrt_session_metrics+ e vedi tutti gli impatti downstream.
Scenario reale: il parametro GA4 page_location è diventato page_url. Grazie agli exposure, abbiamo identificato 3 dashboard e 1 DAG Airflow interessati — la migrazione è stata completata in 2 ore. Senza exposure, i dashboard si sarebbero rotti silenziosamente, lo avremmo scoperto dai reclami dei utenti.
Incremental models: non fare un rebuild completo di 2TB ogni giorno
Nel data di marketing, le partizioni daily raggiungono terabyte. Non puoi fare un refresh completo ad ogni comando dbt run — il costo BigQuery e il tempo sono inaccettabili. Usi i modelli incrementali:
-- models/marts/mrt_user_journey.sql
{{
config(
materialized='incremental',
partition_by={'field': 'event_date', 'data_type': 'date'},
cluster_by=['user_pseudo_id', 'traffic_source'],
incremental_strategy='insert_overwrite'
)
}}
select
event_date,
user_pseudo_id,
traffic_source,
-- ...
from {{ ref('stg_ga4_events') }}
{% if is_incremental() %}
where event_date >= date_sub(current_date(), interval 3 day)
{% endif %}
Questa configurazione fa tre cose: (1) Crea partition in BigQuery — aggiunge il nuovo giorno senza toccare quelli precedenti, (2) La clausola cluster_by migliora le performance delle query, (3) La strategia insert_overwrite — elimina gli ultimi 3 giorni e li riscrive (per gestire late arriving data).
Differenza di costo: 365 giorni di dati, refresh completo = 2,5 TB scan ($12,50), incremental = 3 GB scan ($0,015). Per una pipeline che gira quotidianamente, la differenza annua è ~$4500 vs ~$5. Per questo i modelli incrementali sono la base dello stack dati in produzione.
Connettere lo stack dati al processo decisionale
dbt + BigQuery costruiscono l'infrastruttura, ma il valore reale è nell'impatto sulle decisioni di marketing. Un flusso tipico dal semantic layer a Slack:
- Il manager marketing scrive in Slack:
/metric roas last_30_days campaign=brand - L'app Slack chiama l'API del semantic layer di dbt Cloud
- L'API interroga la tabella
mrt_session_metricse calcola il ROAS - Il risultato torna a Slack: "Campagna brand ROAS: 4.2x"
Questo flusso richiede il semantic layer dbt + un middleware Python personalizzato. Nello stack di produzione Roibase, un DAG Airflow cattura uno snapshot giornaliero dal semantic layer — Looker Studio e le app interne usano questo snapshot, evitando problemi di API rate limit.
Approccio alternativo: nello stack ibrido che utilizziamo nel nostro servizio Dati First-Party & Architettura di Misurazione, usiamo dbt semantic layer + Cube.js. Cube.js aggiunge un layer di caching, migliorando la performance della BI. La scelta dipende dal volume dei dati e dal pattern di query.
Production checklist: prima di deployare lo stack dbt
dbt funziona in locale — prima di passare in produzione, esegui questi controlli:
- CI/CD: con dbt Cloud o GitHub Actions, esegui
dbt build --select state:modified+ad ogni commit - Freshness monitoring: per le sorgenti critiche, definisci
warn_aftereerror_after - Alerting: integra dbt Cloud webhook con Slack — se il build fallisce, il team è notificato in 5 minuti
- Documentazione:
dbt docs generatedeve girare automaticamente, gli artifact vanno pushed su S3/GCS - Cost monitoring: usa BigQuery slot reservation o attiva alert sui costi on-demand — imposta una soglia di $500/day per spike inaspettati
- Backup strategy: mantieni una snapshot table nel data warehouse di produzione — se un modello viene aggiornato male, puoi fare rollback
Regola critica: in produzione, nessun dbt run manuale. Tutta l'esecuzione deve passare attraverso uno scheduler (dbt Cloud, Airflow, Prefect). L'esecuzione manuale rompe la data lineage — se c'è un errore, il root cause analysis diventa impossibile.
dbt + BigQuery è la spina dorsale dello stack di marketing moderno — con source mapping hai collegato i dati raw a contract significativi, il modeling layer ti permette di definire la metrica una sola volta, il semantic layer consente anche a chi non conosce SQL di consumare metriche. In produzione, i modelli incrementali e il coverage dei test rendono la pipeline sostenibile. Il prossimo passo è collegare questi dati all'attivazione real-time — CDP, audience sync, misurazione dell'incrementalità. Ma questo è un'altra discussione su data stack.