Beyond the forecast: making sense of traffic changes with BigQuery
TimesFM spots unusual days in BigQuery; ARIMA_PLUS explains why. We tested both on a year of GA360 data to turn traffic drops into clear answers.
Elisa Liu3 Nov 2026
Blog
Elisa Liu3 November 20269 min read

Your Dataform workflow finishes successfully. The tables refresh on schedule. Everything looks fine.
But when you check the data, you notice something strange: the event count keeps growing, even when the source data hasn’t changed.
What’s going on?
One possible cause is a missing value in the merge key: the field Dataform uses to recognise whether a record already exists.
This can be particularly relevant when working with GA4 exports, where user and session identifiers are not always available. If your incremental table revisits recent data, a missing value in the key can cause the same event to be inserted again.
Let’s look at why this happens and how to prevent it.
An incremental table allows you to process a smaller amount of data on each run, rather than rebuilding the whole table every time.
For example, instead of processing all your GA4 history every day, you might choose to refresh only the most recent few days.
This can save time and reduce the amount of data processed. It can also help you capture events that arrive late or have been updated since the previous run.
The period you choose to revisit is often called a lookback window.
For example, a three-day lookback might work like this:
Wednesday and Thursday are processed more than once. That overlap is intentional: it gives the pipeline another opportunity to pick up late-arriving data.
The important question is: when Dataform sees those records again, does it recognise them?
It helps to think of the date filter and the merge key as answering two different questions:
The date filter brings recent records back into the pipeline. The merge key helps Dataform decide whether to update an existing row or insert a new one.
In Dataform, you can specify the matching column using uniqueKey:
config {
type: "incremental",
uniqueKey: ["event_key"]
}
With a matching key, an existing row can be updated. If no matching row is found, a new row is inserted.
Without a uniqueKey, the usual incremental behaviour is to append the rows returned by the query. If the query selects the same days again, those rows can be added again too.
Before choosing a key, ask yourself:
What should one row in this table represent?
For example:
This level of detail is often called the table’s grain. A key that works for a session table may not work for an event table, because a session can contain many events.
When working with GA4 exports, it is tempting to assume every event will have a usable user or session identifier. In practice, that is not always the case.
Consent settings are one possible reason. With advanced consent mode, Google tags can send measurements without analytics cookies when analytics storage is denied. This can affect the identifiers available for linking events to users or sessions.
For example, if your key uses user_pseudo_id or ga_session_id, check what happens when one of those fields is missing.
A missing identifier doesn’t automatically tell you why it is missing. Consent is one possible explanation, but it is worth investigating your implementation and checking the exported privacy_info.analytics_storage field.
It is also worth remembering that an event can still be useful for event reporting, even when it cannot be confidently linked to a user or session. Removing every event with a missing identifier may mean losing useful data.
Imagine a simplified merge key containing these fields:
At first glance, these look like the same event. The timestamp and event name match, and both user identifiers are missing.
However, in SQL, comparing NULL with NULL using = does not return TRUE. As a result, an equality-based merge may not recognise the incoming record as a match.
Here is what can happen:
After several runs, the table may contain several copies of the same event.
The surprising part is that the source data does not need to contain duplicates. They can target row.
One way to address this is to create a key that handles missing values consistently.
The aim is for the same event to produce the same key each time it is processed, even when one of the selected fields is missing.
For example, you could represent the chosen fields in a structured format that includes missing values, then turn that representation into a compact key. There is a SQL example at the end of this article if you want to see how this works in BigQuery.
But handling nulls is only part of the solution.
Imagine replacing every missing user identifier with "unknown".
That may avoid a null value, but it doesn’t mean the resulting key will always be unique. If two different events share all the other fields used in the key, they could end up with the same key.
Similarly, hashing a set of fields does not make the underlying information more complete. A hash simply turns the values you provide into another representation.
A good key needs to be:
Be careful with random keys. If a new random value is generated on every run, the same event will appear to be a new event each time.
Also, a key that allows an event to be processed consistently does not recover a missing user or session identifier. Avoid grouping all events with a missing session ID into one artificial session.
No. Despite its name, uniqueKey should not be treated as a database uniqueness constraint or a source deduplication step.
You still need to check the incoming query. If it returns multiple rows with the same key, a merge may insert duplicates or fail when multiple source rows match an existing target row.
The key tells Dataform how to match rows; it does not guarantee that your source query has produced one row per key.
A reliable key is important, but it is only useful for records that your incremental query actually processes.
Suppose an event happened on Monday but arrived on Wednesday. If your lookback window only covers Tuesday and Wednesday, that Monday event may be missed.
Choose a window that reflects how late data can arrive or change in your source. If older data is corrected outside that window, you may need a targeted backfill.
If you use updatePartitionFilter, remember that it limits which existing rows in the target table are considered during the merge. This can reduce the amount of data scanned, but the filter needs to work alongside your source lookback window.
For example, if your source query reprocesses the last seven days, but the target filter only considers the last three days, an existing row from five days ago may be excluded from the match. Dataform could then insert the incoming row again, even if its key is correct.
Once you have updated your key and incremental logic, test the table before relying on the results.
Run the incremental process, then run it again with unchanged source data.
The second run should not add extra copies or change the expected results. If it does, investigate whether the merge is matching existing records correctly.
Check both the incoming query result and the finished table.
For a table designed to contain one row per key, the following query should return no rows:
SELECT
event_key,
COUNT() AS row_count
FROM project.dataset.table
GROUP BY event_key
HAVING event_key IS NULL OR COUNT() > 1;
Replace the table and key names with your own. To check the incoming data, run an equivalent check against the query result that feeds the merge.
Check that your lookback window captures late-arriving events and that an updated source record is reflected in the target table as expected.
Then compare reporting totals with the underlying records. A pipeline can finish successfully while still producing incorrect results.
Fixing the incremental logic helps prevent the problem from continuing, but it does not automatically remove duplicates already in the table.
You may need to rebuild the affected history and refresh downstream reports. Before doing so, confirm that the source data needed for the rebuild is still available.
Incremental tables can make pipelines more efficient, but they depend on being able to recognise records that have already been processed.
When counts keep growing unexpectedly, check:
The goal is simple: when a record is revisited, the table should recognise it and update it when needed, without quietly counting it again.
The following example shows one way to create a non-null key from a set of fields, including a missing user identifier:
WITH example_event AS (
SELECT
'website_a' AS source_id,
1700000000123456 AS event_timestamp,
'page_view' AS event_name,
CAST(NULL AS STRING) AS user_pseudo_id
)
SELECT
*,
TO_HEX(
SHA256(
TO_JSON_STRING(
STRUCT(
source_id AS source_id,
event_timestamp AS event_timestamp,
event_name AS event_name,
user_pseudo_id AS user_pseudo_id
)
)
)
) AS event_key
FROM example_event;
In this example:
The same values will produce the same key when the expression stays consistent.
This is an illustration of null handling, not a universal GA4 event key. The four fields shown are not guaranteed to distinguish every real event. In production, choose and validate fields that identify the intended row across your data sources and event batches. Keep the field names, types and order consistent across runs.
Reference:
We build and review Dataform pipelines in BigQuery: tested models, assertions and documentation your team can maintain.
Bring data to connect, a question to investigate or something to fix or build. We can agree a focused scope directly.
TimesFM spots unusual days in BigQuery; ARIMA_PLUS explains why. We tested both on a year of GA360 data to turn traffic drops into clear answers.
Elisa Liu3 Nov 2026
A year ago, I wrote about getting a BigQuery warehouse ready for AI ( Easy ways to prepare your BigQuery warehouse for AI) , but didn't look properly into Knowledge Catalog (known as Dataplex at the time). We decided to go back and look through Google's Knowledge Catalog properly: what's actually in there, how it works and what it relates to. What Knowledge Catalog is Knowledge Catalog is Google's metadata layer for BigQuery data (and a few other sources). It sits alongside your tables rather
Katie Kaczmarek15 Sept 2026
Google's Knowledge Catalog has been renamed four times. Data Catalog, then Dataplex Catalog, then BigQuery universal catalog, then Dataplex Universal Catalog, and now Knowledge Catalog, as of 10 April 2026. The API, gcloud and IAM roles still all say "dataplex". If you land on a page that mentions Dataplex and wonder whether you're reading something out of date, you're probably not. That's just the product's fifth name in four years. I spent some time looking through the different sections of K
Katie Kaczmarek15 Sept 2026