← Back to Google Cloud track
Reference

Google Cloud Cheat Sheet

Projects and IAM, BigQuery, Dataflow, Composer and Pub/Sub — the Google Cloud data track on one page. Bookmark it.

Projects & IAM
Pick the projectone project per environment
gcloud projects list
gcloud config set project lake-dev
gcloud config set compute/region us-central1
gcloud billing projects describe lake-dev
Service account for jobsnot your user key
gcloud iam service-accounts create etl --project=lake-dev --display-name="Lake ETL"
# email: etl@lake-dev.iam.gserviceaccount.com
# Dataflow, Composer, and Pub/Sub run as this account
Two roles, not Owner
gcloud projects add-iam-policy-binding lake-dev --member="serviceAccount:etl@lake-dev.iam.gserviceaccount.com" --role="roles/bigquery.jobUser"
gcloud projects add-iam-policy-binding lake-dev --member="serviceAccount:etl@lake-dev.iam.gserviceaccount.com" --role="roles/bigquery.dataEditor"
See the binding
gcloud projects get-iam-policy lake-dev --flatten="bindings[].members" --filter="bindings.members:etl@lake-dev.iam.gserviceaccount.com" --format="value(bindings.role)"
BigQuery tables
Dataset
bq mk --dataset --location=US lake-dev:lake
bq ls lake-dev:
Partition and cluster
bq mk --table --time_partitioning_field=dt --clustering_fields=order_id lake-dev:lake.orders order_id:STRING,amount:FLOAT64,dt:DATE
Same table in SQL
CREATE TABLE lake.orders (
  order_id STRING,
  amount FLOAT64,
  dt DATE
)
PARTITION BY dt
CLUSTER BY order_id;
Require a partition filter
ALTER TABLE lake.orders
SET OPTIONS (require_partition_filter = true);
BigQuery SQL
Aggregate one day
SELECT dt, SUM(amount) AS revenue
FROM lake.orders
WHERE dt = DATE '2026-03-15'
GROUP BY dt;
Keep the latest row
SELECT * EXCEPT (rn) FROM (
  SELECT *, ROW_NUMBER() OVER (
    PARTITION BY order_id ORDER BY updated_at DESC
  ) AS rn
  FROM lake.orders_raw
)
WHERE rn = 1;
MERGE
MERGE lake.orders t
USING lake.orders_stage s
ON t.order_id = s.order_id
WHEN MATCHED THEN
  UPDATE SET amount = s.amount, dt = s.dt
WHEN NOT MATCHED THEN
  INSERT (order_id, amount, dt)
  VALUES (s.order_id, s.amount, s.dt);
Load & external data
Load Parquetfile must contain dt
bq load --source_format=PARQUET lake-dev:lake.orders gs://lake-dev-raw/bronze/orders/dt=2026-03-15/*.parquet
LOAD DATA
LOAD DATA INTO lake.orders
FROM FILES (
  format = 'PARQUET',
  uris = ['gs://lake-dev-raw/bronze/orders/dt=2026-03-15/*.parquet']
);
External tableno copy into BigQuery storage
CREATE OR REPLACE EXTERNAL TABLE lake.orders_ext (
  order_id STRING,
  amount FLOAT64
)
WITH PARTITION COLUMNS (dt DATE)
OPTIONS (
  format = 'PARQUET',
  uris = ['gs://lake-dev-raw/silver/orders/*'],
  hive_partition_uri_prefix = 'gs://lake-dev-raw/silver/orders/'
);
Dry runbytes that would be billed
bq query --dry_run --use_legacy_sql=false 'SELECT SUM(amount) FROM lake.orders WHERE dt = "2026-03-15"'
Cloud Dataflow
Batch pipeline
import apache_beam as beam

with beam.Pipeline() as p:
    (p
     | beam.io.ReadFromParquet("gs://lake-dev-raw/bronze/orders/*.parquet")
     | beam.Filter(lambda r: r["amount"] is not None)
     | beam.io.WriteToParquet("gs://lake-dev-raw/silver/orders/out"))
Run on Dataflow
python orders_pipeline.py --runner DataflowRunner --project lake-dev --region us-central1 --temp_location gs://lake-dev-tmp/temp --service_account_email etl@lake-dev.iam.gserviceaccount.com
List and cancel
gcloud dataflow jobs list --region=us-central1 --status=active
gcloud dataflow jobs cancel JOB_ID --region=us-central1
Streaming windows
Read the subscription
import json
import apache_beam as beam
from apache_beam.transforms.window import FixedWindows

def parse(raw):
    return json.loads(raw.decode("utf-8"))

(p
 | beam.io.ReadFromPubSub(subscription="projects/lake-dev/subscriptions/orders-dataflow")
 | beam.Map(parse))
One-minute windows
| beam.WindowInto(FixedWindows(60))
Streaming flag
python orders_stream.py --runner DataflowRunner --project lake-dev --region us-central1 --temp_location gs://lake-dev-tmp/temp --streaming --service_account_email etl@lake-dev.iam.gserviceaccount.com
Cloud Composer
DAG
from datetime import datetime, timedelta
from airflow import DAG
from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator

with DAG(
    "nightly_orders",
    start_date=datetime(2026, 3, 1),
    schedule="@daily",
    catchup=False,
    default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
) as dag:
    load = BigQueryInsertJobOperator(
        task_id="load_orders",
        location="US",
        configuration={"query": {
            "query": "SELECT 1",
            "useLegacySql": False,
        }},
    )
Order the tasks
extract >> load
# load runs only after extract succeeds
Query uses the run date
"query": "SELECT * FROM lake.orders_ext WHERE dt = DATE '{{ ds }}'"
# {{ ds }} is the Airflow logical date, not a date baked into the file
Trigger from gcloud
gcloud composer environments run lake-dev --location us-central1 dags trigger -- nightly_orders
Cloud Pub/Sub
Topic and subscription
gcloud pubsub topics create orders
gcloud pubsub subscriptions create orders-pull --topic=orders --ack-deadline=60
Publish
gcloud pubsub topics publish orders --message='{"order_id":"1042","amount":19.5}'
Pull and acka second subscription still has the message
gcloud pubsub subscriptions pull orders-pull --auto-ack --limit=10
# ack only after the write succeeds; otherwise the message is redelivered
Dead letter
gcloud pubsub topics create orders-dead
gcloud pubsub subscriptions create orders-dataflow --topic=orders --ack-deadline=60 --dead-letter-topic=orders-dead --max-delivery-attempts=5
# the Pub/Sub service agent needs publisher on orders-dead
Pub/Sub to BigQuery
Landing tableschemaless payload
CREATE TABLE lake.orders_stream (
  data STRING,
  subscription_name STRING,
  message_id STRING,
  publish_time TIMESTAMP
);
BigQuery subscription
gcloud pubsub subscriptions create orders-bq --topic=orders --bigquery-table=lake-dev:lake.orders_stream --write-metadata --drop-unknown-fields
Read the payload
SELECT
  JSON_VALUE(data, '$.order_id') AS order_id,
  publish_time
FROM lake.orders_stream
WHERE publish_time >= TIMESTAMP('2026-03-15');

Projects to build

The fastest way to make this stick. Build one, then put it in your portfolio.

Beginner

Partitioned orders table

Load one day of Parquet from Cloud Storage into a BigQuery table partitioned by dt, turn on require_partition_filter, and dry-run a query so you can see the bytes it would scan.

BigQuery partitionsbq loaddry run
Beginner

Orders topic

Create a Pub/Sub topic and a pull subscription, publish a few order messages, and pull them with ack. A second subscription on the same topic still receives those messages.

Pub/Sub topicssubscriptionsack
Intermediate

Composer daily load

A Cloud Composer DAG loads the previous day of files into the partitioned table, then runs a dedupe query. Retries live on the DAG, and the load runs as the environment service account.

ComposerAirflow DAGsBigQueryIAM
Intermediate

Minute counts

A Dataflow streaming job reads the orders subscription, windows events into one-minute buckets, and writes the counts to BigQuery. Messages that fail five times land on a dead-letter topic.

DataflowBeam windowsPub/SubBigQuery
Advanced

Service account only

Composer starts a Dataflow batch job as a dedicated service account that can read only the raw bucket and write one dataset. A BigQuery subscription lands the raw stream in a separate table so you can replay it. Workers have no user keys.

IAMDataflowComposerPub/Sub BigQuery subscription