Has conectado Google Analytics 4 (GA4)a Bigquery, pero el camino no ha terminado todavia. Los datos se estan volcando dentro de una tabla, pero es necesario comprender la estructura exacta de los datos que almacenas. Éste es el primer paso obligatorio de cualquier auditoría, pipeline de modelado o reporte analítico en Google Cloud. Sin embargo, al ver los datos nos damos cuenta que la tabla events_ no se comporta como una tabla relacional convencional. En lugar de dimensiones y metricas, el esquema de GA4 es un modelo semiestructurado que entrelaza campos planos, registros jerárquicos (STRUCT) y listas repetidas de pares clave-valor (ARRAY<STRUCT>).
Intentar inspeccionar esta estructura con herramientas habituales suele dejar fuera la parte más valiosa: las dimensiones y métricas personalizadas que viajan dentro de los eventos y las propiedades de usuario. En este artículo aprenderás a extraer un diccionario de datos completo y unificado mediante SQL, descubriendo tanto la arquitectura nativa del export como los parámetros dinámicos, al tiempo que detectas posibles inconsistencias técnicas generadas en el dataLayer.
- Por qué INFORMATION_SCHEMA no es suficiente
- Cómo son los tres niveles de la tabla de GA4
- Consulta SQL unificada
- Detección de tipos mixtos y calidad de tracking
- Automatización con Scheduled Queries
Direct Answer Box (TL;DR)
Para consultar el esquema completo de una tabla de GA4 en BigQuery no basta con
INFORMATION_SCHEMA.COLUMNS, ya que omite los parámetros dinámicos. La solución integral combinaINFORMATION_SCHEMA.COLUMN_FIELD_PATHSpara extraer campos planos y structs fijos (device,geo), junto con una consulta SQL conUNNESTsobreevent_paramsyuser_propertiesen la tabla particionada (events_YYYYMMDD). Esto permite mapear cada clave, armonizar sus tipos de datos (STRING,INT64,FLOAT64) y detectar incoherencias por tipos mixtos originadas en el dataLayer.
Por qué INFORMATION_SCHEMA no es suficiente para el export de GA4
En cualquier almacén de datos estándar, las vistas del catálogo del sistema son el mecanismo para inventariar campos. Cuando explicamos cómo consultar el esquema básico de una tabla de BigQuery, vimos que INFORMATION_SCHEMA.COLUMNS resuelve en segundos la descripción de tipos nativos. No obstante, frente a la complejidad del export de GA4, estas vistas presentan limitaciones.
Las limitaciones de COLUMNS frente a tipos anidados
La vista INFORMATION_SCHEMA.COLUMNS evalúa únicamente las columnas de primer nivel declaradas en el DDL (Data Definition Language) de la tabla. Si lanzas una consulta contra ella en tu dataset de analítica:
SELECT
column_name,
data_type,
is_nullable
FROM `TU_PROYECTO.analytics_XXXXXXXXX.INFORMATION_SCHEMA.COLUMNS`
WHERE table_name = 'events_YYYYMMDD';
Obtendrás resultados planos como event_date (STRING) o event_timestamp (INT64), pero cuando la consulta llega a campos complejos, encontramos los primeros problemas:
column_name |
data_type |
¿Permite ver campos internos? |
|---|---|---|
event_name |
STRING |
Sí (campo escalar directo) |
geo |
RECORD |
❌ No (oculta country, city, etc.) |
device |
RECORD |
❌ No (oculta category, mobile_model_name, etc.) |
event_params |
ARRAY<STRUCT<...>> |
❌ No (solo describe la firma abstracta de la estructura) |
user_properties |
ARRAY<STRUCT<...>> |
❌ No (solo describe la firma abstracta de la estructura) |
COLUMNS etiqueta los structs simplemente como RECORD o desglosa la firma tipológica del array sin mostrar qué campos residen en su interior, impidiendo confeccionar un mapa funcional de telemetría.
COLUMN_FIELD_PATHS: la solución para structs, la barrera con arrays
Para superar esta limitación en tipos estructurados, BigQuery ofrece la vista INFORMATION_SCHEMA.COLUMN_FIELD_PATHS. Esta vista navega de forma recursiva por los structs y devuelve la ruta completa de cada subcampo.
Gracias a COLUMN_FIELD_PATHS, un registro como geo desglosa automáticamente sus miembros: geo.continent, geo.country, geo.region, geo.city, etc., cada uno con su tipo específico (STRING).
Sin embargo, aquí surge la barrera arquitectónica de los event_params y las user_properties:
Esquema físico DDL:
└── event_params (ARRAY<STRUCT>)
├── key (STRING)
└── value (STRUCT)
├── string_value (STRING)
├── int_value (INT64)
├── float_value (FLOAT64)
└── double_value (FLOAT64)
COLUMN_FIELD_PATHS solo puede leer lo que está fijado en los metadatos de la tabla. Para BigQuery, event_params contiene únicamente dos columnas físicas: key y value. El catálogo desconoce por completo si dentro de las filas has enviado page_location, click_text, scroll_depth o transaction_id.
Para conocer los nombres y tipos reales de tus variables personalizadas, no basta con leer los metadatos estáticos: es indispensable interrogar a los datos mismos persistidos en la partición.
Cómo es la tabla de eventos de GA4: los tres niveles de datos
Para estructurar una consulta que unifique metadatos y variables dinámicas, es fundamental clasificar los campos de la tabla events_YYYYMMDD en tres niveles arquitectónicos diferenciados:
| TABLA DE EVENTOS DE GA4 | |
| NIVEL 1: Columnas Planas | event_date, event_name, etc |
| NIVEL 2: Structs Nativos Fijos | geo.*, device.*, traffic_source.* |
| NIVEL 3: Arrays Dinámicos (EAV) | event_params.*, user_properties.* |
Nivel 1: Columnas planas de evento y sesión
Son campos escalares tradicionales que guardan un dato directo por cada fila (evento registrado). Entre ellos destacan:
event_date: Fecha del evento en formatoYYYYMMDD(STRING).event_timestamp: Marca de tiempo en microsegundos UNIX (INT64).event_name: Nombre canónico del evento (STRING).user_pseudo_id: Identificador anónimo de cliente / cookie_ga(STRING).user_id: Identificador propio de usuario autenticado (STRING).
Estos campos se obtienen directamente consultando INFORMATION_SCHEMA.COLUMN_FIELD_PATHS filtrando aquellos registros que no contienen puntos en su ruta ni forman parte de tipos complejos.
Nivel 2: Structs de metadatos nativos (geo, device, traffic_source)
Los structs nativos son objetos JSON embebidos cuyo esquema es rígido y definido por Google. Cada propiedad hija tiene una posición fija en el registro:
geo:continent,country,region,city,metro,sub_continent.device:category,mobile_brand_name,mobile_model_name,operating_system,operating_system_version,web_info.browser.traffic_source:name,medium,source.privacy_info:analytics_storage,ads_storage,uses_transient_token.
Al ser esquemas estáticos, COLUMN_FIELD_PATHS los expone con su jerarquía completa, por lo que no requieren desanidación manual en el motor de consulta.
Nivel 3: Arrays repetidos dinámicos (event_params y user_properties)
Constituyen el núcleo de la personalización en GA4. En lugar de alterar el esquema de la tabla cada vez que un analista crea una dimensión en Google Tag Manager, GA4 utiliza un patrón Entity-Attribute-Value (EAV) dentro de un array de registros:
event_params: Parámetros contextuales del evento (dimensiones y métricas comopage_location,engagement_time_msec,search_term).user_properties: Rasgos persistentes vinculados al usuario (comocrm_status,membership_tier,preferred_language).
Para extraer qué claves existen y qué tipo de datos albergan, es imprescindible recurrir a la operación relacional de aplanado. Ya analizamos en detalle cómo consultar qué parámetros residen dentro de un campo anidado y la mecánica profunda de cómo desanidar parámetros de eventos con UNNEST en BigQuery. Aplicaremos ese principio para construir nuestro inventario.
Nota (Alcance de esta consulta): En esta entrega nos enfocamos exclusivamente en las dimensiones y métricas de eventos (
event_params) y de usuario (user_properties). El arrayitemsy sus correspondientesitems.item_paramsobedecen a la ontología de comercio electrónico mejorado y serán abordados en una guía monográfica dedicada.
Consulta SQL para extraer el diccionario de datos completo
La siguiente consulta SQL unifica en una sola vista tabular todos los campos de tu tabla de GA4: columnas planas, subcampos de structs nativos, parámetros de evento y propiedades de usuario. Además, evalúa la consistencia de tipos de cada parámetro.
Sintaxis y placeholders del proyecto
Antes de ejecutar el código en la consola de BigQuery, debes sustituir los marcadores de posición destacados en mayúsculas:
| Marcador de Posición | Descripción | Ejemplo de Sustitución |
|---|---|---|
TU_PROYECTO |
ID del proyecto de Google Cloud Platform donde reside el dataset. | mi-empresa-analytics-prod |
analytics_XXXXXXXXX |
Nombre del dataset exportado por GA4 (contiene el ID de la propiedad). | analytics_284719203 |
events_YYYYMMDD |
Partición de fecha específica que deseas auditar. | events_20261030 |
Importante (Optimización de costes y rendimiento): Se recomienda apuntar a una partición diaria cerrada (
events_YYYYMMDD), por ejemplo del día anterior, en lugar de utilizar el comodínevents_*. Consultar una partición única procesa solo unos pocos megabytes o gigabytes y ofrece una radiografía exacta del tracking activo.
Código SQL: unión de metadatos y desanidación de parámetros
La consulta utiliza cuatro Common Table Expressions (CTEs), armonizando todos los tipos a la nomenclatura oficial de BigQuery en mayúsculas (STRING, INT64, FLOAT64, RECORD, MIXED):
WITH
-- 1. Campos planos nativos (Nivel 1)
plain_fields AS (
SELECT
'PLAIN_FIELD' AS scope,
field_path AS field_name,
UPPER(data_type) AS data_type,
FALSE AS has_mixed_type_issue
FROM `TU_PROYECTO.analytics_XXXXXXXXX.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS`
WHERE table_name = 'events_YYYYMMDD'
AND field_path NOT LIKE '%.%'
AND data_type NOT LIKE 'STRUCT%'
AND data_type NOT LIKE 'ARRAY%'
),
-- 2. Subcampos de structs nativos fijos (Nivel 2)
struct_fields AS (
SELECT
'STRUCT_FIELD' AS scope,
field_path AS field_name,
UPPER(data_type) AS data_type,
FALSE AS has_mixed_type_issue
FROM `TU_PROYECTO.analytics_XXXXXXXXX.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS`
WHERE table_name = 'events_YYYYMMDD'
AND field_path LIKE '%.%'
AND field_path NOT LIKE 'event_params%'
AND field_path NOT LIKE 'user_properties%'
AND field_path NOT LIKE 'items%'
AND data_type NOT LIKE 'STRUCT%'
AND data_type NOT LIKE 'ARRAY%'
),
-- 3. Parámetros dinámicos de eventos (Nivel 3)
raw_event_params AS (
SELECT
ep.key AS param_key,
CASE
WHEN ep.value.string_value IS NOT NULL THEN 'STRING'
WHEN ep.value.int_value IS NOT NULL THEN 'INT64'
WHEN ep.value.float_value IS NOT NULL OR ep.value.double_value IS NOT NULL THEN 'FLOAT64'
ELSE 'NULL'
END AS detected_type
FROM `TU_PROYECTO.analytics_XXXXXXXXX.events_YYYYMMDD`,
UNNEST(event_params) AS ep
),
arrays_event_params AS (
SELECT
'EVENT_PARAM' AS scope,
param_key AS field_name,
IF(COUNT(DISTINCT detected_type) > 1, 'MIXED', ANY_VALUE(detected_type)) AS data_type,
COUNT(DISTINCT detected_type) > 1 AS has_mixed_type_issue
FROM raw_event_params
WHERE detected_type != 'NULL'
GROUP BY ALL
),
-- 4. Propiedades dinámicas de usuario (Nivel 3)
raw_user_properties AS (
SELECT
up.key AS prop_key,
CASE
WHEN up.value.string_value IS NOT NULL THEN 'STRING'
WHEN up.value.int_value IS NOT NULL THEN 'INT64'
WHEN up.value.float_value IS NOT NULL OR up.value.double_value IS NOT NULL THEN 'FLOAT64'
ELSE 'NULL'
END AS detected_type
FROM `TU_PROYECTO.analytics_XXXXXXXXX.events_YYYYMMDD`,
UNNEST(user_properties) AS up
),
arrays_user_properties AS (
SELECT
'USER_PROPERTY' AS scope,
prop_key AS field_name,
IF(COUNT(DISTINCT detected_type) > 1, 'MIXED', ANY_VALUE(detected_type)) AS data_type,
COUNT(DISTINCT detected_type) > 1 AS has_mixed_type_issue
FROM raw_user_properties
WHERE detected_type != 'NULL'
GROUP BY ALL
)
-- Consolidación y salida estructurada
SELECT * FROM plain_fields
UNION ALL
SELECT * FROM struct_fields
UNION ALL
SELECT * FROM arrays_event_params
UNION ALL
SELECT * FROM arrays_user_properties
ORDER BY scope, field_name;
Muestra de resultados esperados
Al ejecutar la consulta obtendrás una tabla normalizada que actúa como el inventario exhaustivo de tu implementación:
scope |
field_name |
data_type |
has_mixed_type_issue |
|---|---|---|---|
EVENT_PARAM |
engagement_time_msec |
INT64 |
false |
EVENT_PARAM |
ga_session_id |
INT64 |
false |
EVENT_PARAM |
page_location |
STRING |
false |
EVENT_PARAM |
page_id |
MIXED |
true |
EVENT_PARAM |
scroll_depth |
INT64 |
false |
PLAIN_FIELD |
event_date |
STRING |
false |
PLAIN_FIELD |
event_name |
STRING |
false |
PLAIN_FIELD |
user_pseudo_id |
STRING |
false |
STRUCT_FIELD |
device.category |
STRING |
false |
STRUCT_FIELD |
device.web_info.browser |
STRING |
false |
STRUCT_FIELD |
geo.country |
STRING |
false |
USER_PROPERTY |
customer_tier |
STRING |
false |
Detección de tipos mixtos e inconsistencias del dataLayer
Uno de los aportes más críticos de esta consulta para los equipos de gobernanza de datos y analítica digital es la columna has_mixed_type_issue.
Cómo armonizar string_value, int_value y float_value
En el export nativo de GA4, la estructura value de los arrays event_params y user_properties no almacena un campo genérico de texto; contiene cuatro columnas mutuamente excluyentes:
string_value: Se rellena si el valor fue recibido como cadena de texto.int_value: Se rellena si el valor fue procesado como un entero de 64 bits.float_value: Se rellena si el valor contiene decimales de precisión simple.double_value: Se rellena si el valor contiene decimales de doble precisión.
Nuestra sentencia CASE clasifica cada ocurrencia en un tipo SQL estándar:
CASE
WHEN ep.value.string_value IS NOT NULL THEN 'STRING'
WHEN ep.value.int_value IS NOT NULL THEN 'INT64'
WHEN ep.value.float_value IS NOT NULL OR ep.value.double_value IS NOT NULL THEN 'FLOAT64'
ELSE 'NULL'
END
Identificación de colisiones de tipo y alerta de gobernanza de datos
En una implementación limpia, un parámetro determinado (por ejemplo, ga_session_id o transaction_id) debe mantener siempre el mismo tipo de datos a lo largo de todos los eventos. Sin embargo, en auditorías reales es común encontrar que un mismo parámetro se almacena a veces como número (INT64) y a veces como texto (STRING).
¿Por qué ocurre esto?
- Definición del tipo de variable en la recogida con Tag Manager: Una etiqueta de evento envía la variable leída del dataLayer como un número puro (
10492), mientras que otra etiqueta en una plantilla distinta la envía interpolada como string ("10492"). - Inconsistencias en el dataLayer del CMS: La capa de datos frontend emite variables sin tipar estrictamente en JavaScript, alternando entre strings y enteros según la página o el flujo de navegación.
Cuando agrupamos por param_key mediante la instrucción GROUP BY ALL, la función condicional:
IF(COUNT(DISTINCT detected_type) > 1, 'MIXED', ANY_VALUE(detected_type))
Detecta si existen múltiples tipos para la misma clave. Si la cuenta es mayor que 1, el parámetro recibe el tipo MIXED y activa la bandera has_mixed_type_issue = TRUE.
Consulta de diagnóstico para parámetros con tipo mixto
Si la consulta general reporta parámetros con has_mixed_type_issue = true, puedes aislar las colisiones y contar el volumen exacto de registros afectados con esta consulta de diagnóstico:
SELECT
ep.key AS conflicting_param,
CASE
WHEN ep.value.string_value IS NOT NULL THEN 'STRING'
WHEN ep.value.int_value IS NOT NULL THEN 'INT64'
WHEN ep.value.float_value IS NOT NULL OR ep.value.double_value IS NOT NULL THEN 'FLOAT64'
END AS observed_type,
COUNT(1) AS event_count,
ANY_VALUE(event_name) AS sample_event
FROM `TU_PROYECTO.analytics_XXXXXXXXX.events_YYYYMMDD`,
UNNEST(event_params) AS ep
WHERE ep.key IN (
-- Subconsulta que localiza los parámetros polimórficos
SELECT ep_sub.key
FROM `TU_PROYECTO.analytics_XXXXXXXXX.events_YYYYMMDD`,
UNNEST(event_params) AS ep_sub
GROUP BY ep_sub.key
HAVING COUNT(DISTINCT CASE
WHEN ep_sub.value.string_value IS NOT NULL THEN 'STRING'
WHEN ep_sub.value.int_value IS NOT NULL THEN 'INT64'
WHEN ep_sub.value.float_value IS NOT NULL OR ep_sub.value.double_value IS NOT NULL THEN 'FLOAT64'
END) > 1
)
GROUP BY ALL
ORDER BY conflicting_param, event_count DESC;
Esta consulta te mostrará exactamente qué tipo predomina y en qué eventos (sample_event) se produce la desviación, permitiendo al equipo técnico corregir el tag en GTM o normalizar el dataLayer antes de que corrompa modelos analíticos aguas abajo.
Automatización y gobierno: de la consulta manual a Scheduled Queries
Ejecutar esta consulta de forma aislada en la consola resuelve una duda puntual. Sin embargo, su valor estratégico se multiplica cuando se transforma en un proceso de auditoría continua.
Cómo convertir esta auditoría en una tabla de diccionario viva
Las implementaciones de tracking son dinámicas: nuevos despliegues de código, campañas de marketing y cambios en el CMS incorporan nuevos parámetros con frecuencia, en muchos casos sin previa notificación al equipo de analítica.
Para mantener el esquema de datos bajo control, puedes materializar la salida de esta consulta en una tabla estática de diccionario (ga4_data_dictionary) y programar su actualización (diaria, semanal, mensual,...). En nuestra guía sobre cómo programar consultas en BigQuery mediante Scheduled Queries, detallamos el procedimiento paso a paso para configurar ejecuciones desatendidas con BigQuery Data Transfer Service.
Al programar esta consulta para ejecutarse periódicamente sobre la partición recién cerrada (events_ del día anterior), consigues:
- Detección automática de variables no documentadas: Identificar la aparición de nuevas dimensiones creadas en GTM que no consten en el plan de medición.
- Monitorización de calidad de datos: Crear paneles en Looker Studio o alertas automáticas cuando haya una nueva variable o
has_mixed_type_issuesea verdadero. - Optimización de costes en reporting: se puede consultar la tabla de diccionario en milisegundos para verificar la sintaxis de un parámetro sin necesidad de escanear la tabla entera de eventos.
Para profundizar en la explotación técnica del export de GA4 y de BigQuery, te recomendamos consultar las siguientes artículosulos de nuestro blog:
- BigQuery: cómo desanidar parámetros de eventos con UNNEST — Guía práctica sobre la sintaxis relacional de aplanado de arrays en consultas agregadas.
- BigQuery: consultar parámetros dentro de un campo anidado — Técnicas avanzadas de filtrado y agregación para estructuras anidadas.
- BigQuery: cómo programar consultas paso a paso — Automatización con Scheduled Queries para alimentar tablas de reporte y auditoría continua.
- BigQuery: cómo consultar el esquema básico de una tabla — Inspección de metadatos relacionales con vistas
INFORMATION_SCHEMA.
David Hernández
Digital Analyst
Soy un apasionado de los datos, la tecnología, la ciencia y el aprendizaje continuo.
Trabajo como Digital Analyst desde 2020.
Participo en el ciclo completo de los datos: definición e implementación de recolección, procesado y distribución, análisis de datos y reporting.
