Migrating from Google BigQuery Legacy to Google BigQuery V2

Updated by James Dietrich

Google BigQuery Legacy will be switched off on 1 January 2027, so move to Google BigQuery V2 before that date.

This guide is for anyone who queries Dreamdata data in BigQuery Legacy, or who maintains dashboards, scheduled queries or pipelines that read from it.

The move has two parts and you can do them separately:

  1. Access. You subscribe to a V2 listing in your own Google Cloud project, instead of reading a dataset shared from Dreamdata's project.
  2. Queries. The V2 schema is different from Legacy. There are six tables instead of ten, and many flat columns are now nested records.

Legacy and V2 can run side by side while you rewrite your queries, so you don't need to switch over in one go.

Why move to V2?

  • You control access. The dataset lives in your own project, and you manage who can see it with your own IAM (Identity and Access Management) settings.
  • No billing project setup. On Legacy you have to set your own billing project, or queries fail with a permissions error. On V2 this step isn't needed.
  • Lower query costs. V2 tables are partitioned by month on timestamp, and events, spend and stages are also clustered, so a query over a date range reads less data.
  • New data. Signals, stage transitions, journey settings, branded-search labels, ad accounts and the Google advertising channel are only available in V2. Legacy will not get new features.
  • Field descriptions. Every V2 column has a description you can read in the BigQuery console.

A note on names

The integration is called Google BigQuery V2 in the Dreamdata Platform, but the dataset in the listing is called data_platform_v3. You choose your own dataset name when you subscribe, so the name in your project may be different.

This guide uses your_project.your_dataset for your V2 dataset and dreamdata.your_legacy_dataset for your Legacy dataset. Replace them with your own names.

Before you start

You need a Google Cloud project and these three permissions in it:

  • serviceusage.services.enable
  • analyticshub.listings.subscribe
  • bigquery.datasets.create

To run queries on the V2 dataset, you also need the BigQuery Job User role.

If your Google Cloud project is in the US, you need one extra step. The V2 listing is in the EU, and Dreamdata provides a US copy of it. To use the US copy, run this command in the project where you subscribed. Replace dreamdata with your dataset name. This step is free.

ALTER SCHEMA `dreamdata`
          ADD REPLICA IF NOT EXISTS `us`
          OPTIONS(location='us');

Make a list of everything that reads your Legacy dataset, so you know what to rewrite. This could include:

  • Scheduled queries
  • dbt models
  • Looker Studio or Power BI dashboards
  • Reverse ETL jobs or other pipelines

Migration steps

Set up V2 first, then rewrite and check your queries while Legacy keeps running.

  1. In the Dreamdata Platform, go to Data Platform → Data Access → Google BigQuery V2.
  2. Add at least one Google user or group email. A group email means access stays in place when someone leaves your team.
  3. Wait for the next full data modelling run to finish. The See your Listing button stays greyed out until then.
  4. Click See your Listing to open the Analytics Hub listing, choose your project, and give the dataset a name.
  5. Rewrite your queries against the V2 dataset. See Rewriting your queries below.
  6. Run the checks in Checking your numbers below on both datasets and compare the results.
  7. Point your dashboards and pipelines at V2.
  8. Contact Dreamdata support to switch off your Legacy dataset.

For more detail on steps 1 to 4, see Google BigQuery V2.

Which table replaces which

The ten Legacy tables become six V2 tables, and one Legacy table has no V2 equivalent.

Legacy table

V2 table

What to know

companies

companies

Company attributes move into a properties record. Three company IDs become one.

contacts

contacts

One row per contact. Companies move into a repeated companies array, so there are fewer rows.

events

events

Event and session fields move into nested event and session records.

sessions

events

There is no separate sessions table. Use WHERE dd_event_session_order = 1.

session_performance

attribution

paid_session_performance

attribution

Filter on session.spend_source IS NOT NULL.

revenue_attribution

attribution

One row per attribution model becomes one row with an array of models.

revenue

stages

Revenue is called stages in V2.

paid_ads

spend

Ad fields move into ad_hierarchy and context records.

google_search

None

There is no V2 equivalent.

New in V2

Field

What it is

events.signals

The signals attached to each event, with their category and whether they count towards the engagement score. You set up signals in the Dreamdata Platform.

stages.stage_transitions

Every later stage reached by the same company or contact. You can use it for funnel conversion queries without joining a table to itself.

stages.journey

The journey type, start date and end date for each stage.

companies.properties.engagement_score

A rolling 30-day engagement score from 0 to 1.

session.dd_brand_search_label

Whether a search was a brand or non-brand search.

session.google_advertising_channel

Google's own channel type: Search, Display, Video or Demand_Gen.

ad_account

The ad account name and ID, on spend and in the session ad hierarchy.

spend.ad_hierarchy.level_3

The ad or keyword level, which is the most detailed level an ad network provides.

row_checksum

A value that changes only when something in the row changes. You can use it to load only changed rows instead of reloading whole tables.

Removed in V2

These Legacy fields have no V2 replacement.

Removed

Notes

google_search table

The whole table.

page_content

Page text on sessions and attribution.

capterraCategory

On paid_session_performance.

industry_raw, seniority_raw

Only the mapped values industry and seniority are kept.

tracking, is_deal_contact

On the attribution tables.

is_website_unique, last_activity_timestamp

On companies.

event_channel, event_source

V2 has event.campaign and event.medium, but channel and source are only available at session level.

dd_event_rank

dd_event_session_order gives the order within a session only.

browserVersion, osVersion on attribution

Still available on events.

adHierarchy_level_3_* on events and attribution

Level 3 is only available on spend.

event_properties, company_properties, contact_properties, stage_properties

Custom properties are now only on companies, contacts and stages.

Rewriting your queries

Some changes make a query fail with an error, which is easy to spot. Others let the query run but return different numbers, so check these carefully.

Changes that affect your numbers

1. Attribution models are in an array. In Legacy, revenue_attribution has one row per attribution model, and you pick a model with WHERE attributionModel = 'Last Touch'. In V2, attribution has one row per stage and session, with all models in a repeated attribution field. Use UNNEST and filter on model. If you don't filter on a model, your totals are multiplied by the number of attribution models on your account.

Legacy

V2

attributionModel

attribution.model

attributableRevenue

attribution.value

attributableDeal

attribution.weight

2. Events are stored once per company. When a contact belongs to more than one company, V2 stores the same event once for each company. dd_event_id and dd_session_id are unique per row, not per real event. To count events, use COUNT(DISTINCT dd_event_activity_id). To count sessions, use COUNT(DISTINCT dd_session_activity_id). Don't use dd_is_primary_event for counting, because it is deprecated.

3. Company and contact details are not on events or attribution. Legacy had about 25 company and contact columns on every event and attribution row, such as companyName, industry, number_of_employees, jobTitle and seniority. In V2, join companies on dd_company_id for company details, and join contacts on dd_contact_id for contact details.

The attribution table is session-level only and has no event columns, such as event, url or utm_*. Use events for those.

4. Revenue is now called stages.

Legacy

V2

revenue table

stages table

revenueModel

stage_name, or stage.name on attribution

revenue

value, or stage.value on attribution

revenueTimestamp

timestamp, or stage.timestamp on attribution

dealId

dd_object_id

lookback_start_date

journey.start_date

journey.start_date works differently from lookback_start_date. V2 has a journey record with type, start_date and end_date, and start_date is empty (NULL) when the journey covers all tracked data.

5. Several IDs are now one.

Legacy

V2

companyId, unifiedCompanyId, anonymousCompanyId

dd_company_id. Anonymous companies have properties.is_anonymous_company set on companies.

userId, anonymousId, sessionUserId

dd_visitor_id, plus dd_contact_id for known contacts

sessionId

Removed, because it was not unique

6. Ad impressions are counted differently. Rows with dd_tracking_type = 'exposure' are impressions, such as linkedin_ad_impression. These rows have an empty dd_session_activity_id and a quantity above 1.

COUNT(DISTINCT dd_session_activity_id) leaves these rows out, so session counts can be much lower than in Legacy if you run LinkedIn ads. To count real sessions, filter on dd_tracking_type = 'activity'. To count impressions, use SUM(quantity) on the exposure rows.

Changes that cause errors

  • Column names change from camelCase to snake_case, and IDs start with dd_.
  • The event column is now event_name.
  • conversion is now event.is_conversion, and conversionType is now event.conversion_name.
  • custom_properties is a JSON column instead of a string, so use JSON_VALUE() instead of JSON_EXTRACT_SCALAR().
  • Ad hierarchy fields are nested. For example, adHierarchy_level_1_name is now session.ad_hierarchy.level_1.name.

Filter on dates to keep costs down

V2 tables are partitioned by month on timestamp, so always filter on timestamp. A query without a date filter still runs, but it reads the whole table.

On attribution, timestamp is the session start time. Filtering only on stage.timestamp does not reduce the data read.

Example: attributed revenue by channel

Legacy

SELECT
            channel,
            ROUND(SUM(attributableRevenue), 0) AS attributed_revenue,
            ROUND(SUM(attributableDeal), 2)    AS attributed_deals
          FROM `dreamdata.your_legacy_dataset.revenue_attribution`
          WHERE attributionModel = 'Last Touch'
            AND revenueModel     = 'Closed Won'
            AND revenueTimestamp >= '2026-01-01'
          GROUP BY channel
          ORDER BY attributed_revenue DESC

V2

SELECT
            a.session.channel       AS channel,
            ROUND(SUM(m.value), 0)  AS attributed_revenue,
            ROUND(SUM(m.weight), 2) AS attributed_deals
          FROM `your_project.your_dataset.attribution` AS a,
               UNNEST(a.attribution) AS m
          WHERE m.model            = 'Last Touch'
            AND a.stage.name       = 'Closed Won'
            AND a.stage.timestamp >= '2026-01-01'
          GROUP BY channel
          ORDER BY attributed_revenue DESC

Example: events by company industry and size

Legacy

SELECT
            industry,
            number_of_employees,
            COUNT(*) AS events
          FROM `dreamdata.your_legacy_dataset.events`
          WHERE timestamp >= '2026-06-01' AND timestamp < '2026-07-01'
            AND industry IS NOT NULL
          GROUP BY industry, number_of_employees
          ORDER BY events DESC

V2

SELECT
            c.properties.industry            AS industry,
            c.properties.number_of_employees AS number_of_employees,
            COUNT(*)                         AS events
          FROM `your_project.your_dataset.events` AS e
          JOIN `your_project.your_dataset.companies` AS c
            ON c.dd_company_id = e.dd_company_id
          WHERE e.timestamp >= '2026-06-01' AND e.timestamp < '2026-07-01'
            AND c.properties.industry IS NOT NULL
          GROUP BY industry, number_of_employees
          ORDER BY events DESC

This example uses COUNT(*) so the result matches Legacy. To count unique events, use COUNT(DISTINCT e.dd_event_activity_id).

Example: sessions per month by channel

Legacy

SELECT
            DATE_TRUNC(DATE(timestamp), MONTH) AS month,
            channel,
            COUNT(*) AS sessions
          FROM `dreamdata.your_legacy_dataset.sessions`
          WHERE timestamp >= '2026-04-01' AND timestamp < '2026-07-01'
          GROUP BY month, channel
          ORDER BY month, sessions DESC

V2

SELECT
            DATE_TRUNC(DATE(timestamp), MONTH)     AS month,
            session.channel                        AS channel,
            COUNT(DISTINCT dd_session_activity_id) AS sessions
          FROM `your_project.your_dataset.events`
          WHERE timestamp >= '2026-04-01' AND timestamp < '2026-07-01'
            AND dd_event_session_order = 1
            AND dd_tracking_type = 'activity'
          GROUP BY month, channel
          ORDER BY month, sessions DESC

This V2 query counts real sessions only, so it will be lower than Legacy if you have ad impressions. To match the Legacy COUNT(*), remove the dd_tracking_type filter and use COUNT(*).

Checking your numbers

Before you switch over, run these four checks on both datasets and compare the results. Run the Legacy and V2 queries separately, because the two datasets can be in different regions and BigQuery can't combine them in one query.

  1. Attributed revenue by channel. The results should match exactly. If V2 is a multiple of Legacy, the query is missing a filter on the attribution model.
  2. Spend by channel by month. The results should match exactly.
  3. Stage count and total value by stage name. The results should match exactly.
  4. Sessions per month. The V2 rows for activity and exposure should add up to the Legacy total.

Check 1: attributed revenue by channel

-- Legacy
          SELECT channel, ROUND(SUM(attributableRevenue), 0) AS attributed_revenue
          FROM `dreamdata.your_legacy_dataset.revenue_attribution`
          WHERE attributionModel  = 'Last Touch'
            AND revenueModel      = 'Closed Won'
            AND revenueTimestamp >= '2026-01-01' AND revenueTimestamp < '2027-01-01'
          GROUP BY channel ORDER BY channel;
          
          -- V2
          SELECT a.session.channel AS channel, ROUND(SUM(m.value), 0) AS attributed_revenue
          FROM `your_project.your_dataset.attribution` AS a, UNNEST(a.attribution) AS m
          WHERE m.model            = 'Last Touch'
            AND a.stage.name       = 'Closed Won'
            AND a.stage.timestamp >= '2026-01-01' AND a.stage.timestamp < '2027-01-01'
          GROUP BY channel ORDER BY channel;

Check 2: spend by channel by month

-- Legacy
          SELECT DATE_TRUNC(DATE(timestamp), MONTH) AS month, channel,
                 ROUND(SUM(cost), 2) AS cost, ROUND(SUM(clicks), 0) AS clicks
          FROM `dreamdata.your_legacy_dataset.paid_ads`
          WHERE timestamp >= '2026-01-01' AND timestamp < '2027-01-01'
          GROUP BY month, channel ORDER BY month, channel;
          
          -- V2
          SELECT DATE_TRUNC(DATE(timestamp), MONTH) AS month, channel,
                 ROUND(SUM(cost), 2) AS cost, ROUND(SUM(clicks), 0) AS clicks
          FROM `your_project.your_dataset.spend`
          WHERE timestamp >= '2026-01-01' AND timestamp < '2027-01-01'
          GROUP BY month, channel ORDER BY month, channel;

Check 3: stage count and value by stage name

-- Legacy
          SELECT revenueModel AS stage_name, COUNT(*) AS stages, ROUND(SUM(revenue), 2) AS total_value
          FROM `dreamdata.your_legacy_dataset.revenue`
          WHERE revenueTimestamp >= '2026-01-01' AND revenueTimestamp < '2027-01-01'
          GROUP BY stage_name ORDER BY stage_name;
          
          -- V2
          SELECT stage_name, COUNT(*) AS stages, ROUND(SUM(value), 2) AS total_value
          FROM `your_project.your_dataset.stages`
          WHERE timestamp >= '2026-01-01' AND timestamp < '2027-01-01'
          GROUP BY stage_name ORDER BY stage_name;

Check 4: sessions per month

-- Legacy
          SELECT DATE_TRUNC(DATE(timestamp), MONTH) AS month,
                 COUNT(*) AS session_rows, SUM(quantity) AS quantity
          FROM `dreamdata.your_legacy_dataset.sessions`
          WHERE timestamp >= '2026-01-01' AND timestamp < '2027-01-01'
          GROUP BY month ORDER BY month;
          
          -- V2: the activity and exposure rows for each month add up to the Legacy total
          SELECT DATE_TRUNC(DATE(timestamp), MONTH) AS month, dd_tracking_type,
                 COUNT(*)                               AS session_rows,
                 COUNT(DISTINCT dd_session_activity_id) AS sessions,
                 SUM(quantity)                          AS quantity
          FROM `your_project.your_dataset.events`
          WHERE timestamp >= '2026-01-01' AND timestamp < '2027-01-01'
            AND dd_event_session_order = 1
          GROUP BY month, dd_tracking_type ORDER BY month, dd_tracking_type;

The sessions column is 0 on exposure rows, because impressions have no session activity ID.

If your numbers don't match

  • If a total is a whole multiple of the Legacy total, check that you filter on one attribution model after UNNEST.
  • If a session count is much lower than Legacy, check for exposure rows.
  • If a count is slightly higher than Legacy, check that you count distinct activity IDs, because events are stored once per company.
  • If rows are missing, check for an inner join to companies, which leaves out events with no dd_company_id.

Full field mapping

These tables list every Legacy field and where to find it in V2. An empty V2 cell means the field was removed.

companies → companies

Legacy

V2

Note

companyId, unifiedCompanyId

dd_company_id

anonymousCompanyId

dd_company_id

Anonymous companies have properties.is_anonymous_company set.

name

properties.name

createdDate

properties.created_date

website

domain

all_websites

all_domains

unique_websites

Use all_domains.

owner_email, owner_name

account_owner.email, account_owner.name

annual_revenue, number_of_employees, industry, country, linkedin_url, engagement_score, is_from_primary_crm

properties.* with the same name

industry_raw, is_website_unique, last_activity_timestamp

Removed.

data_source

source_system.source

Now a repeated record.

source_system.id

source_system.id

source_system.source_object_id

source_system.id

source_system.source_object

source_system.object

source_system.source_object_url

source_system.object_url

custom_properties

custom_properties

Now JSON instead of a string.

audiences.audience_id

audiences.dd_audience_id

audiences.created_on

audiences.created_date

contacts → contacts

Legacy

V2

Note

dd_contact_id, email

Same name

dd_company_contact_id

Removed. There is now one row per contact.

companyId, anonymousCompanyId

companies[].dd_company_id

Now a repeated array.

is_primary_company

companies[].is_primary_company

website

companies[].domain

createdDate

properties.created_date

corporateEmail

properties.is_corporate_email

jobTitle

properties.title

country, role, name, first_name, last_name, seniority, additional_emails

properties.* with the same name

seniority_raw

Removed.

data_source

source_system.source

Now a repeated record.

source_system_id

source_system.id

custom_properties

custom_properties

Now JSON instead of a string.

audiences.audience_id

audiences.dd_audience_id

audiences.created_on

audiences.created_date

events → events

The sessions table maps to events in the same way, using WHERE dd_event_session_order = 1. Its page_content field was removed.

Legacy

V2

Note

dd_event_id

dd_event_id

Unique per company copy. Use dd_event_activity_id to count.

dd_activity_id

dd_event_activity_id

dd_session_id

dd_session_id

Use dd_session_activity_id to count.

dd_event_order_in_session, event_order_in_session

dd_event_session_order

dd_event_rank

Removed.

companyId, unifiedCompanyId, anonymousCompanyId

dd_company_id

userId, anonymousId, sessionUserId

dd_visitor_id

Plus dd_contact_id for known contacts.

sessionId

Removed.

eventId

source_system.id

event

event_name

event_type

dd_tracking_type

activity or exposure.

data_source

source_system.source

data_group

Removed.

channel, medium, source, campaign, term, keyword

session.* with the same name

session_term

session.term

event_channel, event_source

Removed.

event_medium, event_campaign

event.medium, event.campaign

session_landing_url

session.landing_page_url

session_landing_urlClean

session.landing_page

session_referrer, session_referrerClean

session.referrer, session.referrer_clean

conversion

event.is_conversion

conversionType

event.conversion_name

content_category

event.content_category

url, urlClean

event.url, event.url_clean

referrer, referrerClean

event.referrer, event.referrer_clean

utm_medium, utm_campaign, utm_source, utm_term

event.utm_*

host

event.host

region, country, city

event.visitor_region, event.visitor_country, event.visitor_city

browser, browserVersion, os, osVersion, device

event.browser, event.browser_version, event.os, event.os_version, event.device

matchType

event.match_type, session.match_type

advertisingChannel

session.google_advertising_channel

campaignGroup

session.ad_hierarchy.level_1.name

ad_group, ad_group_id

session.ad_hierarchy.level_2.name, session.ad_hierarchy.level_2.id

adHierarchy_level_1_*, adHierarchy_level_2_*

session.ad_hierarchy.level_1.*, session.ad_hierarchy.level_2.*

adHierarchy_level_3_*

Removed. Level 3 is only on spend.

facebookAdId, facebookCampaignId, linkedInCampaignId, googleAdsCampaignId

session.ad_hierarchy.level_*.id

One ID per level replaces the per-network columns.

Contact fields such as email, jobTitle, role, first_name, last_name, seniority

Join contacts on dd_contact_id

Company fields such as companyName, industry, annual_revenue, number_of_employees, website, linkedin_url

Join companies on dd_company_id

event_properties, company_properties, contact_properties

Removed. Use custom_properties on companies and contacts.

stages.dealId

stages.dd_stage_id

Join stages to get dd_object_id.

stages.revenueModel

stages.name

stages.revenueTimestamp

stages.timestamp

session_performance, paid_session_performance and revenue_attribution → attribution

All three Legacy tables become one. The attribution table is session-level only and has no event columns.

Legacy

V2

Note

dd_session_id

dd_session_id

Use dd_session_activity_id to count.

companyId, unifiedCompanyId, anonymousCompanyId

dd_company_id

userId, anonymousId, sessionUserId

dd_visitor_id, dd_contact_id

timestamp

timestamp

The session start time. Tables are partitioned on this field.

attributionModel

attribution.model

Now in a repeated array.

attributableRevenue

attribution.value

attributableDeal

attribution.weight

revenue

stage.value

revenueModel

stage.name

revenueTimestamp

stage.timestamp

dealId

Join stages on dd_stage_id, then use dd_object_id

lookback_start_date

Join stages, then use journey.start_date

Works differently. See Revenue is now called stages above.

primary_deal_owner_name, primary_deal_owner_email

Join stages, then use primary_owner.name, primary_owner.email

channel, medium, source, term, keyword, campaign

session.* with the same name

conversion

session.contain_conversion

conversionType

session.first_conversion_name

content_category

session.landing_page_content_category

browser, os, device

session.browser, session.os, session.device

browserVersion, osVersion

Removed. Still on events.

country, region, city

session.visitor_country, session.visitor_region, session.visitor_city

matchType

session.match_type

advertisingChannel

session.google_advertising_channel

adHierarchy_level_1_*, adHierarchy_level_2_*

session.ad_hierarchy.level_1.*, session.ad_hierarchy.level_2.*

adHierarchy_level_3_*

Removed. Level 3 is only on spend.

adNetwork

session.spend_source

event, event_type, url, urlClean, utm_*

Removed. Use events.

page_content, capterraCategory, tracking, is_deal_contact, data_group

Removed.

data_source

source_system.source

Contact and company fields

Join contacts or companies

Same as for events.

event_properties, company_properties, contact_properties

Removed.

stage_properties

Join stages, then use custom_properties

revenue → stages

Legacy

V2

Note

dealId

dd_object_id

Unique within a stage, not across all stages.

companyId

dd_company_id

Can be empty.

revenueTimestamp

timestamp

revenue

value

revenueModel

stage_name

lookback_start_date

journey.start_date

Empty when the journey covers all tracked data.

primary_dd_contact_id

dd_primary_contact_id

contacts[].dd_contact_id

object_contacts[].dd_contact_id

deal_single_email

dd_primary_contact_id

A contact ID instead of an email.

deal_selected_emails

object_contacts[].dd_contact_id

custom_properties

custom_properties

Now JSON instead of a string.

dd_stage_id is new and is the unique ID for each row in stages.

Legacy

V2

Note

timestamp, cost, impressions, clicks, channel, source

Same name

channel still matches session.channel on events and attribution.

adNetwork

adNetwork

Deprecated. Use source.

type

source_system.type

data_source

source_system.source

account

ad_account.name

campaign_group, campaign_group_id

ad_hierarchy.level_1.name, ad_hierarchy.level_1.id

campaign, campaign_id

context.campaign, context.campaign_id

ad_group, ad_group_id

ad_hierarchy.level_2.name, ad_hierarchy.level_2.id

keyword, keyword_id

context.keyword, context.keyword_id

match_type

context.match_type

adHierarchy_level_1_*, _2_*, _3_*

ad_hierarchy.level_1.*, .level_2.*, .level_3.*

The google_search table has no V2 equivalent.

FAQ

Can I keep using Legacy while I move?

Yes. Legacy and V2 run side by side until you ask for Legacy to be switched off, or until 1 January 2027.

Why does the contacts table have fewer rows?

V2 has one row per contact. Legacy had one row per contact and company pair.

Who pays for queries on V2?

Dreamdata pays to store the data. You pay for the queries you run, in the same way as for any other table in your project.

Where can I get help?

Contact Dreamdata support through your account manager.


How did we do?