Schema Reference
The GA4 BigQuery export schema has a fixed outer structure with several nested fields. The key challenge is that event parameters and user properties are stored as arrays of structs rather than flat columns — every parameter lookup requires an UNNEST operation.
Table structure overview
Section titled “Table structure overview”Each daily table (events_YYYYMMDD) contains one row per event. The top-level columns are:
events_YYYYMMDD├── event_date STRING├── event_timestamp INT64 (microseconds since epoch)├── event_name STRING├── event_params ARRAY<STRUCT<key, value>>├── event_previous_timestamp INT64├── event_value_in_usd FLOAT64├── event_bundle_sequence_id INT64├── event_server_timestamp_offset INT64├── user_id STRING├── user_pseudo_id STRING├── privacy_info STRUCT├── user_properties ARRAY<STRUCT<key, value>>├── user_first_touch_timestamp INT64├── user_ltv STRUCT<revenue, currency>├── device STRUCT├── geo STRUCT├── app_info STRUCT├── traffic_source STRUCT (first-touch)├── collected_traffic_source STRUCT (session-level)├── session_traffic_source_last_click STRUCT (last-click attribution)├── stream_id STRING├── platform STRING├── event_dimensions STRUCT├── ecommerce STRUCT├── items ARRAY<STRUCT>├── is_active_user BOOLEAN├── batch_event_index INT64├── batch_page_id INT64└── batch_ordering_id INT64Complete top-level fields reference
Section titled “Complete top-level fields reference”| Field | Type | Description |
|---|---|---|
event_date | STRING | Date in YYYYMMDD format |
event_timestamp | INTEGER | Microseconds since Unix epoch |
event_name | STRING | Event name (e.g., page_view, purchase) |
event_params | RECORD (REPEATED) | Event parameters array |
event_previous_timestamp | INTEGER | Previous event timestamp for this user |
event_value_in_usd | FLOAT | Event value converted to USD |
event_bundle_sequence_id | INTEGER | Bundle sequence ID |
event_server_timestamp_offset | INTEGER | Server timestamp offset in microseconds |
user_id | STRING | User ID (only if set in implementation) |
user_pseudo_id | STRING | Client ID / anonymous device identifier |
privacy_info | RECORD | Consent mode privacy settings |
user_properties | RECORD (REPEATED) | User properties array |
user_first_touch_timestamp | INTEGER | First touch timestamp in microseconds |
user_ltv | RECORD | Lifetime value information |
device | RECORD | Device information |
geo | RECORD | Geographic information |
app_info | RECORD | App information (mobile streams only) |
traffic_source | RECORD | First-touch traffic source (user-level) |
collected_traffic_source | RECORD | Session-level traffic source with UTM and click IDs |
session_traffic_source_last_click | RECORD | Last-click attribution data |
stream_id | STRING | Data stream ID |
platform | STRING | WEB, IOS, or ANDROID |
event_dimensions | RECORD | Event dimensions |
ecommerce | RECORD | Ecommerce transaction data |
items | RECORD (REPEATED) | Ecommerce items array |
is_active_user | BOOLEAN | Whether the user was active in the reporting period |
batch_event_index | INTEGER | Index of this event within the batch |
batch_page_id | INTEGER | Page ID within the batch |
batch_ordering_id | INTEGER | Ordering ID within the batch |
Top-level columns
Section titled “Top-level columns”event_date
Section titled “event_date”Type: STRING
Format: YYYYMMDD (e.g., 20240115)
Use: Date filtering. Note this is a string, not a DATE type. Convert with PARSE_DATE('%Y%m%d', event_date).
-- Filter by event_date (uses _TABLE_SUFFIX for partition pruning, also filter by event_date for intraday tables)WHERE _TABLE_SUFFIX = '20240115'-- Or convert for date arithmetic:WHERE PARSE_DATE('%Y%m%d', event_date) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)event_timestamp
Section titled “event_timestamp”Type: INT64 Unit: Microseconds since Unix epoch Use: Time-of-day analysis, session reconstruction, ordering events within a session.
-- Convert to readable timestampTIMESTAMP_MICROS(event_timestamp) AS event_time
-- Get hour of dayEXTRACT(HOUR FROM TIMESTAMP_MICROS(event_timestamp) AT TIME ZONE 'America/New_York') AS hour_of_dayevent_name
Section titled “event_name”Type: STRING
Use: Filter to specific events. Always pair with _TABLE_SUFFIX filter.
WHERE event_name = 'purchase'WHERE event_name IN ('add_to_cart', 'begin_checkout', 'purchase')WHERE event_name LIKE 'video_%'user_id
Section titled “user_id”Type: STRING
Nullable: Yes — only populated if you set User ID in your implementation
Use: Join with your CRM or first-party data. This is the value you set with gtag('config', ..., { user_id: '...' }).
user_pseudo_id
Section titled “user_pseudo_id”Type: STRING
Use: The anonymous user identifier. Equivalent to the client_id on web. Use this as the primary user identifier when User ID is not available.
Note: This value resets when a user clears cookies. It is not a persistent identifier across browsers or devices.
event_params — the key nested field
Section titled “event_params — the key nested field”event_params is a repeated record with the following exact structure:
event_params ARRAY<STRUCT< key STRING, value STRUCT< string_value STRING, int_value INT64, float_value FLOAT64, double_value FLOAT64 >>>Each parameter is stored as one row in the array, with the value in one of four sub-fields depending on its type. Only one value field is populated per parameter — the others are NULL. To extract a parameter, UNNEST the array and filter by key:
-- Extract string parameter(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_title') AS page_title
-- Extract integer parameter(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS session_id
-- Extract float or double (use COALESCE for parameters that might be either)(SELECT COALESCE(value.float_value, value.double_value) FROM UNNEST(event_params) WHERE key = 'value') AS transaction_value
-- Safe extraction (returns NULL if param not present)(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'search_term') AS search_termCommonly used event_params keys
Section titled “Commonly used event_params keys”| Key | Type | Description |
|---|---|---|
ga_session_id | int_value | Session identifier |
ga_session_number | int_value | nth session for this user |
session_engaged | string_value | ’1’ if session was engaged |
page_location | string_value | Full page URL |
page_title | string_value | Page title |
page_referrer | string_value | Referring URL |
percent_scrolled | int_value | Scroll depth (90 for Enhanced Measurement scroll) |
link_url | string_value | Outbound click destination URL |
search_term | string_value | Site search query |
file_name | string_value | Downloaded file name |
video_title | string_value | YouTube video title |
video_percent | int_value | Video progress percentage |
engagement_time_msec | int_value | Engagement time in milliseconds |
transaction_id | string_value | Purchase transaction ID |
value | float/double_value | Purchase or event value |
currency | string_value | ISO 4217 currency code |
coupon | string_value | Coupon code applied |
affiliation | string_value | Store or affiliate name |
user_properties
Section titled “user_properties”Similar to event_params but with an additional set_timestamp_micros field. Contains user properties set with gtag('set', 'user_properties', {...}).
user_properties ARRAY<STRUCT< key STRING, value STRUCT< string_value STRING, int_value INT64, float_value FLOAT64, double_value FLOAT64, set_timestamp_micros INT64 >>>The set_timestamp_micros field records when the user property was last set for this user. This is unique to user_properties — event_params does not have it.
-- Extract user property value(SELECT value.string_value FROM UNNEST(user_properties) WHERE key = 'subscription_tier') AS subscription_tier
-- Extract when the property was last set(SELECT TIMESTAMP_MICROS(value.set_timestamp_micros) FROM UNNEST(user_properties) WHERE key = 'subscription_tier') AS tier_set_at
-- Extract with both value and timestampSELECT user_pseudo_id, up.key AS property_name, COALESCE(up.value.string_value, CAST(up.value.int_value AS STRING)) AS property_value, TIMESTAMP_MICROS(up.value.set_timestamp_micros) AS set_timeFROM `project.analytics_PROPERTY_ID.events_*`, UNNEST(user_properties) AS upWHERE _TABLE_SUFFIX = '20240115'User property limits
Section titled “User property limits”- Maximum 25 custom user properties per GA4 property
- Property names: up to 24 characters
- Property values: up to 36 characters
device record
Section titled “device record”| Field | Type | Description |
|---|---|---|
device.category | STRING | mobile, desktop, or tablet |
device.mobile_brand_name | STRING | e.g., Apple, Samsung |
device.mobile_model_name | STRING | e.g., iPhone 14, Galaxy S23 |
device.mobile_marketing_name | STRING | Marketing name of the device |
device.mobile_os_hardware_model | STRING | OS-reported hardware model |
device.operating_system | STRING | iOS, Android, Windows, Macintosh |
device.operating_system_version | STRING | OS version string |
device.vendor_id | STRING | iOS vendor ID (IDFV) |
device.advertising_id | STRING | GAID (Android) or IDFA (iOS), if enabled |
device.language | STRING | Device language (e.g., en-us) |
device.is_limited_ad_tracking | STRING | Whether ad tracking is limited |
device.time_zone_offset_seconds | INTEGER | Offset from UTC in seconds |
device.browser | STRING | Chrome, Safari, Firefox, etc. |
device.browser_version | STRING | Browser version string |
device.web_info.browser | STRING | Browser (web-specific) |
device.web_info.browser_version | STRING | Browser version (web-specific) |
device.web_info.hostname | STRING | Hostname of the page |
SELECT device.category, device.operating_system, device.browser, device.web_info.hostname, COUNT(*) AS sessionsFROM `project.analytics_PROPERTY_ID.events_*`WHERE event_name = 'session_start' AND _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'GROUP BY 1, 2, 3, 4ORDER BY sessions DESCgeo record
Section titled “geo record”| Field | Type | Description |
|---|---|---|
geo.continent | STRING | Americas, Europe, Asia, etc. |
geo.country | STRING | Country name |
geo.region | STRING | State or province name |
geo.city | STRING | City name |
geo.sub_continent | STRING | Sub-continent region |
geo.metro | STRING | Designated Market Area (US only) |
traffic_source record (first-touch)
Section titled “traffic_source record (first-touch)”Contains the original traffic source for the user — the source from their very first session. This is user-level, not session-level.
| Field | Type | Description |
|---|---|---|
traffic_source.name | STRING | Campaign name |
traffic_source.medium | STRING | Medium (e.g., organic, cpc) |
traffic_source.source | STRING | Source (e.g., google, facebook) |
collected_traffic_source record (session-level)
Section titled “collected_traffic_source record (session-level)”Contains session-level traffic source information, including UTM parameters and Google click IDs. This is the record to use for session-level attribution.
| Field | Type | Description |
|---|---|---|
collected_traffic_source.manual_campaign_id | STRING | UTM campaign ID (utm_id) |
collected_traffic_source.manual_campaign_name | STRING | UTM campaign name (utm_campaign) |
collected_traffic_source.manual_source | STRING | UTM source (utm_source) |
collected_traffic_source.manual_medium | STRING | UTM medium (utm_medium) |
collected_traffic_source.manual_term | STRING | UTM term (utm_term) |
collected_traffic_source.manual_content | STRING | UTM content (utm_content) |
collected_traffic_source.gclid | STRING | Google Ads click ID |
collected_traffic_source.dclid | STRING | Display & Video 360 click ID |
collected_traffic_source.srsltid | STRING | Google Merchant Center click ID |
-- Session-level traffic source attributionSELECT collected_traffic_source.manual_source AS source, collected_traffic_source.manual_medium AS medium, collected_traffic_source.manual_campaign_name AS campaign, collected_traffic_source.gclid, 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_PROPERTY_ID.events_*`WHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131' AND (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'entrances') = 1GROUP BY 1, 2, 3, 4ORDER BY sessions DESCsession_traffic_source_last_click record
Section titled “session_traffic_source_last_click record”Contains last-click attribution data for the session. This is a newer field that provides attribution information aligned with Google Ads reporting.
ecommerce record
Section titled “ecommerce record”Populated for events that use GA4’s ecommerce data model (purchase, add_to_cart, begin_checkout, etc.):
| Field | Type | Description |
|---|---|---|
ecommerce.total_item_quantity | INTEGER | Total quantity of all items |
ecommerce.purchase_revenue_in_usd | FLOAT | Revenue converted to USD |
ecommerce.purchase_revenue | FLOAT | Revenue in property currency |
ecommerce.refund_value_in_usd | FLOAT | Refund amount in USD |
ecommerce.refund_value | FLOAT | Refund amount in property currency |
ecommerce.shipping_value_in_usd | FLOAT | Shipping cost in USD |
ecommerce.shipping_value | FLOAT | Shipping cost in property currency |
ecommerce.tax_value_in_usd | FLOAT | Tax amount in USD |
ecommerce.tax_value | FLOAT | Tax amount in property currency |
ecommerce.unique_items | INTEGER | Number of distinct items |
ecommerce.transaction_id | STRING | Transaction/order ID |
items array
Section titled “items array”The items array contains one element per product in the event. Each item is a STRUCT with the following fields:
| Field | Type | Description |
|---|---|---|
items.item_id | STRING | Product SKU or ID |
items.item_name | STRING | Product name |
items.item_brand | STRING | Product brand |
items.item_variant | STRING | Product variant (e.g., size, color) |
items.item_category | STRING | Primary category |
items.item_category2 | STRING | Category level 2 |
items.item_category3 | STRING | Category level 3 |
items.item_category4 | STRING | Category level 4 |
items.item_category5 | STRING | Category level 5 |
items.price_in_usd | FLOAT | Unit price in USD |
items.price | FLOAT | Unit price in local currency |
items.quantity | INTEGER | Quantity |
items.item_revenue_in_usd | FLOAT | Item revenue in USD |
items.item_revenue | FLOAT | Item revenue in local currency |
items.item_refund_in_usd | FLOAT | Item refund amount in USD |
items.item_refund | FLOAT | Item refund amount in local currency |
items.coupon | STRING | Item-level coupon code |
items.affiliation | STRING | Store or affiliate name |
items.location_id | STRING | Physical location ID |
items.item_list_id | STRING | ID of the list the item was in |
items.item_list_name | STRING | Name of the list the item was in |
items.item_list_index | STRING | Position in the list |
items.promotion_id | STRING | Promotion ID |
items.promotion_name | STRING | Promotion name |
items.creative_name | STRING | Creative associated with the promotion |
items.creative_slot | STRING | Creative slot name |
-- Unnest items for product-level analysisSELECT event_date, item.item_id, item.item_name, item.item_brand, item.item_category, item.item_category2, item.price, item.quantity, item.item_revenue, item.coupon, item.promotion_nameFROM `project.analytics_PROPERTY_ID.events_*`, UNNEST(items) AS itemWHERE event_name = 'purchase' AND _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'app_info struct
Section titled “app_info struct”Populated for app streams:
app_info.id -- Bundle ID / package nameapp_info.version -- App version stringapp_info.install_store -- 'google' or 'apple'app_info.firebase_app_id -- Firebase app IDapp_info.install_source -- Install sourceprivacy_info struct
Section titled “privacy_info struct”privacy_info.ads_storage -- 'Yes', 'No', 'Unset'privacy_info.analytics_storage -- 'Yes', 'No', 'Unset'privacy_info.uses_transient_token -- 'Yes', 'No'analytics_storage = 'No' means the event was collected with consent mode analytics denied — the data is modeled/estimated, not from a real user session.
user_ltv struct
Section titled “user_ltv struct”user_ltv.revenue -- Cumulative revenue from this user since first visituser_ltv.currency -- Currency of the revenueusers_* and pseudonymous_users_* tables
Section titled “users_* and pseudonymous_users_* tables”In addition to the daily events_* tables, the BigQuery export includes user-level tables:
users_*— Contains data for users who have auser_idsetpseudonymous_users_*— Contains data for all users (identified byuser_pseudo_id)
These tables contain aggregated user data:
| Field | Description |
|---|---|
user_id | User ID (if set) |
user_pseudo_id | Device/Client ID |
user_info | User-level aggregates (first touch, last active, first purchase) |
audiences | Audience memberships |
user_properties | All user properties |
device | Device info |
geo | Location info |
user_ltv | Lifetime value (revenue, currency) |
predictions | ML predictions (purchase probability, churn probability) |
-- Query the users table for lifetime valueSELECT user_pseudo_id, user_info.last_active_timestamp_micros, user_info.user_first_touch_timestamp_micros, user_info.first_purchase_date, user_ltv.revenue_in_usd AS lifetime_revenueFROM `project.analytics_PROPERTY_ID.users_*`WHERE _TABLE_SUFFIX = '20240115'Common query patterns
Section titled “Common query patterns”All events for a specific date range
Section titled “All events for a specific date range”SELECT *FROM `project.analytics_PROPERTY_ID.events_*`WHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'LIMIT 1000Count events by name for a date
Section titled “Count events by name for a date”SELECT event_name, COUNT(*) AS countFROM `project.analytics_PROPERTY_ID.events_20240115`GROUP BY event_nameORDER BY count DESCPage views with title and path
Section titled “Page views with title and path”SELECT (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page_url, (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_title') AS page_title, COUNT(*) AS viewsFROM `project.analytics_PROPERTY_ID.events_*`WHERE event_name = 'page_view' AND _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'GROUP BY 1, 2ORDER BY views DESCLIMIT 50