Adrienne Vermorel

Export GA4 BigQuery : la référence complète du schéma

Partie 1 de la série GA4 + BigQuery pour analytics engineers


L’export GA4 vers BigQuery délivre des données brutes, non échantillonnées, au niveau événement, directement dans votre data warehouse. Plus d’avertissements d’échantillonnage. Plus de regroupement de dimensions en “(other)”. Un accès SQL complet à chaque clic, scroll et achat.

Mais la réalité est plus complexe. Vous rencontrerez des tableaux imbriqués nécessitant des acrobaties UNNEST, trois structures de source de trafic différentes qui se remplissent selon des conditions distinctes, des modifications de schéma non documentées qui cassent les requêtes de production, et des subtilités que la documentation Google passe complètement sous silence.

Cette référence est le compagnon pratique de la documentation officielle. C’est le guide de terrain que j’aurais aimé avoir quand j’ai ouvert une table events_ pour la première fois en me demandant pourquoi mes comptes de sessions étaient faux. Que vous configuriez votre premier export ou que vous déboguiez un pipeline qui tourne depuis des années, ce guide couvre ce que vous devez vraiment savoir.


Ce qui est exporté

Quand vous liez une propriété GA4 à BigQuery, un dataset nommé analytics_<property_id> apparaît dans votre projet. À l’intérieur, vous trouverez des tables shardées par date (pas des tables partitionnées) qui capturent chaque événement transitant par votre propriété.

Les quatre types de tables

TableObjectifLimitation principale
events_YYYYMMDDExport quotidien, votre source de véritéLatence de 10+ heures
events_intraday_YYYYMMDDDonnées streaming quasi temps réelChamps d’attribution manquants
pseudonymous_users_YYYYMMDDExport utilisateur clé par device IDOptionnel, à activer séparément
users_YYYYMMDDExport utilisateur clé par user_id personnaliséOptionnel, à activer séparément

Les tables quotidiennes (events_YYYYMMDD) contiennent des données entièrement traitées avec une attribution utilisateur complète. Elles arrivent généralement en milieu d’après-midi dans le fuseau horaire de votre propriété, bien qu’il n’y ait pas de SLA garanti pour les propriétés standard. Ces tables continuent de se mettre à jour pendant 72 heures pour capturer les événements arrivant en retard (les données d’applications mobiles et les hits du Measurement Protocol arrivent souvent avec du délai).

Les tables streaming (events_intraday_YYYYMMDD) fournissent des données en quelques minutes après l’occurrence de l’événement. Elles sont supprimées automatiquement une fois que la table quotidienne correspondante est complète. Limitation critique : les champs traffic_source, user_ltv et is_active_user ne sont jamais renseignés dans les tables intraday.

Les tables utilisateur exportent une ligne par identifiant utilisateur avec les appartenances aux audiences, les métriques de valeur vie et les scores prédictifs. La table pseudonymous_users_ utilise user_pseudo_id (device ID) comme clé, tandis que users_ utilise votre user_id personnalisé. Les deux nécessitent une activation explicite dans les paramètres de liaison BigQuery.

Quotidien vs Streaming : quand utiliser lequel

timeline
title Chronologie d'export GA4 (Jour N)
section Jour N
Événements se produisent : Activité utilisateur tout au long de la journée
Intraday disponible : En quelques minutes après chaque événement
section Jour N+1
Export quotidien terminé : ~10-16 heures après minuit
Table intraday supprimée : Après finalisation de l'export quotidien
section Jour N+3
Fenêtre de données tardives se ferme : 72 heures pour les retardataires

Le choix n’est pas l’un ou l’autre. Activez les deux exports et utilisez-les à des fins différentes.

Les exports quotidiens sont votre source de vérité pour le reporting. Ils incluent une attribution utilisateur complète et une complétude des données garantie (après la fenêtre de 72 heures). Le compromis est la latence : vous ne verrez pas les données d’hier avant le milieu de l’après-midi aujourd’hui.

Les exports streaming permettent le monitoring du jour même et les tableaux de bord temps réel. Attendez-vous à un écart de 0,5-2% entre l’intraday et les données quotidiennes finales, avec certains cas limites montrant jusqu’à 20% de différences. N’utilisez pas les tables intraday pour le reporting final.

Le coût diffère significativement. Le chargement batch quotidien est gratuit — vous ne payez que le stockage et les requêtes. L’ingestion streaming coûte 0,05 $ par Go, soit environ 600 000 événements. Un site avec 1 million d’événements quotidiens paie environ 1,50 $/mois pour le streaming.

Les propriétés standard font face à une limite de 1 million d’événements par jour sur les exports quotidiens (pas de limite sur le streaming). Les propriétés GA4 360 peuvent exporter jusqu’à 20 milliards d’événements quotidiennement.


Le schéma de la table events

Chaque ligne dans la table events représente un seul événement. C’est le changement fondamental par rapport à Universal Analytics, où une ligne représentait une session avec des événements imbriqués à l’intérieur. Comprendre ce modèle centré sur l’événement est essentiel.

events_YYYYMMDD (une ligne = un événement)
├── Champs de premier niveau
│ ├── event_name, event_timestamp, event_date
│ ├── user_pseudo_id, user_id
│ └── platform, stream_id
├── RECORDs imbriqués (valeurs uniques)
│ ├── device.category, device.browser, device.os...
│ ├── geo.country, geo.city, geo.region...
│ ├── traffic_source.source, .medium, .name
│ ├── collected_traffic_source.manual_source...
│ └── privacy_info.analytics_storage...
└── RECORDs REPEATED (tableaux, nécessitent UNNEST)
├── event_params[] ─── key + value.string_value/int_value/double_value
├── user_properties[] ─── key + value + set_timestamp_micros
└── items[] ─── item_id, item_name, price, quantity...
└── items.item_params[] (tableau imbriqué)

Champs événement principaux

Les champs fondamentaux apparaissent au premier niveau de chaque ligne :

ChampTypeDescription
event_nameSTRINGNom de l’événement (page_view, purchase, événements personnalisés)
event_timestampINTEGERHeure UTC en microsecondes depuis l’epoch Unix
event_dateSTRINGDate dans le fuseau horaire de la propriété (format YYYYMMDD)
event_value_in_usdFLOATValeur convertie en devise quand l’événement inclut le paramètre value
event_bundle_sequence_idINTEGERSéquence dans le bundle d’upload

Ajoutés en juillet 2024, trois champs d’ordonnancement de batch résolvent un problème de longue date : les événements groupés ensemble partageaient auparavant des timestamps identiques sans moyen de déterminer leur séquence originale.

ChampObjectif
batch_event_indexOrdre dans le batch
batch_page_idRegroupe les événements de la même page
batch_ordering_idIdentifiant d’ordonnancement unique

La structure imbriquée event_params

La plupart de vos données vivent ici. Le champ event_params est un REPEATED RECORD, ce qui signifie que c’est un tableau de paires clé-valeur attaché à chaque événement.

Chaque élément contient une key (STRING) et un RECORD value avec quatre champs possibles :

Champ valueTypeUtilisé pour
string_valueSTRINGValeurs texte, URLs, catégories
int_valueINTEGERCompteurs, IDs, flags booléens (0/1)
float_valueFLOATActuellement non utilisé par GA4
double_valueFLOATValeurs décimales, prix

GA4 détecte automatiquement quel champ utiliser. Le ga_session_id atterrit dans int_value. Le page_location atterrit dans string_value. Les paramètres personnalisés peuvent atterrir n’importe où selon les valeurs que vous envoyez.

Paramètres courants que vous extrairez fréquemment :

ParamètreChamp valueDescription
ga_session_idint_valueIdentifiant de session (timestamp Unix du début de session)
ga_session_numberint_valueNombre de sessions pour cet utilisateur
page_locationstring_valueURL complète de la page
page_titlestring_valueTitre de la page
page_referrerstring_valueURL de la page précédente
sourcestring_valueSource de trafic
mediumstring_valueMedium de trafic
campaignstring_valueNom de campagne
engaged_session_eventint_value1 si la session est engagée, 0 sinon

Pour extraire un paramètre, vous utiliserez UNNEST avec une sous-requête :

SELECT
event_name,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page_location,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS session_id
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = '20260112'

Nous couvrirons les patterns UNNEST en profondeur dans la Partie 2 de cette série.

Identification utilisateur

Trois champs gèrent l’identité utilisateur au premier niveau :

ChampDescription
user_pseudo_idIdentifiant device/navigateur (la valeur du cookie _ga sur le web, Firebase Instance ID sur les apps)
user_idVotre identifiant personnalisé, défini via gtag('set', 'user_id', '...') ou équivalent
user_first_touch_timestampTimestamp en microsecondes de la première interaction de l’utilisateur avec votre propriété

Le REPEATED RECORD user_properties reprend la structure de event_params, stockant les propriétés utilisateur définies via setUserProperty. Contrairement aux paramètres d’événement, les propriétés utilisateur persistent à travers les événements et incluent un champ set_timestamp_micros qui trace quand chacune a été mise à jour pour la dernière fois.

Device, géographie et plateforme

Le RECORD device contient les détails matériel et logiciel :

ChampValeurs exemples
device.categorymobile, tablet, desktop
device.operating_systemAndroid, iOS, Windows, Macintosh
device.operating_system_version14.0, 10
device.browserChrome, Safari, Firefox
device.browser_version120.0.6099.109
device.mobile_brand_nameApple, Samsung, Google
device.mobile_model_nameiPhone, Pixel 7

Le RECORD geo fournit la localisation basée sur l’IP :

ChampDescription
geo.continentAmericas, Europe, Asia
geo.countryUnited States, France, Japan
geo.regionCalifornia, Île-de-France
geo.citySan Francisco, Paris
geo.metroRégion DMA (US uniquement)

Le champ platform (STRING) indique web, iOS ou Android. Le stream_id identifie quel flux de données a généré l’événement.

Champs de source de trafic : trois structures, trois objectifs

GA4 exporte trois structures de source de trafic différentes, chacune servant un objectif d’attribution distinct :

flowchart LR
subgraph ts["traffic_source"]
ts_when["Quand : Une fois par vie utilisateur"]
ts_what["Attribution : First touch"]
ts_note["⚠️ Jamais dans les tables intraday"]
end
subgraph cts["collected_traffic_source"]
cts_when["Quand : Chaque événement avec UTMs"]
cts_what["Attribution : Niveau événement"]
cts_note["Contient : gclid, dclid, manual_*"]
end
subgraph stslc["session_traffic_source_last_click"]
stslc_when["Quand : Par session"]
stslc_what["Attribution : Last click"]
stslc_note["Ajouté : Juillet 2024"]
end
user((Utilisateur arrive)) --> ts
event((Événement se déclenche)) --> cts
session((Session commence)) --> stslc

traffic_source (RECORD) : Attribution first-touch montrant comment l’utilisateur a été acquis initialement. Les valeurs ne changent jamais après la première définition. Contient source, medium, name (campagne). Non renseigné dans les tables intraday.

collected_traffic_source (RECORD) : Paramètres UTM au niveau événement et click IDs capturés au moment de l’événement. Contient manual_source, manual_medium, manual_campaign_name, plus les click IDs comme gclid, dclid et srsltid.

session_traffic_source_last_click (RECORD) : Ajouté en juillet 2024. Attribution last-click complète avec des records imbriqués pour les données de campagne spécifiques aux plateformes : google_ads_campaign, sa360_campaign, dv360_campaign, cm360_campaign et manual_campaign.

Quand vous construisez des modèles d’attribution, comprenez quel champ répond à votre question. “Comment avons-nous acquis cet utilisateur ?” → traffic_source. “Quelle campagne était active quand cet événement spécifique s’est déclenché ?” → collected_traffic_source. “Qu’est-ce qui a conduit cette session ?” → session_traffic_source_last_click.

Structures e-commerce

Le RECORD ecommerce stocke les données au niveau transaction :

ChampDescription
ecommerce.transaction_idID de commande
ecommerce.purchase_revenueRevenu en devise locale
ecommerce.purchase_revenue_in_usdRevenu converti en USD
ecommerce.shipping_valueCoût de livraison
ecommerce.tax_valueMontant des taxes
ecommerce.total_item_quantityTotal d’articles dans la transaction

Le REPEATED RECORD items contient une entrée par produit :

ChampDescription
items.item_idSKU du produit
items.item_nameNom du produit
items.item_brandMarque
items.item_category jusqu’à item_category5Hiérarchie de catégories
items.pricePrix unitaire
items.quantityQuantité
items.couponCoupon appliqué
items.item_paramsParamètres produit personnalisés (REPEATED RECORD)

Le champ items.item_params (ajouté en octobre 2023) est lui-même un REPEATED RECORD imbriqué dans items, permettant des dimensions produit personnalisées.

Confidentialité et consentement

Le RECORD privacy_info capture l’état du consentement :

ChampValeursDescription
privacy_info.ads_storageYes, No, UnsetConsentement aux cookies publicitaires
privacy_info.analytics_storageYes, No, UnsetConsentement aux cookies analytics
privacy_info.uses_transient_tokenTRUE/FALSEUtilisation de la mesure sans cookies

Quand analytics_storage est refusé sous Consent Mode, les événements sont toujours collectés mais user_pseudo_id et ga_session_id sont supprimés. Cela crée des lignes orphelines qui ne peuvent pas être attribuées à des utilisateurs ou des sessions.


Pièges critiques

Le problème de type event_params

GA4 détecte automatiquement les types de données des paramètres, stockant les valeurs dans celui des quatre champs value qui correspond. Les paramètres personnalisés peuvent atterrir dans différents champs selon les valeurs envoyées. Envoyez “123” comme string une fois et ça va dans string_value ; envoyez 123 comme nombre et ça va dans int_value.

Quand vous n’êtes pas sûr du type d’un paramètre, utilisez COALESCE :

SELECT
COALESCE(
value.string_value,
CAST(value.int_value AS STRING),
CAST(value.double_value AS STRING)
) AS param_value
FROM UNNEST(event_params)
WHERE key = 'custom_param'

Les événements session_start ne sont pas fiables

⚠️ Attention : Ne comptez pas les événements session_start pour calculer les sessions.

L’événement session_start est évalué côté client, déclenché quand un événement inclut le paramètre _ss. Cela cause des événements session_start en double au sein de sessions uniques et des événements manquants dans d’autres. Le problème est particulièrement aigu pour les sous-propriétés où le premier événement de la session peut se produire avant l’application des filtres.

L’interface GA4 elle-même n’utilise plus session_start pour sa métrique de sessions. Elle estime à partir des IDs de session uniques à la place. Faites de même :

-- Correct : compter les IDs de session distincts
SELECT
COUNT(DISTINCT CONCAT(
user_pseudo_id,
CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING)
)) AS sessions
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260112'

ga_session_id n’est pas unique entre utilisateurs

Le ga_session_id est simplement le timestamp Unix de quand la session a commencé. Plusieurs utilisateurs démarrant des sessions à la même seconde partagent des valeurs identiques. Concaténez toujours avec user_pseudo_id pour créer des identifiants de session vraiment uniques.

Pièges de fuseau horaire

Trois contextes de fuseau horaire différents coexistent :

ChampFuseau horaire
event_timestampUTC (microsecondes)
event_dateFuseau horaire de la propriété (string YYYYMMDD)
_TABLE_SUFFIXPacific Time

Conversion vers votre fuseau horaire local :

SELECT
DATETIME(TIMESTAMP_MICROS(event_timestamp), 'Europe/Paris') AS local_time
FROM `project.analytics_123456789.events_*`

Quand les utilisateurs refusent le consentement analytics_storage, Advanced Consent Mode collecte toujours les événements comme “pings sans cookies” mais supprime user_pseudo_id et ga_session_id. Ces lignes orphelines gonflent les comptages d’événements tout en étant impossibles à attribuer.

Si le consentement est accordé plus tard sur la même page, GA4 “recoud” le consentement aux hits précédemment refusés. Vous verrez cela comme user_pseudo_id devenant renseigné sur des événements qui en manquaient précédemment.

Les données BigQuery diffèrent de l’interface GA4

Attendez-vous à des écarts. Les exports BigQuery excluent :

  • Les conversions modélisées du Consent Mode
  • La déduplication utilisateur de Google Signals
  • La modélisation comportementale pour les données manquantes

L’interface utilise l’approximation HyperLogLog++ pour les grandes cardinalités tandis que BigQuery fournit des comptages exacts. Les dimensions à haute cardinalité montrant “(other)” dans l’interface apparaissent avec tous les détails dans BigQuery.

Le piège des données fournies par l’utilisateur

⚠️ Avertissement critique : Activer la fonctionnalité “User-provided data” dans l’admin GA4 désactive définitivement l’export de user_id vers BigQuery sans option de retour en arrière.

Si vos pipelines dépendent de user_id dans BigQuery, vérifiez ce paramètre avant que quiconque ne l’active.


Évolution du schéma et comparaison avec UA

Comment le schéma a évolué

L’export BigQuery de GA4 a été lancé vers 2019 sous le nom “App+Web”, héritant sa structure du schéma d’export de Firebase. Google ne fournit pas de changelog officiel, donc les praticiens doivent surveiller les requêtes INFORMATION_SCHEMA pour détecter les ajouts.

Ajouts majeurs au schéma par date approximative :

PériodeAjout
Mars 2020RECORD ecommerce
Juin 2021privacy_info pour Consent Mode
Mai 2023collected_traffic_source pour l’attribution au niveau événement
Juillet 2023Champ is_active_user
Octobre 2023RECORD imbriqué items.item_params
Juillet 2024Champs de séquençage de batch, session_traffic_source_last_click

Les nouveaux champs ne sont jamais rétroactifs. Les tables historiques ne contiendront pas les champs ajoutés après leur date d’export.

Différences clés avec Universal Analytics

Si vous migrez depuis UA, le changement de modèle mental est significatif :

AspectUniversal AnalyticsGA4
Unité de baseSessionÉvénement
Nom de tablega_sessions_YYYYMMDDevents_YYYYMMDD
La ligne représenteUne sessionUn événement
ID de sessionvisitId (champ direct)ga_session_id (nécessite UNNEST)
ID utilisateurfullVisitorIduser_pseudo_id
Données de pagehits.page.pagePathpage_location dans event_params
Dimensions personnaliséesTableau indexé (dimension1, dimension2)Paires clé-valeur
DisponibilitéGA360 uniquementToutes les propriétés

Ce qui a complètement disparu : hits.isExit, métriques de session pré-calculées (totals.bounces, totals.pageviews), dimensions personnalisées au scope session (GA4 ne supporte que les scopes utilisateur et événement).

Réalité de la migration : Les données UA et GA4 ne peuvent pas être combinées directement. La différence fondamentale de modèle signifie que les données historiques UA doivent rester dans des tables séparées. Toute analyse unifiée nécessite des couches de transformation personnalisées qui mappent les concepts entre les deux schémas.


Requêter efficacement

Filtrez toujours _TABLE_SUFFIX

GA4 utilise des tables shardées par date, pas des tables partitionnées. Sans filtre _TABLE_SUFFIX, vous scannerez tout votre dataset, potentiellement des années de données.

-- Requêter les 30 derniers jours
SELECT
event_name,
COUNT(*) AS events
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN
FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
GROUP BY event_name

UNNEST inline vs CROSS JOIN UNNEST

Deux patterns pour travailler avec les données imbriquées :

UNNEST inline extrait des paramètres spécifiques tout en maintenant une ligne par événement. Utilisez ceci pour la plupart des requêtes :

SELECT
event_name,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page_location
FROM `project.analytics_123456789.events_*`

CROSS JOIN UNNEST étend les lignes, créant une ligne par paramètre. Utilisez pour l’exploration :

SELECT
ep.key,
COUNT(*) AS occurrences
FROM `project.analytics_123456789.events_*`,
UNNEST(event_params) AS ep
GROUP BY ep.key
ORDER BY occurrences DESC

Macros helper réutilisables

Réduisez la répétition avec des macros dbt :

-- macros/ga4_param_string.sql
{% macro ga4_param_string(column, param_name) %}
(SELECT value.string_value FROM UNNEST({{ column }}) WHERE key = '{{ param_name }}')
{% endmacro %}
-- macros/ga4_param_int.sql
{% macro ga4_param_int(column, param_name) %}
(SELECT value.int_value FROM UNNEST({{ column }}) WHERE key = '{{ param_name }}')
{% endmacro %}

Utilisation dans un modèle dbt :

SELECT
{{ ga4_param_int('event_params', 'ga_session_id') }} AS session_id,
{{ ga4_param_string('event_params', 'page_location') }} AS page_location
FROM {{ source('ga4', 'events') }}
WHERE _TABLE_SUFFIX = '20260112'

Checklist de contrôle des coûts

  • Sélectionnez uniquement les colonnes nécessaires : évitez SELECT * sur les tables avec structures imbriquées
  • Lancez d’abord des dry runs : estimez le coût avant d’exécuter de grandes requêtes
  • Matérialisez les agrégations fréquentes : requêtes planifiées créant des tables de résumé
  • Utilisez APPROX_COUNT_DISTINCT : quand la précision exacte n’est pas requise pour les comptages utilisateur

Points clés à retenir

L’export BigQuery de GA4 fournit un accès aux données analytics brutes, mais son modèle centré sur l’événement et ses structures imbriquées nécessitent une approche délibérée :

  1. Activez les exports quotidien et streaming. Utilisez le quotidien comme source de vérité, le streaming pour le monitoring du jour même.
  2. Maîtrisez le pattern UNNEST inline. Vous l’utiliserez constamment pour l’extraction de paramètres.
  3. Combinez toujours user_pseudo_id + ga_session_id pour l’analyse de session. Aucun des deux champs n’est unique seul.
  4. Ne comptez jamais les événements session_start pour les métriques de session. Comptez les IDs de session distincts à la place.
  5. Traitez le schéma comme une cible mouvante. Surveillez les ajouts de champs, testez les requêtes après les releases majeures de GA4.

Suite de cette série

Maintenant que vous comprenez le schéma, le vrai travail commence : aplatir ces structures imbriquées efficacement. Partie 2 : Unnester les événements GA4 couvre les patterns pour chaque cas d’usage, de l’extraction simple de paramètres à l’unnesting multi-niveaux complexe pour les articles e-commerce avec dimensions personnalisées.