Home / FinOps

GCP Billing Export to BigQuery: Build a Cost Dashboard with Detailed Export & SQL | 2026

GCP Billing Export to BigQuery: Build a Cost Dashboard with Detailed Export & SQL | 2026

GCP Billing Export to BigQuery is the foundation for every cost visibility and FinOps workflow on Google Cloud. Enable the Detailed Usage Cost export (not Standard), enforce resource labels on all Terraform-managed resources, and use the BigQuery SQL queries in this guide to break down costs by team, service, SKU, and label — then connect Looker Studio for a live, shareable cost dashboard. This guide covers setup, Terraform automation, and 7 production-ready SQL queries.

By Mateusz Chmielewski · Aug 22, 2026 · 15 min read

What Is GCP Billing Export to BigQuery?

GCP Billing Export to BigQuery is a feature that continuously streams your Google Cloud billing data into a BigQuery dataset in near-real-time (typically 24-hour lag for standard export, or within hours for detailed export). Once enabled, every charge, credit, discount, adjustment, and usage measurement across all GCP services is written as rows into a partitioned BigQuery table that you own and can query with standard SQL. The export is free to enable; you pay only for BigQuery storage (~$0.02/GB/month) and queries ($5/TB scanned, or free with partitioned filters on small datasets).

Think of GCP Billing Export like receiving a digital bank statement that is automatically deposited into a spreadsheet (BigQuery) every day. The basic 'Standard' export is like a monthly summary — one row per service per day. The 'Detailed' export is like the full transaction ledger — every individual resource usage with labels, SKU codes, credits, and timestamps. You need the ledger to do per-team charge-back and anomaly detection.

ConceptExplanationWhen to use
Standard Usage Cost ExportDaily aggregate billing rows per service per project. Missing resource-level labels and sub-day granularity. Sufficient for top-level cost trends.Basic cost visibility dashboards where per-resource or per-team allocation is not required.
Detailed Usage Cost Export1-hour granularity with resource labels, system labels, and per-SKU breakdown. Required for per-team cost allocation. Table suffix: gcp_billing_export_resource_v1_*.Every production FinOps use case — enable this instead of Standard from day one.
Pricing ExportA separate export that writes the GCP public price list to BigQuery. Useful for modeling costs before creating resources.Pre-purchase cost modeling, CUD break-even analysis.
Resource LabelsKey-value tags (team=platform, env=prod) applied to GCP resources in Terraform via the labels block. Propagate into billing export as labels array rows within 24–48 hours.Mandatory on every Terraform resource for per-team and per-environment cost allocation.
_TABLE_SUFFIX partitioningBilling export tables are date-sharded (one table per day). Always filter with WHERE _TABLE_SUFFIX BETWEEN 'YYYYMMDD' AND 'YYYYMMDD' to avoid full-table scans and reduce query cost.Every billing SQL query — without this filter, a wildcard query scans all historical data and costs more.
Credits ArrayA REPEATED field in the billing export containing credits applied to each row: COMMITTED_USAGE_DISCOUNT, SUSTAINED_USE_DISCOUNT, PROMOTION, FREE_TIER. Must be UNNESTed to sum individually.CUD utilization queries, effective discount rate calculations, net cost after credits reports.

Why BigQuery Export Instead of the GCP Console Billing Reports?

The GCP Console billing dashboard shows top-level cost trends but cannot break costs down by team label, identify individual runaway resources, or detect cost anomalies across services. It provides no export, no SQL, no automation, and no custom alerting. Finance teams asking 'how much did the platform team spend on GKE last month?' or 'which Cloud Run revision caused the cost spike on August 15th?' cannot get answers from the console alone.

BigQuery billing export transforms GCP cost data into a queryable, shareable ledger. This guide pairs with [GCP Committed Use Discounts](/tutorial/gcp-committed-use-discounts-resource-spend-flex-cuds-billing-sql) for the CUD utilization queries, and complements [GKE Autopilot Production Checklist](/tutorial/gke-autopilot-production-checklist-pdb-hpa-vpa-spot-cost) and [Cloud SQL PostgreSQL in Production](/tutorial/cloud-sql-postgres-production-private-ip-ha-backups-terraform) for per-service cost tracking. For budget enforcement at the infrastructure level, see also [Terraform Module Structure & Versioning with GitHub Actions](/tutorial/terraform-module-structure-versioning-github-actions).

FeaturethisServicealtAaltB
Per-team cost allocationYes — query by resource label (team=platform)GCP Console billing: No — project-level onlyLooker Studio without BQ export: No raw data access
Custom SQL queriesYes — full BigQuery SQL with JOINs, window functions, anomaly detectionGCP Console: No SQL — fixed charts onlyCost Management tools (e.g., Apptio): Yes, but adds SaaS cost
Historical data retentionUnlimited — stored in your BigQuery dataset under your controlGCP Console: 12 months rollingThird-party tools: depends on pricing tier
Anomaly detectionCustom SQL — week-over-week variance, rolling averagesGCP Console: manual inspection onlyCloud Monitoring budget alerts: threshold-based only
Cost to operate~$0.01/day BQ storage; near-zero query cost with partition filtersFree — included in GCPThird-party: $500–$5,000+/month depending on spend

Prerequisites

  • Billing Account Admin IAM role on the GCP billing account
  • BigQuery Admin role on the destination project (can be a dedicated billing project)
  • Terraform v1.6+ with google provider ~> 5.x for budget automation
  • gcloud CLI v480.0+ authenticated
  • Resource labels ('team', 'environment', 'service') applied to Terraform-managed GCP resources

Step-by-Step Guide

Step 1: Enable Detailed Usage Cost Export to BigQuery

Enable the Detailed Usage Cost export in GCP Console (Billing → Billing Export → BigQuery export). Choose a dedicated BigQuery dataset in a billing-analytics project. The Detailed export includes resource-level labels and 1-hour granularity — without it, per-team cost allocation is impossible. Standard export is insufficient for FinOps workflows.

# Step 1: Create a dedicated BigQuery dataset for billing export
bq mk \
  --dataset \
  --location=EU \
  --description="GCP Billing Export — Detailed Usage Cost" \
  billing-analytics-project:billing_export

# Step 2: Enable export in GCP Console:
# Billing → Billing Export → BigQuery export
# → Detailed usage cost
# → Project: billing-analytics-project
# → Dataset: billing_export
# → Save

# Step 3: Verify export table exists (allow 24–48h for first data)
bq ls --format=pretty billing-analytics-project:billing_export

# Step 4: Inspect schema
bq show \
  --schema \
  --format=prettyjson \
  billing-analytics-project:billing_export.gcp_billing_export_resource_v1_XXXXXXXX

Step 2: Enforce Resource Labels on All Terraform Resources

Add a labels block with 'team', 'environment', and 'service' tags to every Terraform resource. These propagate into the billing export for cost allocation. Without labels, billing rows are identified only by project and service — there is no way to attribute costs to a team or product. Labels are the only mechanism for chargeback in GCP.

# locals.tf — centralize label definitions
locals {
  common_labels = {
    team        = var.team        # e.g., "platform", "payments", "ml"
    environment = var.environment # e.g., "prod", "staging", "dev"
    service     = var.service     # e.g., "api", "worker", "pipeline"
    managed_by  = "terraform"
  }
}

# Apply to every resource — example: GKE node pool
resource "google_container_node_pool" "main" {
  name    = "main-pool"
  cluster = google_container_cluster.primary.name

  node_config {
    labels = local.common_labels   # propagate into billing export
    # ...
  }
}

# Cloud SQL instance
resource "google_sql_database_instance" "postgres" {
  # ...
  settings {
    user_labels = local.common_labels
  }
}

# Cloud Run service
resource "google_cloud_run_v2_service" "app" {
  # ...
  labels = local.common_labels
}

Step 3: Query Monthly Cost Breakdown by Service

Run the foundational billing SQL query to break down monthly GCP costs by service, with and without credits. The top-level service breakdown is the first filter in any cost investigation — it identifies which GCP service is driving the bill before drilling into SKU or resource-level detail.

-- Monthly cost by service (last 6 months)
-- Use _TABLE_SUFFIX to restrict partition scan and reduce query cost
SELECT
  DATE_TRUNC(DATE(usage_start_time), MONTH)   AS billing_month,
  service.description                          AS service,
  SUM(cost)                                    AS gross_cost,
  SUM(
    COALESCE((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0)
  )                                            AS total_credits,
  SUM(cost) + SUM(
    COALESCE((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0)
  )                                            AS net_cost
FROM
  `billing-analytics-project.billing_export.gcp_billing_export_resource_v1_*`
WHERE
  _TABLE_SUFFIX BETWEEN
    FORMAT_DATE('%Y%m%d', DATE_TRUNC(DATE_SUB(CURRENT_DATE(), INTERVAL 6 MONTH), MONTH))
    AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
GROUP BY 1, 2
HAVING SUM(cost) > 1
ORDER BY billing_month DESC, net_cost DESC

Step 4: Per-Team Cost Allocation via Resource Labels

Query billing data using resource labels to break costs down by team. This is the primary chargeback/showback mechanism in GCP. Per-team cost allocation enables engineering leaders to see exactly what their team is spending and identify waste. It also enables accurate budget-to-actuals comparison per team.

-- Per-team cost breakdown (last 3 months)
SELECT
  DATE_TRUNC(DATE(usage_start_time), MONTH)                     AS billing_month,
  (SELECT value FROM UNNEST(labels)
   WHERE key = 'team' LIMIT 1)                                  AS team,
  (SELECT value FROM UNNEST(labels)
   WHERE key = 'environment' LIMIT 1)                           AS environment,
  service.description                                            AS service,
  SUM(cost) + SUM(
    COALESCE((SELECT SUM(c.amount) FROM UNNEST(credits) c), 0)
  )                                                              AS net_cost
FROM
  `billing-analytics-project.billing_export.gcp_billing_export_resource_v1_*`
WHERE
  _TABLE_SUFFIX BETWEEN
    FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY))
    AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
GROUP BY 1, 2, 3, 4
HAVING
  team IS NOT NULL
  AND SUM(cost) > 1
ORDER BY billing_month DESC, net_cost DESC

Step 5: Top-SKU Cost Drill-Down and Anomaly Detection

Query the top spending SKUs and build a week-over-week variance query to detect cost anomalies before they appear on the monthly bill. A misrouted Cloud Run job that ran for 6 hours at max-instances, or a BigQuery query that scanned a full 10TB table, shows up as an anomalous SKU spike days before the monthly bill. SQL-based anomaly detection catches it in near-real-time.

-- Week-over-week cost anomaly detection (services with >50% WoW increase)
WITH weekly_costs AS (
  SELECT
    DATE_TRUNC(DATE(usage_start_time), WEEK) AS billing_week,
    service.description                       AS service,
    sku.description                           AS sku,
    SUM(cost)                                 AS weekly_cost
  FROM
    `billing-analytics-project.billing_export.gcp_billing_export_resource_v1_*`
  WHERE
    _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
  GROUP BY 1, 2, 3
),
with_prev AS (
  SELECT
    billing_week,
    service,
    sku,
    weekly_cost,
    LAG(weekly_cost) OVER (
      PARTITION BY service, sku ORDER BY billing_week
    ) AS prev_week_cost
  FROM weekly_costs
)
SELECT
  billing_week,
  service,
  sku,
  ROUND(weekly_cost, 2)                                   AS this_week,
  ROUND(prev_week_cost, 2)                                AS last_week,
  ROUND((weekly_cost - prev_week_cost) / prev_week_cost * 100, 1) AS wow_change_pct
FROM with_prev
WHERE
  prev_week_cost > 10                              -- filter noise
  AND weekly_cost > prev_week_cost * 1.5          -- >50% increase
ORDER BY wow_change_pct DESC
LIMIT 20

Step 6: Build a Looker Studio Cost Dashboard

Connect Looker Studio to your BigQuery billing summary tables to create a shareable, self-updating cost dashboard for engineering leads and finance. SQL queries are powerful but require technical access. Looker Studio turns the same data into a point-and-click dashboard that finance, product, and leadership can consume without touching BigQuery.

# Step 1: Create a BigQuery view for the dashboard (reduce query cost)
bq mk \
  --use_legacy_sql=false \
  --view='
    SELECT
      DATE_TRUNC(DATE(usage_start_time), MONTH) AS billing_month,
      project.id AS project_id,
      (SELECT value FROM UNNEST(labels) WHERE key = "team" LIMIT 1) AS team,
      service.description AS service,
      SUM(cost) + SUM(COALESCE(
        (SELECT SUM(c.amount) FROM UNNEST(credits) c), 0
      )) AS net_cost
    FROM `billing-analytics-project.billing_export.gcp_billing_export_resource_v1_*`
    WHERE _TABLE_SUFFIX >= FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE(), INTERVAL 365 DAY))
    GROUP BY 1, 2, 3, 4
  ' \
  billing-analytics-project:billing_export.cost_dashboard_view

# Step 2: In Looker Studio (https://lookerstudio.google.com/):
# → Create → Data Source → BigQuery → billing-analytics-project → billing_export → cost_dashboard_view
# → Create Report
# Recommended charts:
# 1. Scorecard: Total net cost (current month)
# 2. Time series: Net cost by month (last 12 months)
# 3. Bar chart: Net cost by team (current month)
# 4. Table: Top 10 SKUs by cost (current month, sorted)
# 5. Pie chart: Cost by service (current month)
# 6. Bar chart: Week-over-week cost trend by service

Step 7: Automate Budget Alerts with Terraform

Create google_billing_budget resources in Terraform with per-project and per-team budget thresholds that trigger Pub/Sub notifications for Slack/email alerting. Budget alerts are the safety net that catches runaway costs before they compound into a surprise monthly bill. Terraform-managed budgets are version-controlled and survive project recreation — unlike console-created budgets.

resource "google_billing_budget" "prod_project" {
  billing_account = var.billing_account_id
  display_name    = "Prod Project Monthly Budget"

  budget_filter {
    projects               = ["projects/${google_project.prod.number}"]
    credit_types_treatment = "INCLUDE_ALL_CREDITS"
  }

  amount {
    specified_amount {
      currency_code = "USD"
      units         = "5000"  # $5,000/month budget
    }
  }

  # Alert at 50%, 90%, 100%, and 110% of budget
  threshold_rules {
    threshold_percent = 0.5
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 0.9
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 1.0
    spend_basis       = "CURRENT_SPEND"
  }
  threshold_rules {
    threshold_percent = 1.1
    spend_basis       = "FORECASTED_SPEND"
  }

  # Publish to Pub/Sub → Cloud Function → Slack webhook
  all_updates_rule {
    pubsub_topic                     = google_pubsub_topic.billing_alerts.id
    schema_version                   = "1.0"
    monitoring_notification_channels = [var.billing_notification_channel]
    disable_default_iam_recipients   = false
  }
}

resource "google_pubsub_topic" "billing_alerts" {
  name = "billing-budget-alerts"
}

Verification & Health Check

Best Practices

  • Always Use Detailed Export, Never Standard
  • Always Filter by _TABLE_SUFFIX in Every Query
  • Create Summary Views and Materialized Views for Dashboards

Common Mistakes

  • {"errorCode":"BILLING_EXPORT_NOT_FLOWING — no rows in last 48h","symptoms":"BigQuery billing export table exists but has no rows for the last 2 days. SELECT COUNT(*) FROM billing_export_table WHERE usage_start_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR) returns 0.","rootCause":"The billing export service account ([email protected]) lost BigQuery Data Editor permission on the export dataset — typically caused by a dataset IAM policy reset during a terraform apply that uses resource-level IAM.","fixCommand":"bq add-iam-policy-binding --member='serviceAccount:[email protected]' --role='roles/bigquery.dataEditor' billing-analytics-project:billing_export","code":"# Terraform: preserve billing export IAM explicitly\nresource \"google_bigquery_dataset_iam_member\" \"billing_export_writer\" {\n dataset_id = google_bigquery_dataset.billing_export.dataset_id\n role = \"roles/bigquery.dataEditor\"\n member = \"serviceAccount:[email protected]\"\n}\n","language":"hcl","filename":"billing-export-iam.tf","prevention":"Declare the billing export service account IAM binding as a Terraform resource. This prevents it from being removed during terraform apply."}
  • {"errorCode":"HIGH_QUERY_COST — full table scan on billing export","symptoms":"BigQuery billing shows unexpected $10–50 charges for billing analysis queries. bq show --format=prettyjson shows bytes_processed in the terabyte range.","rootCause":"Query is missing the _TABLE_SUFFIX date filter, or uses a TIMESTAMP-based WHERE clause on usage_start_time which does not prune partitions on date-sharded tables.","fixCommand":"bq query --dry_run 'SELECT ... WHERE _TABLE_SUFFIX >= \"20260801\"' — check bytes_processed before running","code":"-- WRONG: TIMESTAMP filter does NOT prune date-sharded tables\nWHERE usage_start_time >= TIMESTAMP('2026-08-01') -- full scan!\n\n-- RIGHT: _TABLE_SUFFIX prunes the date-sharded tables\nWHERE _TABLE_SUFFIX >= FORMAT_DATE('%Y%m%d', DATE('2026-08-01'))\n","language":"sql","filename":"fix-partition-filter.sql","prevention":"Add a BigQuery IAM Condition that limits max bytes billed per query to 10 GB (--maximum_bytes_billed=10737418240). Queries that exceed this cap fail with an error rather than running a runaway full scan."}

Cost Analysis

Featuremetriccost1kcost10kcost100kcost1m
BigQuery storage for billing export dataset (5 GB × $0.02/GB/month)$0.10/mo$0.10/mo$0.10/mo$0.10/mo
Ad-hoc SQL queries with _TABLE_SUFFIX filter (~1 GB scanned/query × $5/TB)$0.005$0.005$0.005$0.005
Scheduled monthly summary query (10 GB scan × $5/TB)$0.05/mo$0.05/mo$0.05/mo$0.05/mo
Looker Studio refreshes via materialized view (effectively free)$0.00$0.00$0.00$0.00
Total monthly operating cost~$0.20~$0.20~$0.20~$0.20

References

Browse all tutorials