# Labels, billing export and cost attribution in BigQuery > How to find out which team, pipeline or dashboard spends your BigQuery money. Job labels reach billing data, dataset labels reach only storage billing, table labels never do, and the billing export to BigQuery lets you query costs by label. Source: https://docs.querylab.io/bigquery/labels-and-billing-export/ Updated: 2026-10-14 A BigQuery invoice says how much the project spent on queries and storage, not who spent it. Two tools close that gap. **Labels** are key-value tags (`team:growth`, `pipeline:nightly-orders`) attached to jobs, datasets and reservations. **Billing export** writes your detailed Cloud Billing data into a BigQuery table, labels included, so you can query costs with SQL. Where a label ends up in the billing data depends on what it's attached to, and that is where cost attribution usually goes wrong. ## Which labels reach the bill | Label on | In `INFORMATION_SCHEMA.JOBS` | In billing data | | --------------------------- | ---------------------------- | ---------------------------------------- | | a job (query, load, export) | yes, `labels` column | yes, for that job's cost | | a dataset | n/a | storage cost only; not jobs that read it | | a table or view | no | **no** | | a reservation | n/a | yes, for the reservation's slot cost | So to attribute query cost, label the **jobs**. Labelling the dataset a team owns doesn't tell you who queried it, and table labels are only for organizing. ## Labelling jobs From `bq`: ```sh bq query --nouse_legacy_sql --label=team:growth --label=dashboard:weekly-kpis \ 'SELECT ...' ``` In the API, set `configuration.labels` on the job. Client libraries, dbt, Dataform, Airflow operators and most BI tools have a setting for job labels; use it so their queries are attributable. In a script or [session](https://docs.querylab.io/bigquery/sessions/), `SET @@query_label = 'team:growth,dashboard:weekly-kpis'` labels every query that follows. We ran a query with two labels; `INFORMATION_SCHEMA.JOBS_BY_USER` returned them as an array of key-value structs: ```sql SELECT job_id, (SELECT STRING_AGG(CONCAT(l.key, '=', l.value), ',' ORDER BY l.key) FROM UNNEST(labels) AS l) AS labels FROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_USER WHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR); -- label_26373 | dashboard=weekly-kpis,team=growth ``` Label rules: up to 64 labels per resource; keys up to 63 characters, starting with a lowercase letter; keys and values only lowercase letters, digits, underscores and dashes (international characters are allowed). A job's labels can't be changed once it is submitted. Querylab.io's [Query history](https://docs.querylab.io/writing-sql/query-history/) matches labels in its search and has a Labels quick filter. ## Cost by label from INFORMATION_SCHEMA For on-demand queries, `INFORMATION_SCHEMA.JOBS` already has everything needed, no billing export required: ```sql SELECT (SELECT value FROM UNNEST(labels) WHERE key = 'team') AS team, ROUND(SUM(total_bytes_billed) / POW(1024, 4) * 6.25, 2) AS approx_usd FROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT WHERE creation_time >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH) AND job_type = 'QUERY' AND statement_type != 'SCRIPT' GROUP BY team ORDER BY approx_usd DESC; ``` Jobs without the label show up as `NULL`, which is the first thing to fix. See [Monitoring jobs and costs](https://docs.querylab.io/bigquery/monitoring-jobs-and-costs/). ## Billing export to BigQuery `INFORMATION_SCHEMA` covers one project and one region and has no prices. The Cloud Billing export covers every project on the billing account, every service, with the actual cost after discounts. Turn it on in the Cloud Billing console (Billing export, BigQuery export); it writes to a table named `gcp_billing_export_v1_` in a dataset you choose, several times a day. It isn't retroactive beyond the start of the previous month: turn it on early, before you need the history. BigQuery cost per team and SKU this month, from the standard export: ```sql SELECT (SELECT value FROM UNNEST(labels) WHERE key = 'team') AS team, sku.description AS sku, ROUND(SUM(cost) + SUM(IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) AS c), 0)), 2) AS net_cost FROM mydataset.gcp_billing_export_v1_XXXXXX_XXXXXX_XXXXXX WHERE service.description = 'BigQuery' AND usage_start_time >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH) GROUP BY team, sku ORDER BY net_cost DESC; ``` Credits (the free tier, committed-use discounts, promotions) are negative amounts in `credits`, so add them to `cost` for the net figure. The SKU names separate on-demand analysis, storage (active and long-term, logical and physical), streaming and the editions' slot SKUs. The export table is itself billed like any table when you query it, so filter on `usage_start_time` and select only the columns you need. ## What can't be attributed this way - **Capacity pricing.** Slots are billed per reservation, not per job. To split a reservation's cost, use each job's `total_slot_ms` from `INFORMATION_SCHEMA.JOBS` as the share, or give teams separate reservations with labels. See [Reservations](https://docs.querylab.io/bigquery/reservations/). - **Storage by table.** Use `INFORMATION_SCHEMA.TABLE_STORAGE` for bytes per table, and multiply by the storage prices. See [Storage pricing](https://docs.querylab.io/bigquery/storage-pricing/). - **Cached results** cost nothing and don't appear in billing at all. Open the playground at https://app.querylab.io?playground=true&utm_source=docs&utm_medium=concepts to run it. It is the real app on public BigQuery datasets and needs no sign-in. ## Sources Based on the BigQuery documentation by Google, CC BY 4.0, rewritten with examples checked against BigQuery: - [Introduction to labels](https://cloud.google.com/bigquery/docs/labels-intro) - [Adding labels to resources](https://cloud.google.com/bigquery/docs/adding-labels) - [Export Cloud Billing data to BigQuery](https://cloud.google.com/billing/docs/how-to/export-data-bigquery)