Skip to content

Pitfall #5: Misunderstanding the Intraday Table

By Timo Dechau · Last updated March 25, 2026

You’re building a dashboard that needs “today’s” data. You query events_* with today’s date in the _TABLE_SUFFIX filter and get… nothing. Or you get data, but it’s from yesterday. So you dig around, discover events_intraday_*, and start querying that instead. Problem solved, right? Until tomorrow, when someone notices your numbers for yesterday are either missing events or double-counting them. You’ve walked into one of GA4’s quieter traps.

GA4’s BigQuery export actually produces two sets of tables, and they have a lifecycle most people never read about:

  1. events_intraday_YYYYMMDD — Created during the day. Contains events as they stream in, refreshed multiple times per hour. This is your “live-ish” data. It’s fresh but incomplete.

  2. events_YYYYMMDD — Created once Google finishes processing the full day’s data. This is the final, complete table for that date.

Here’s the critical part: when events_20260325 is finalized, events_intraday_20260325 gets deleted. The intraday table is temporary. It exists only while the day is still being processed.

The timing of this handoff is where things get messy. Google doesn’t guarantee when the daily table will appear. It’s usually within 24-72 hours after the day ends, but I’ve seen it take longer. During that gap, the intraday table is your only source for that date. After the handoff, it’s gone and the daily table is your only source.

Two common mistakes:

  • Querying only events_* and wondering why “today” has no data (because the daily table for today doesn’t exist yet — the intraday table does)
  • Querying both events_* and events_intraday_* with a UNION ALL and accidentally double-counting the overlap period where both exist for the same date

The cleanest pattern is a query that checks both tables but avoids overlap. The logic: for any given date, use the daily table if it exists; fall back to intraday only if the daily table doesn’t exist yet.

SELECT * FROM (
-- Final daily tables
SELECT
event_timestamp,
event_name,
user_pseudo_id,
event_params,
'daily' AS source_table
FROM
`your_project.analytics_123456789.events_*`
WHERE
_TABLE_SUFFIX BETWEEN '20260320' AND '20260325'
UNION ALL
-- Intraday for dates not yet finalized
SELECT
event_timestamp,
event_name,
user_pseudo_id,
event_params,
'intraday' AS source_table
FROM
`your_project.analytics_123456789.events_intraday_*`
WHERE
_TABLE_SUFFIX BETWEEN '20260320' AND '20260325'
AND _TABLE_SUFFIX NOT IN (
SELECT DISTINCT _TABLE_SUFFIX
FROM
`your_project.analytics_123456789.events_*`
WHERE
_TABLE_SUFFIX BETWEEN '20260320' AND '20260325'
)
)

The subquery in the NOT IN clause ensures you only pull intraday data for dates where no finalized daily table exists. Once the daily table lands, the intraday rows for that date are excluded automatically.

For scheduled pipelines, the more robust approach is to check INFORMATION_SCHEMA.TABLES for table existence before deciding which to query. But for ad-hoc analysis, the pattern above is reliable and readable.

One more thing: if you’re building materialized models (say, in dbt or Dataform), design your incremental logic around this handoff. Process intraday data provisionally, then reprocess once the daily table drops. The daily table is authoritative — it may contain events that the intraday table missed, and it applies final processing that intraday doesn’t.

Run this to compare row counts between the intraday and daily table for the most recent date where both exist. If both return rows for the same date, you need the deduplication pattern:

SELECT
'daily' AS table_type,
_TABLE_SUFFIX AS date_shard,
COUNT(*) AS row_count
FROM
`your_project.analytics_123456789.events_*`
WHERE
_TABLE_SUFFIX >= FORMAT_DATE(
'%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
)
GROUP BY _TABLE_SUFFIX
UNION ALL
SELECT
'intraday' AS table_type,
_TABLE_SUFFIX AS date_shard,
COUNT(*) AS row_count
FROM
`your_project.analytics_123456789.events_intraday_*`
WHERE
_TABLE_SUFFIX >= FORMAT_DATE(
'%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
)
GROUP BY _TABLE_SUFFIX
ORDER BY date_shard, table_type

Look at the output. For recent dates, you should see either a daily row or an intraday row — not both. If you see both for the same date_shard, that’s the overlap window. Any query spanning that period without the deduplication logic above is double-counting.