
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.
| Concept | Explanation | When to use |
|---|---|---|
| Standard Usage Cost Export | Daily 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 Export | 1-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 Export | A 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 Labels | Key-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 partitioning | Billing 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 Array | A 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).
| Feature | thisService | altA | altB |
|---|---|---|---|
| Per-team cost allocation | Yes — query by resource label (team=platform) | GCP Console billing: No — project-level only | Looker Studio without BQ export: No raw data access |
| Custom SQL queries | Yes — full BigQuery SQL with JOINs, window functions, anomaly detection | GCP Console: No SQL — fixed charts only | Cost Management tools (e.g., Apptio): Yes, but adds SaaS cost |
| Historical data retention | Unlimited — stored in your BigQuery dataset under your control | GCP Console: 12 months rolling | Third-party tools: depends on pricing tier |
| Anomaly detection | Custom SQL — week-over-week variance, rolling averages | GCP Console: manual inspection only | Cloud Monitoring budget alerts: threshold-based only |
| Cost to operate | ~$0.01/day BQ storage; near-zero query cost with partition filters | Free — included in GCP | Third-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
| Feature | metric | cost1k | cost10k | cost100k | cost1m |
|---|---|---|---|---|---|
| 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 |