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
| Table | Objectif | Limitation principale |
|---|---|---|
events_YYYYMMDD | Export quotidien, votre source de vérité | Latence de 10+ heures |
events_intraday_YYYYMMDD | Données streaming quasi temps réel | Champs d’attribution manquants |
pseudonymous_users_YYYYMMDD | Export utilisateur clé par device ID | Optionnel, à activer séparément |
users_YYYYMMDD | Export 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 retardatairesLe 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 :
| Champ | Type | Description |
|---|---|---|
event_name | STRING | Nom de l’événement (page_view, purchase, événements personnalisés) |
event_timestamp | INTEGER | Heure UTC en microsecondes depuis l’epoch Unix |
event_date | STRING | Date dans le fuseau horaire de la propriété (format YYYYMMDD) |
event_value_in_usd | FLOAT | Valeur convertie en devise quand l’événement inclut le paramètre value |
event_bundle_sequence_id | INTEGER | Sé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.
| Champ | Objectif |
|---|---|
batch_event_index | Ordre dans le batch |
batch_page_id | Regroupe les événements de la même page |
batch_ordering_id | Identifiant 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 value | Type | Utilisé pour |
|---|---|---|
string_value | STRING | Valeurs texte, URLs, catégories |
int_value | INTEGER | Compteurs, IDs, flags booléens (0/1) |
float_value | FLOAT | Actuellement non utilisé par GA4 |
double_value | FLOAT | Valeurs 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ètre | Champ value | Description |
|---|---|---|
ga_session_id | int_value | Identifiant de session (timestamp Unix du début de session) |
ga_session_number | int_value | Nombre de sessions pour cet utilisateur |
page_location | string_value | URL complète de la page |
page_title | string_value | Titre de la page |
page_referrer | string_value | URL de la page précédente |
source | string_value | Source de trafic |
medium | string_value | Medium de trafic |
campaign | string_value | Nom de campagne |
engaged_session_event | int_value | 1 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_idFROM `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 :
| Champ | Description |
|---|---|
user_pseudo_id | Identifiant device/navigateur (la valeur du cookie _ga sur le web, Firebase Instance ID sur les apps) |
user_id | Votre identifiant personnalisé, défini via gtag('set', 'user_id', '...') ou équivalent |
user_first_touch_timestamp | Timestamp 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 :
| Champ | Valeurs exemples |
|---|---|
device.category | mobile, tablet, desktop |
device.operating_system | Android, iOS, Windows, Macintosh |
device.operating_system_version | 14.0, 10 |
device.browser | Chrome, Safari, Firefox |
device.browser_version | 120.0.6099.109 |
device.mobile_brand_name | Apple, Samsung, Google |
device.mobile_model_name | iPhone, Pixel 7 |
Le RECORD geo fournit la localisation basée sur l’IP :
| Champ | Description |
|---|---|
geo.continent | Americas, Europe, Asia |
geo.country | United States, France, Japan |
geo.region | California, Île-de-France |
geo.city | San Francisco, Paris |
geo.metro | Ré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)) --> stslctraffic_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 :
| Champ | Description |
|---|---|
ecommerce.transaction_id | ID de commande |
ecommerce.purchase_revenue | Revenu en devise locale |
ecommerce.purchase_revenue_in_usd | Revenu converti en USD |
ecommerce.shipping_value | Coût de livraison |
ecommerce.tax_value | Montant des taxes |
ecommerce.total_item_quantity | Total d’articles dans la transaction |
Le REPEATED RECORD items contient une entrée par produit :
| Champ | Description |
|---|---|
items.item_id | SKU du produit |
items.item_name | Nom du produit |
items.item_brand | Marque |
items.item_category jusqu’à item_category5 | Hiérarchie de catégories |
items.price | Prix unitaire |
items.quantity | Quantité |
items.coupon | Coupon appliqué |
items.item_params | Paramè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 :
| Champ | Valeurs | Description |
|---|---|---|
privacy_info.ads_storage | Yes, No, Unset | Consentement aux cookies publicitaires |
privacy_info.analytics_storage | Yes, No, Unset | Consentement aux cookies analytics |
privacy_info.uses_transient_token | TRUE/FALSE | Utilisation 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_valueFROM UNNEST(event_params)WHERE key = 'custom_param'Les événements session_start ne sont pas fiables
⚠️ Attention : Ne comptez pas les événements
session_startpour 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 distinctsSELECT COUNT(DISTINCT CONCAT( user_pseudo_id, CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING) )) AS sessionsFROM `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 :
| Champ | Fuseau horaire |
|---|---|
event_timestamp | UTC (microsecondes) |
event_date | Fuseau horaire de la propriété (string YYYYMMDD) |
_TABLE_SUFFIX | Pacific Time |
Conversion vers votre fuseau horaire local :
SELECT DATETIME(TIMESTAMP_MICROS(event_timestamp), 'Europe/Paris') AS local_timeFROM `project.analytics_123456789.events_*`Consent Mode crée des données orphelines
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_idvers 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ériode | Ajout |
|---|---|
| Mars 2020 | RECORD ecommerce |
| Juin 2021 | privacy_info pour Consent Mode |
| Mai 2023 | collected_traffic_source pour l’attribution au niveau événement |
| Juillet 2023 | Champ is_active_user |
| Octobre 2023 | RECORD imbriqué items.item_params |
| Juillet 2024 | Champs 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 :
| Aspect | Universal Analytics | GA4 |
|---|---|---|
| Unité de base | Session | Événement |
| Nom de table | ga_sessions_YYYYMMDD | events_YYYYMMDD |
| La ligne représente | Une session | Un événement |
| ID de session | visitId (champ direct) | ga_session_id (nécessite UNNEST) |
| ID utilisateur | fullVisitorId | user_pseudo_id |
| Données de page | hits.page.pagePath | page_location dans event_params |
| Dimensions personnalisées | Tableau indexé (dimension1, dimension2) | Paires clé-valeur |
| Disponibilité | GA360 uniquement | Toutes 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 joursSELECT event_name, COUNT(*) AS eventsFROM `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_nameUNNEST 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_locationFROM `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 occurrencesFROM `project.analytics_123456789.events_*`, UNNEST(event_params) AS epGROUP BY ep.keyORDER BY occurrences DESCMacros 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_locationFROM {{ 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 :
- Activez les exports quotidien et streaming. Utilisez le quotidien comme source de vérité, le streaming pour le monitoring du jour même.
- Maîtrisez le pattern UNNEST inline. Vous l’utiliserez constamment pour l’extraction de paramètres.
- Combinez toujours
user_pseudo_id+ga_session_idpour l’analyse de session. Aucun des deux champs n’est unique seul. - Ne comptez jamais les événements
session_startpour les métriques de session. Comptez les IDs de session distincts à la place. - 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.