Overview and What You Will Learn
In this lab, you will load a dataset into BigQuery, run a SQL query against it, and check exactly how much data that query scanned - directly connecting BigQuery's per-query pricing model to a concrete, observable number rather than an abstract concept.
Why This Matters in Production
A team runs a SELECT * query against a multi-terabyte BigQuery table just to preview a handful of rows, and the query scans the entire table's worth of data to do it - a habit that quietly becomes one of the largest line items on the account once dozens of engineers run similarly unscoped queries daily. Understanding that BigQuery charges by data scanned, not by rows returned, changes how a team writes queries from the very first day.
Core Principles
BigQuery is GCP's serverless data warehouse - there is no cluster to provision or manage, and its cost model is based specifically on the volume of data a query actually scans, not on any pre-provisioned compute capacity sitting idle between queries.
+------------------------------------------+| Data loaded into BigQuery tables || (from Cloud Storage, streaming inserts, || or other sources) |+------------------------------------------+ | v+------------------------------------------+| SQL query submitted |+------------------------------------------+ | v+------------------------------------------+| BigQuery scans only the specific columns || referenced, across the relevant partitions || (if the table is partitioned) |+------------------------------------------+ | v+------------------------------------------+| Billed based on data scanned, not rows || returned or query complexity |+------------------------------------------+Selecting fewer columns and querying a smaller time range (on a properly partitioned table) both directly reduce the data scanned, and therefore the cost - this is the single most important cost lever in BigQuery, more impactful than nearly any other optimization.
Detailed Step-by-Step Practical Lab
- Create a project and enable the BigQuery API:
gcloud projects create gcp-bq-lab-2026 --name="BigQuery Lab"gcloud config set project gcp-bq-lab-2026gcloud services enable bigquery.googleapis.com- Create a dataset to hold your tables:
bq mk --dataset --location=asia-south1 gcp-bq-lab-2026:sales_data- Create a simple table and load it with a small amount of sample data:
bq mk --table gcp-bq-lab-2026:sales_data.orders \ region:STRING,revenue:FLOAT,order_date:DATE echo "region,revenue,order_dateMumbai,4599.00,2026-01-15Delhi,2300.50,2026-01-16Mumbai,1899.00,2026-01-17" > orders.csv bq load --source_format=CSV --skip_leading_rows=1 \ gcp-bq-lab-2026:sales_data.orders orders.csv \ region:STRING,revenue:FLOAT,order_date:DATE- Run a query selecting only the specific columns needed, rather than every column:
bq query --use_legacy_sql=false \ 'SELECT region, SUM(revenue) as total_revenue FROM `gcp-bq-lab-2026.sales_data.orders` GROUP BY region'- Check exactly how much data that query scanned, using the
--dry_runflag before actually running it:
bq query --use_legacy_sql=false --dry_run \ 'SELECT region, SUM(revenue) as total_revenue FROM `gcp-bq-lab-2026.sales_data.orders` GROUP BY region'Note
--dry_runestimates the bytes a query would process without actually running it or incurring cost - this is the practical habit worth building for any query against a table of meaningful size, checking the estimated scan volume before committing to running it.
- Compare that against a wasteful
SELECT *query scanning every column, even though only two are actually needed:
bq query --use_legacy_sql=false --dry_run \ 'SELECT * FROM `gcp-bq-lab-2026.sales_data.orders`'- Clean up:
bq rm -f -t gcp-bq-lab-2026:sales_data.ordersbq rm -f -d gcp-bq-lab-2026:sales_datagcloud projects delete gcp-bq-lab-2026 --quietProduction Best Practices & Common Pitfalls
Common MistakeRunning
SELECT *out of habit against a large BigQuery table, scanning every column even when a query only needs two or three of them. Since BigQuery's cost is based on data scanned, selecting only the specific columns actually needed can dramatically reduce cost for the exact same logical query.
TipPartition large tables by a commonly-filtered column, typically a date, so queries filtered to a specific date range only scan the relevant partitions rather than the entire table's history. This is frequently the single highest-impact optimization for both cost and query speed on a growing table.
--dry_runcosts nothing and takes no time to run, making it a genuinely free habit to check before running any query against an unfamiliar or large table for the first time.- BigQuery is not the right tool for a live application's frequent, small transactional reads and writes. Its pricing and performance model is built for analytical queries scanning meaningful volumes of data, not the pattern of a typical application backend hitting a database on every user request.
Quick Reference & Troubleshooting Commands
| Command | Description |
|---|---|
bq mk --dataset |
Create a new BigQuery dataset |
bq load |
Load data from a file into a BigQuery table |
bq query --dry_run |
Estimate a query's data scan without running it |
bq query --use_legacy_sql=false |
Run a standard SQL query against BigQuery |