ServicesÀ proposNotesContact Me contacter →
EN FR
Note

GA4 : CROSS JOIN versus LEFT JOIN UNNEST

Pourquoi la syntaxe virgule dans FROM table, UNNEST(array) supprime silencieusement des lignes — et quand utiliser LEFT JOIN UNNEST pour préserver les événements sans données de tableau.

Planté
ga4bigqueryanalyticsdata modeling

La syntaxe virgule dans FROM table, UNNEST(array) est du sucre syntaxique pour CROSS JOIN UNNEST. La conséquence est que CROSS JOIN supprime silencieusement les lignes dont le tableau est vide ou NULL.

Cela a plus d’importance qu’on pourrait le croire. Tous les événements GA4 n’ont pas tous leurs tableaux renseignés. Si vous écrivez une requête qui touche des événements avec et sans un tableau imbriqué particulier, CROSS JOIN filtrera silencieusement les événements que vous aviez l’intention de conserver.

Le problème en pratique

Considérez une requête qui tente de voir à la fois les achats et les démarrages de session :

-- CROSS JOIN implicite : les événements sans items disparaissent
SELECT event_name, item.item_name
FROM `project.dataset.events_*`,
UNNEST(items) AS item
WHERE event_name IN ('purchase', 'session_start')

Vous attendez des lignes pour les deux types d’événements. Vous n’obtenez que les achats. Les événements session_start n’ont pas de tableau items, donc le CROSS JOIN les exclut entièrement. Pas d’erreur, pas d’avertissement — juste des données manquantes.

C’est un CROSS JOIN qui fait exactement ce que CROSS JOIN fait : produire le produit cartésien de deux ensembles. Lorsqu’un ensemble est vide, le produit est vide. Les lignes disparaissent.

LEFT JOIN préserve toutes les lignes

Lorsque vous devez conserver les événements qu’ils aient ou non leur tableau renseigné, utilisez LEFT JOIN UNNEST explicitement :

-- LEFT JOIN : tous les événements apparaissent, NULL pour les items manquants
SELECT event_name, item.item_name
FROM `project.dataset.events_*`
LEFT JOIN UNNEST(items) AS item
WHERE event_name IN ('purchase', 'session_start')

Les événements session_start apparaissent maintenant avec NULL dans la colonne item.item_name. Chaque événement de votre clause WHERE est représenté dans la sortie.

La différence syntaxique est minime — remplacez la virgule par LEFT JOIN et supprimez la virgule avant UNNEST. Mais la différence sémantique est significative.

Quel type de JOIN pour quel tableau

GA4 possède trois tableaux imbriqués principaux. Le bon type de JOIN dépend duquel vous travaillez :

event_params : Utilisez CROSS JOIN (syntaxe virgule). Chaque événement GA4 a des paramètres. Un événement sans event_params serait un problème de corruption de données, pas un état normal. Le CROSS JOIN est sûr ici car le tableau n’est jamais vide en pratique.

Cela dit, la plupart des extractions event_params utilisent de toute façon des sous-requêtes corrélées plutôt que UNNEST en clause FROM. Les sous-requêtes corrélées n’ont pas ce problème de type de JOIN car elles opèrent dans la clause SELECT, pas la clause FROM.

items : Par défaut, LEFT JOIN. Seuls les événements e-commerce (purchase, add_to_cart, view_item, begin_checkout, etc.) renseignent le tableau items. Si votre requête filtre sur un seul type d’événement e-commerce, CROSS JOIN est correct — chaque événement purchase devrait avoir des items. Mais dès que vous mélangez des types d’événements ou écrivez une requête susceptible de rencontrer des événements sans items, LEFT JOIN évite la perte silencieuse de lignes.

Le pattern UNNEST des items dans dbt utilise CROSS JOIN intentionnellement car il pré-filtre sur les événements e-commerce et inclut un garde ARRAY_LENGTH(items) > 0. C’est la bonne approche pour un modèle items dédié. Pour les requêtes ad-hoc sur des types d’événements mixtes, LEFT JOIN est plus sûr.

user_properties : Par défaut, LEFT JOIN. Tous les événements ne portent pas de propriétés utilisateur. Si un utilisateur n’a pas de propriétés utilisateur personnalisées définies, le tableau peut être vide. CROSS JOIN supprimerait ces événements.

Le modèle mental

Pensez-y ainsi : si vous interrogez un tableau spécifique parce que vous voulez des données depuis ce tableau, CROSS JOIN est approprié — vous filtrez délibérément aux événements qui ont cette donnée. Si vous interrogez des événements et enrichissez optionnellement avec des données de tableau, LEFT JOIN est correct — vous voulez tous les événements indépendamment.

La distinction correspond à l’intention :

  • « Montrez-moi les détails des produits pour les achats » → CROSS JOIN est correct (vous voulez seulement les événements qui ont des items)
  • « Montrez-moi tous les événements avec les détails des produits si disponibles » → LEFT JOIN requis (vous voulez chaque événement, avec les items comme contexte bonus)

Combiner avec des sous-requêtes corrélées

Le pattern le plus propre pour les requêtes mixtes combine LEFT JOIN pour un tableau avec des sous-requêtes corrélées pour un autre. Cela évite à la fois le problème de perte silencieuse de lignes et le problème du produit cartésien d’un UNNEST sur plusieurs tableaux :

-- Sûr : une ligne par item, params extraits via sous-requête
SELECT
item.item_name,
(SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'transaction_id') AS transaction_id
FROM `project.dataset.events_*`,
UNNEST(items) AS item
WHERE event_name = 'purchase'
AND _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'

Ici, CROSS JOIN sur items est sûr car nous filtrons sur les événements purchase. L’extraction event_params utilise une sous-requête corrélée plutôt qu’un second UNNEST dans la clause FROM. Pas de produit cartésien, pas de perte de lignes.

Scénario de débogage courant

Si votre requête GA4 retourne moins de lignes qu’attendu, vérifiez d’abord votre type de JOIN UNNEST. Le symptôme — requête correcte, résultats plausibles, juste moins que prévu — rend cela difficile à détecter. Vous n’obtenez pas d’erreur. Vous obtenez un nombre qui semble raisonnable mais est faux.

Le diagnostic rapide : remplacez temporairement le UNNEST virgule par LEFT JOIN UNNEST. Si votre comptage de lignes augmente, vous perdiez silencieusement des lignes à cause des tableaux vides.