Google BigQuery

How to unnest GA4 event_params in BigQuery: page_location, ga_session_id and items

In the GA4 BigQuery Export, values like page_location and ga_session_id sit inside the event_params array, one key and four possible value fields per parameter. This guide shows the subquery pattern that reads one parameter per column, which value field each common key uses, a parameter that arrives as two types, and how a plain UNNEST turns 22,096 events into 215,602 rows or drops half of them. Every query runs on Google's public GA4 sample.

Google BigQueryAlyona Samovar4 min read

How to unnest GA4 event_params in BigQuery: page_location, ga_session_id and items

In the GA4 BigQuery Export, an event’s details are not columns. They sit in event_params, an array of key-value pairs, and each value is stored in one of four fields: string_value, int_value, float_value or double_value. To read one parameter as a column, use a subquery over the array:

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 ga_session_id
FROM `bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_20210101`
WHERE event_name = 'page_view';
event_name page_location ga_session_id
page_view https://shop.googlemerchandisestore.com/Google+Redesign/Apparel/Google+Dino+Game+Tee 3209612510
page_view https://shop.googlemerchandisestore.com/ 7473279052

One row per event, one column per parameter, and nothing multiplied. Change the key and the value field for any other parameter.

Note: Originally published in August 2024. Fully rewritten in October 2026: every query below was run on Google’s public GA4 sample, bigquery-public-data.ga4_obfuscated_sample_ecommerce, on 16 October 2026.

Which value field to read

Which value field to read

Pick the wrong field and the column is NULL, with no error: reading ga_session_id from string_value returned NULL on every row. This query shows which field each key uses on one day of the sample:

SELECT
  p.key,
  COUNT(*) AS occurrences,
  COUNTIF(p.value.string_value IS NOT NULL) AS as_string,
  COUNTIF(p.value.int_value IS NOT NULL) AS as_int
FROM `bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_20210101`,
  UNNEST(event_params) AS p
GROUP BY p.key
ORDER BY occurrences DESC;
key occurrences as_string as_int
page_location 22096 22096 0
ga_session_id 22096 0 22096
ga_session_number 22096 0 22096
page_title 21985 21982 0
engaged_session_event 20776 0 20776
session_engaged 20090 17890 2200
page_referrer 17241 5795 0
engagement_time_msec 12965 0 12965
source, medium, campaign 6660 each 6660 0

Run it on your own property once and keep the result: it is the map for every query you write later.

A parameter can arrive as two types

session_engaged came as a string 17,890 times and as an integer 2,200 times on the same day. A query that reads only string_value misses 2,200 values. Read both:

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

Custom parameters do this often when different pages or apps send them differently. The type check above finds them.

Sessions: ga_session_id is not unique on its own

Sessions: ga_session_id is not unique on its own

ga_session_id is a timestamp of when the session started, so two users can share one. A session is the pair user_pseudo_id and ga_session_id:

COUNT(DISTINCT CONCAT(
  user_pseudo_id, '-',
  CAST((SELECT value.int_value FROM UNNEST(event_params)
        WHERE key = 'ga_session_id') AS STRING)
)) AS sessions

A full sessions-per-day query, with its result, is in BigQuery query examples.

The Turning Point

OWOX Data Marts

See your first report built in real time. 15 minutes.

  1. Connect your data warehouse
  2. Pick your metrics
  3. Get a live Google Sheets report

In the time it takes to write a ticket. Then imagine never writing that ticket again.

Book a Demo

We'll use your actual use case

What a plain UNNEST does to row counts

What a plain UNNEST does to row counts

The other way to read the array, joining it to the table, multiplies rows:

Query on one day (22,096 events) Rows
FROM events_20210101 22,096
FROM events_20210101, UNNEST(event_params) 215,602
FROM events_20210101, UNNEST(items) 18,716
FROM events_20210101 LEFT JOIN UNNEST(items) 38,427
  • UNNEST(event_params) gives one row per parameter, about ten per event. Any COUNT(*) or SUM after it is ten times too large. Use it only to explore keys, as in the type query above.
  • , UNNEST(items) drops every event without items. 19,711 of the 22,096 events had none, and they disappear. Use LEFT JOIN UNNEST(items) when the other events must stay.

The items array

The items array

Ecommerce events carry products in items, which is itself an array of records. Unnest it when the question is about products, and keep event-level totals out of that query:

SELECT
  i.item_name,
  SUM(i.quantity) AS quantity,
  ROUND(SUM(i.item_revenue_in_usd), 2) AS revenue
FROM `bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_*`,
  UNNEST(items) AS i
WHERE _TABLE_SUFFIX BETWEEN '20210101' AND '20210107'
  AND event_name = 'purchase'
GROUP BY i.item_name;

ecommerce.purchase_revenue_in_usd is an order total. Summed after unnesting items, it is counted once per product line. The result of this query and the fan-out effect are in query examples and JOIN and UNION.

Keep the cost down

Keep the cost down

  • Always filter _TABLE_SUFFIX on events_*. The wildcard tables guide measures the difference.
  • Each subquery over event_params reads the whole array column. Ten parameter columns cost about the same as one; the column is read once. What drives the cost is the number of days.
  • Flatten once. For daily reporting, a scheduled query that turns yesterday’s events into a table with one column per parameter you use is cheaper and simpler than unnesting in every report. The export itself is covered in the GA4 to BigQuery guide.

One flat table, one definition

One flat table, one definition

Every team that uses the GA4 export ends up writing the same unnesting: page location, session ID, source and medium, the session key. Copied into each report, it drifts: one copy reads session_engaged as a string only, another forgets the user in the session key. In OWOX Data Marts that flattening query is published once as a Data Mart, with a description per column, and reports in Google Sheets or dashboards read the flat result. OWOX Data Marts runs your SQL as written, in your own BigQuery project, and does not transform the data. Arrays in general are in the array functions guide.

Your New Normal

Turn your data into decisions.

Governed data marts give you the clean foundation ML needs to actually work.

  • No AI hallucinations
  • Analyst-governed definitions
  • Every number traces to SQL
Get started free

FAQ

Frequently Asked Questions

How do I count GA4 sessions in BigQuery?

A session is the pair of user_pseudo_id and the ga_session_id parameter: COUNT(DISTINCT CONCAT(user_pseudo_id, '-', CAST(ga_session_id AS STRING))). ga_session_id alone is not unique, because two users can start a session at the same moment.

How do I get page_location from the GA4 export in BigQuery?

With a subquery over event_params: (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page_location. It returns one value per event and does not multiply rows.

What are event_params in the GA4 BigQuery Export?

An array of key-value pairs on each event. Each value sits in one of four fields, string_value, int_value, float_value or double_value, depending on its type; page_location is a string and ga_session_id an integer.

Why does my GA4 parameter come back NULL in BigQuery?

Usually the wrong value field: ga_session_id is stored in int_value, so reading string_value returns NULL with no error. Some parameters arrive in two types: in Google's sample, session_engaged was a string 17,890 times and an integer 2,200 times in one day. Read both with COALESCE.

Why does UNNEST multiply rows in GA4 queries?

Joining the table to UNNEST(event_params) creates one row per parameter: 22,096 events became 215,602 rows in our test, so counts and sums after it are about ten times too large. A comma join to UNNEST(items) also drops events with no items; use LEFT JOIN UNNEST(items) to keep them.

Who wrote this

Alyona Samovar

Alyona Samovar · Senior Digital Analyst

Alyona Samovar is a Senior Digital Analyst who spent years at OWOX helping enterprise clients design analytics systems, build BigQuery reporting pipelines, and optimize marketing measurement. She specializes in SQL-based analytics, data modeling, and turning raw marketing data into actionable insights. Alyona writes about practical analytics workflows, BigQuery best practices, and data-driven decision-making.