Independent GA4 education, written for understanding18 comprehensive guides • 2026 edition
The GA4Reader

BigQuery export: work with raw GA4 event data

Understand the export schema, nested parameters, timestamps, SQL unnesting, and cost-aware analysis.

BY THE END OF THIS GUIDE

Navigate the GA4 export schema

Unnest event parameters safely

Write a cost-aware event query

01

The export is event-level, not a copy of reports

The GA4 BigQuery export provides raw, structured event records. Each row represents an event and includes fields such as event_name, event_timestamp, user_pseudo_id, session-related fields, device, geography, ecommerce items, and nested event parameters. It does not simply reproduce every standard GA4 report table.

Daily export tables are commonly named events_YYYYMMDD; streaming export can also provide intraday tables for fresher data. Export begins after linking, so plan ahead if you will need raw historical analysis. Check your project’s location, permissions, billing, retention, and privacy controls before enabling a production workflow.

  • One row per event.
  • event_params contains repeated key/value records.
  • items contains repeated ecommerce item records.
  • event_timestamp is stored in microseconds.
  • user_pseudo_id is a pseudonymous identifier, not a person’s name or email.
02

UNNEST turns nested fields into usable rows

BigQuery stores event parameters as repeated records. UNNEST lets you read each parameter key and its typed value fields. A parameter’s value may appear in string_value, int_value, float_value, or double_value depending on what was sent. Choose the field that matches your implementation and keep types consistent.

Use a correlated subquery when you want one parameter value without multiplying event rows. A cross join with UNNEST can be useful when you intentionally want one row per parameter or item, but it changes the grain of the result. Always state whether your output rows represent events, parameters, items, users, or sessions.

WORKED EXAMPLE
SELECT
  event_date,
  event_name,
  (
    SELECT value.string_value
    FROM UNNEST(event_params)
    WHERE key = 'form_name'
  ) AS form_name
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
  AND event_name = 'generate_lead';
ANNOTATED SCREENSHOT WALKTHROUGH

Inspect a GA4 SQL query and its result grain

Illustrative interface
learning.example / bigquerySCREENSHOT

Daily orders and item units

SQL workspace • GA4 export example

1–30 Nov 2026
Rows scanned1.2M
Result rows31
Query statusValidatedsmall period

Orders and units by day

event_dateOrdersUnits
2026-11-01184312
2026-11-02196338
2026-11-03172296
2026-11-04208354

Query review

Table suffix20261101–20261130

Row grainOne row per item

OrdersDISTINCT transaction_id

UNNESTitems!

STEP 1 OF 3

Validate the output shape

Daily orders and units answer different questions. The chart is checked against known orders before automation.

Interactive, product-style screenshot created for learning. It uses illustrative data and is not a live Google Analytics interface.
03

Reconcile before you automate

Raw queries and GA4 interface reports may differ because of processing, identity, attribution, thresholds, filters, time zones, and metric definitions. Start with simple event counts, document your filters, and reconcile against a known period. Do not assume the interface is wrong when your first SQL count differs.

Control query cost by selecting only needed columns, filtering _TABLE_SUFFIX, and avoiding unbounded SELECT * scans on large tables. Build reusable, reviewed queries or table functions for common definitions. Raw access increases flexibility, but it also makes you responsible for every metric rule.

04

Read the export schema as nested event records

Start by inspecting one small day and a handful of fields. Top-level columns describe the event and its context, while repeated records hold parameters and items. Some fields are records with nested subfields, so a flat spreadsheet mental model will not be enough.

event_name tells you the action; event_timestamp gives event time in microseconds; event_date provides a date partition useful for controlled queries. user_pseudo_id supports pseudonymous event-level analysis, while ga_session_id inside event_params is commonly used for session grouping when present. Validate your own implementation rather than assuming every field is always populated.

Decide the grain of every query result before writing SELECT. One row per event, one row per item, one row per session, and one row per user require different expansions and aggregations. Many inflated metrics come from unnesting items and then counting events without deduplication.

VISUAL EXPLAINERGA4 export rows contain nested records
ONE EVENT ROWevent_nameevent_timestampuser_pseudo_idevent_params [ ]key + typed valueitems [ ]item_id + price + quantityUNNEST → one row per repeated record
Unnesting parameters or items changes the row grain and can multiply counts.
Reference table for this chapter
FieldRoleCommon caution
event_timestampEvent time in microsecondsConvert deliberately and document time zone
event_paramsRepeated event parameter recordsValue is stored in typed fields
itemsRepeated ecommerce item recordsExpansion multiplies event rows
user_pseudo_idPseudonymous identifierNot a universal person ID
event_date / suffixDaily table organizationBusiness days may need local-time logic
05

Write session and user aggregations without changing the grain accidentally

To count sessions, extract the session identifier and combine it with the appropriate pseudonymous user key where your design requires it. Counting only a session number across users can merge unrelated sessions. To count users, count distinct user_pseudo_id while acknowledging its identity limitations.

Build incrementally: first filter events, then create a base table with one row per event and needed fields, then aggregate. Common table expressions make the grain of each stage visible. Review intermediate counts before adding another join or UNNEST.

When joining conversion events back to sessions, avoid duplicating session metrics. Aggregate events to one row per session first, then join to a session-level table. This pattern keeps session counts and session acquisition values stable.

WORKED EXAMPLE
WITH events_base AS (
  SELECT
    user_pseudo_id,
    (SELECT value.int_value FROM UNNEST(event_params)
      WHERE key = 'ga_session_id') AS session_id,
    event_name,
    event_timestamp
  FROM `project.analytics_123456789.events_20260131`
), sessions AS (
  SELECT user_pseudo_id, session_id
  FROM events_base
  WHERE session_id IS NOT NULL
  GROUP BY user_pseudo_id, session_id
)
SELECT COUNT(*) AS observed_sessions FROM sessions;
06

Analyze ecommerce items with explicit deduplication

Unnesting items creates one row per item within each purchase event. This is ideal for product revenue and quantity analysis, but counting purchase events after expansion requires a distinct transaction identifier. Keep transaction_id in the item-level base so order metrics can be recovered correctly.

Check whether your business metric uses item revenue, purchase event value, quantity, tax, shipping, or refunds. These are not interchangeable. Define revenue in the query comments and reconcile a sample of known orders with the commerce system.

For product performance, decide whether the denominator is item views, add-to-cart events, purchasing users, or orders containing the item. “Product conversion rate” is ambiguous until that denominator is written down.

WORKED EXAMPLE
SELECT
  item.item_id,
  item.item_name,
  SUM(item.quantity) AS units,
  COUNT(DISTINCT transaction_id) AS orders_containing_item
FROM `project.analytics_123456789.events_20260131`,
  UNNEST(items) AS item
WHERE event_name = 'purchase'
  AND transaction_id IS NOT NULL
GROUP BY item.item_id, item.item_name
ORDER BY units DESC;
07

Create a reconciliation and cost-control checklist

Compare a small set of definitions against the interface: event count by day, sessions using your documented logic, purchasers, and purchase count by transaction ID. Investigate processing delays, filters, identity, attribution, time zones, and thresholding before assuming one source is defective.

Control cost by selecting named columns, restricting table suffixes, avoiding repeated scans of large raw tables, and materializing reviewed intermediate tables when justified. Test complex queries on small periods first. Query cost and freshness are architectural decisions, not afterthoughts.

Version-control production SQL with metric definitions and owners. A query that produces the right number once is not yet a governed data product. Add tests for null identifiers, duplicate transactions, date boundaries, and unexpected row growth.

Reference table for this chapter
CheckWhy it mattersFailure signal
Row grain after UNNESTPrevents duplicated countsPurchases exceed unique transactions
Time-zone logicAligns business daysDaily totals shift at midnight
Typed parameter valuesReads the correct value fieldUnexpected null metrics
Table suffix filtersControls scanned dataUnexpected query cost
Definition reconciliationExplains interface differencesUnowned metric discrepancies
08

Worked example: calculate daily orders and item units together

A retailer needs one table per day containing unique orders and units sold. Because items are nested, unnesting items is necessary for units but can duplicate order counts. The safe approach is to preserve transaction_id at item level, count distinct transactions, and sum item quantity with the approved refund and cancellation exclusions.

The query should also define the business date. If stakeholders operate in the property’s local time zone, relying only on export event_date may create boundary differences. Build and validate a timestamp conversion rather than silently mixing UTC and local-day logic.

Reconcile a small set of order IDs with the commerce system before automating the query. Check multi-item orders, quantity greater than one, partial refunds, duplicate delivery, and missing transaction IDs.

VISUAL EXPLAINERGA4 export rows contain nested records
ONE EVENT ROWevent_nameevent_timestampuser_pseudo_idevent_params [ ]key + typed valueitems [ ]item_id + price + quantityUNNEST → one row per repeated record
Unnesting parameters or items changes the row grain and can multiply counts.
WORKED EXAMPLE
WITH item_rows AS (
  SELECT
    event_date,
    transaction_id,
    item.item_id,
    item.quantity
  FROM `project.analytics_123456789.events_20260131`,
    UNNEST(items) AS item
  WHERE event_name = 'purchase'
    AND transaction_id IS NOT NULL
)
SELECT
  event_date,
  COUNT(DISTINCT transaction_id) AS orders,
  SUM(quantity) AS units
FROM item_rows
GROUP BY event_date
ORDER BY event_date;
09

SQL review checklist before a query becomes a metric

A reviewed query is a metric definition in code. It should state the source tables, date logic, filters, row grain at each stage, parameter types, deduplication keys, and expected aggregation. Comments should explain business choices rather than merely repeating SQL keywords.

Run the query on a small known period and compare intermediate row counts. Look specifically for the moment a join or UNNEST changes the grain. Production scheduling also needs an owner, cost expectation, freshness rule, and failure alert.

  • Source table suffix and business time zone are explicit.
  • Every CTE has a stated row grain.
  • UNNEST is used only when repeated-record expansion is intended.
  • Orders and users use documented distinct identifiers.
  • Typed parameter fields match the implementation.
  • Revenue, refunds, and cancellations have explicit rules.
  • Small-period reconciliation and query-cost checks passed.
FIELD EXERCISE

Write a daily lead-event query plan

Plan a query that returns daily generate_lead counts by form_name for one calendar month.

  1. Restrict the table suffix to the required dates.
  2. Filter event_name before aggregation.
  3. Unnest only form_name with the correct value type.
  4. Group by event_date and form_name.
  5. Document the row grain, time zone, and one reason the total may differ from the interface.
REVIEW YOUR UNDERSTANDING

Why might an UNNEST cross join increase the number of rows?

FURTHER READING

Continue with the primary documentation.

GA4 BigQuery exportBigQuery documentation