← Back to blog
analytics·April 25, 2026·6 min read

Your Google Ads data is more useful than you think

Google Ads told our client their ROAS was 3:1. After connecting it to actual revenue in BigQuery, it was 7:1. They'd been underinvesting for months.

analytics

Your Google Ads data is more useful than you think

Google Ads told our client their ROAS was 3:1. After we connected it to their actual revenue data in BigQuery, it was 7:1. They'd been underinvesting in their best campaign for months.

This is the Google Ads problem in one sentence: the platform shows you what it can measure, which is not the same as what's actually happening.

What you see in the Google Ads UI

The Google Ads dashboard shows you:

  • Impressions, clicks, CTR — how many people saw your ad and clicked
  • CPC — what you paid per click
  • Conversions — events that fired your conversion tag (form submissions, page loads, purchases)
  • Conversion value — the value you manually assigned to those events
  • ROAS — revenue divided by spend, based on the above

For e-commerce with direct purchase tracking, this is reasonably accurate. For B2B, SaaS, or any business with a sales cycle longer than a single session, these numbers are structurally wrong.

Here's why: Google Ads counts a "conversion" when a tag fires. For most B2B companies, that tag fires on a form submission — a lead, not a sale. Google doesn't know whether that lead became a $5K deal or a $500K deal. It doesn't know whether the lead closed in two weeks or six months. It doesn't know whether the lead was already in your pipeline from a different channel.

Your ROAS in the Google Ads UI is a guess based on incomplete data.

What changes when the data leaves the silo

The fix: export Google Ads data to your warehouse and join it to your CRM.

Google Ads ──→ BigQuery (via Fivetran / BigQuery Data Transfer)
GA4 ──→ BigQuery (native export)
CRM (HubSpot / Salesforce) ──→ BigQuery (via Fivetran)
         │
         ▼
      dbt models
         │
         ▼
   Sigma / Looker

Staging: Google Ads performance

-- models/staging/stg_google_ads__campaign_performance.sql
SELECT
    segments_date                   AS report_date,
    campaign_name,
    campaign_id,
    ad_group_name,
    metrics_impressions             AS impressions,
    metrics_clicks                  AS clicks,
    metrics_cost_micros / 1e6       AS spend,
    metrics_conversions             AS platform_conversions,
    metrics_conversions_value       AS platform_conversion_value,
    SAFE_DIVIDE(metrics_clicks, NULLIF(metrics_impressions, 0)) AS ctr,
    SAFE_DIVIDE(metrics_cost_micros / 1e6, NULLIF(metrics_clicks, 0)) AS cpc
FROM {{ source('google_ads', 'campaign_stats') }}

The join: ads → sessions → CRM deals

-- models/marts/mart_ads_to_revenue.sql
SELECT
    ga.campaign_name,
    ads.spend,
    ads.clicks,
    COUNT(DISTINCT c.contact_id)       AS leads,
    COUNT(DISTINCT d.deal_id)          AS deals_created,
    COUNT(DISTINCT CASE WHEN d.stage = 'closed_won' THEN d.deal_id END) AS deals_won,
    SUM(CASE WHEN d.stage = 'closed_won' THEN d.amount END) AS actual_revenue,
    SAFE_DIVIDE(
        SUM(CASE WHEN d.stage = 'closed_won' THEN d.amount END),
        ads.spend
    ) AS true_roas
FROM {{ ref('stg_google_ads__campaign_performance') }} ads
LEFT JOIN {{ ref('int_ga4__sessions') }} ga
    ON ads.campaign_name = ga.utm_campaign
    AND ads.report_date = DATE(ga.session_start)
LEFT JOIN {{ ref('stg_hubspot__contacts') }} c
    ON ga.user_pseudo_id = c.ga_user_id
LEFT JOIN {{ ref('stg_hubspot__deals') }} d
    ON c.contact_id = d.contact_id
GROUP BY 1, 2, 3

That true_roas column is the number that matters. Not what Google thinks your conversion is worth — what your CRM says you actually closed.

Enhanced conversions: feeding truth back to Google

Once you have actual revenue data in your warehouse, the next step is sending it back to Google. Enhanced conversions let you upload hashed first-party data (email, phone) alongside conversion events, so Google's algorithm can optimize for actual business outcomes instead of form submissions.

The pattern:

Warehouse (closed deals) → Hightouch / Census → Google Ads Offline Conversions API
-- models/activation/google_ads_offline_conversions.sql
SELECT
    c.email                    AS hashed_email,  -- Hightouch hashes before upload
    d.closed_date              AS conversion_time,
    d.amount                   AS conversion_value,
    ga.gclid                   AS google_click_id,
    'CLOSED_WON'               AS conversion_action
FROM {{ ref('stg_hubspot__deals') }} d
JOIN {{ ref('stg_hubspot__contacts') }} c
    ON d.contact_id = c.contact_id
JOIN {{ ref('int_ga4__sessions') }} ga
    ON c.ga_user_id = ga.user_pseudo_id
WHERE d.stage = 'closed_won'
  AND ga.gclid IS NOT NULL
  AND d.closed_date >= CURRENT_DATE - 90  -- Google accepts 90-day lookback

Hightouch (or Census) picks up this model on a schedule and pushes conversions to the Google Ads API. Google's bidding algorithm starts learning from real revenue, not proxy events.

We solved exactly this for a client whose HubSpot → Google Ads conversion tracking was broken. The existing integration was double-counting conversions and sending wrong values. We cut HubSpot out of the conversion loop entirely, built the model in dbt, and pushed conversions directly via the API. Their Smart Bidding improved within two weeks because it was finally optimizing for real numbers.

The CPC trap

Here's a subtlety that most dashboards miss: you can't average CPCs across campaigns by taking a simple mean.

If Campaign A spent $500 on 100 clicks (CPC: $5.00) and Campaign B spent $2,000 on 200 clicks (CPC: $10.00), the blended CPC is not $7.50. It's:

-- Correct: weighted average
SELECT
    SUM(spend) / NULLIF(SUM(clicks), 0) AS blended_cpc
FROM {{ ref('stg_google_ads__campaign_performance') }}
 
-- $2,500 / 300 = $8.33

Simple average would give you $7.50. The weighted average is $8.33. That 11% difference compounds across every metric built on top of it — CAC, ROAS, budget allocation decisions.

We see this mistake in dashboards constantly. It's not a Google Ads problem — it's a BI tool problem. Most Looker Studio / Sheets dashboards default to AVG() instead of SUM(spend) / SUM(clicks).

What the warehouse enables

QuestionGoogle Ads UIGoogle Ads + warehouse
What's my CPC?YesYes (correctly weighted)
Which campaign gets the most clicks?YesYes
Which campaign generates the most revenue?No — conversion proxies onlyYes — joined to CRM
What's my true CAC by campaign?NoYes — spend / closed deals
Is Smart Bidding optimizing for the right signal?MaybeYes — feed it real conversions
Which keywords generate pipeline, not just leads?NoYes — full funnel attribution

The stack

LayerToolWhy
Ad data extractionFivetran (Google Ads connector)Handles API pagination, schema changes, rate limits
WarehouseBigQueryNative integration, free GA4 export
TransformationdbtCampaign taxonomy, CPC calculations, attribution models
Reverse ETLHightouchPushes offline conversions back to Google Ads
VisualizationSigma or LookerConnected to warehouse, reads from mart models

The uncomfortable math

If your true ROAS is higher than what Google shows, you've been underinvesting. If it's lower, you've been over-investing. Either way, you've been making budget decisions on wrong numbers.

The only way to know is to connect the dots — from click to lead to deal to revenue. Google Ads can't do that alone. Your warehouse can.


We build the pipeline that connects Google Ads to actual revenue — from BigQuery extraction to dbt models to reverse ETL with Hightouch. If your ROAS is a guess, book a discovery call and we'll show you the real number.

Got a similar problem?

30 minutes. We'll tell you honestlywhat's broken.