Skip to main content

Migrate the GA4 Export Dataset to Another GCP Project

Sometimes the GA4 BigQuery export ends up in the wrong place - an agency-managed project you want under internal control, a suboptimal cloud region, a sandbox that is being decommissioned, or a setup you want consolidated for governance reasons. This guide moves the export (including its history) to a new GCP project and gets GA4Dataform running there.

On a high level, the migration needs two things: repoint the GA4-BigQuery link, and transfer the historical tables. Then reinstall GA4Dataform in the new project.

Deep dive

This guide is based on our article How to Migrate Your GA4 BigQuery Export to Another GCP Project, which walks through the options in more detail.

Before you start: access you need

  • BigQuery Data Viewer on the source dataset (the tables you are copying)
  • BigQuery Data Editor and BigQuery Job User on the target project (where the data is going)
  • Editor access to the GA4 property (to change the BigQuery link)
  1. In GA4, go to Admin → Product links → BigQuery links.
  2. Delete the existing link to the old project.
  3. Create a new link and select the new GCP project, with the same location (region) as the old dataset.
Relink immediately

Create the new link right after deleting the old one. If you miss a day's export, that day's data is lost for good - with Streaming enabled you might salvage part of it, but don't rely on that.

From the next daily export, GA4 creates a dataset named analytics_<property_id> in the new project and writes new events_* tables there. Deleting a link only stops future exports - nothing is deleted from the old project.

Step 2: Copy the historical data

The GA4 export is date-sharded (one events_YYYYMMDD table per day), which rules out manually copying tables in the BigQuery UI - good luck clicking through hundreds of shards. Pick one of these instead:

Option A: bq cp loop in Cloud Shell

bq cp copies one table at a time, so wrap it in a loop over the date shards:

#!/bin/bash

SOURCE_PROJECT="source-project-id"
SOURCE_DATASET="analytics_123456789"
TARGET_PROJECT="target-project-id"
TARGET_DATASET="analytics_123456789"

START_DATE="20240322"
END_DATE="20240507"

current_date=${START_DATE}

while [ "$current_date" -le "$END_DATE" ]; do
echo "Copying events_${current_date}"
bq cp -f --project_id=${SOURCE_PROJECT} \
${SOURCE_PROJECT}:${SOURCE_DATASET}.events_${current_date} \
${TARGET_PROJECT}:${TARGET_DATASET}.events_${current_date}

current_date=$(date -I -d "$current_date + 1 day" | sed 's/-//g')
done

Adjust the first six variables. The script covers the events_* shards; extend it if you also want other sharded tables from the export.

Option B: SQL CREATE TABLE COPY loop

The same looping logic in pure SQL - no Cloud Shell needed, run it straight in the BigQuery console:

DECLARE start_date  DATE DEFAULT DATE('2024-05-01');
DECLARE end_date DATE DEFAULT DATE('2024-05-15');

DECLARE source_project STRING DEFAULT('source-project');
DECLARE source_dataset STRING DEFAULT('analytics_123456789');

DECLARE target_project STRING DEFAULT('target-project');
DECLARE target_dataset STRING DEFAULT('analytics_123456789');

DECLARE daterange ARRAY<DATE> DEFAULT GENERATE_DATE_ARRAY(
start_date, end_date, INTERVAL 1 DAY
);

DECLARE statement STRING DEFAULT(
'CREATE OR REPLACE TABLE ' ||
'`' || target_project || '.' || target_dataset || '.' || 'events_{suffix}` ' ||
'COPY ' ||
'`' || source_project || '.' || source_dataset || '.' || 'events_{suffix}`;'
);

DECLARE i INT64 DEFAULT 0;
DECLARE date DATE;
DECLARE suffix STRING;
DECLARE query STRING;

LOOP
SET i = i + 1;

IF(i > ARRAY_LENGTH(daterange)) THEN LEAVE;
END IF;

SET date = daterange[ORDINAL(i)];
SET suffix = FORMAT_DATE('%Y%m%d', date);
SET query = REPLACE(statement, '{suffix}', suffix);

EXECUTE IMMEDIATE query;
END LOOP;

Whichever option you pick

  • Don't copy events_intraday_* tables. They are transient; the new export produces its own.
  • Don't overwrite newer shards. If the new export already produced events_YYYYMMDD tables, keep those and only copy the older ones.
  • Keep the region consistent. The copied dataset must end up in the region you plan to run GA4Dataform in.

Step 3: Reinstall GA4Dataform in the new project

Run the installer against the new project (see the Quick Start Guide), or set up the Community version manually. Re-apply your custom configuration from the old installation - everything under definitions/custom/ and includes/custom/.

Then rebuild the output tables over the full history (see Rebuild Tables).

Step 4: Validate and clean up

Before deleting anything, compare the two projects:

-- run in both projects and compare
SELECT _TABLE_SUFFIX AS day, COUNT(*) AS events
FROM `analytics_<property_id>.events_*`
GROUP BY day
ORDER BY day;

When the day-by-day counts match, you can delete the old dataset and the old GA4Dataform installation (see Delete Dataform repository) to stop storage costs.

Premium alternative: no migration at all

If your goal is only to keep outputs in a different project than the export, you don't need to migrate anything - Premium's Cross-Project Setup reads the export from one project and writes outputs to another.