Prepare and test your skills
Prepare and test your skills
Optimizing data structures for performance and cost means shaping files, tables, and storage choices at write time so downstream queries scan less data. BigQuery (opens in a new tab) charges for bytes scanned and uses columnar storage with persisted statistics to plan efficient queries. Well-structured writes therefore lower cost and speed up analytics and machine learning training.
Source file format and layout control how fast Cloud Storage (opens in a new tab) data can be loaded and queried. Columnar formats such as Apache Parquet and Apache ORC generally perform much better for analytic queries than row-based formats such as JSON or CSV. Formats that support parallel reads such as Avro are usually faster than CSV files with embedded newlines or compressed files that cannot be split. A Hive partitioned layout organizes files in Cloud Storage by partitioning column values, which lets BigQuery prune partitions and speed up queries that filter on those columns. File count and size also matter, because too many tiny files or very large files can hurt query performance.
Partitioning in BigQuery divides a table into segments by date, ingestion time, or a time-unit column so queries that filter on the partition key read only relevant segments. For partitioned tables, a separate limit for partitioned table modifications replaces the standard per-table daily limit for load, copy, and query jobs that append or overwrite. When data is streamed with the BigQuery Storage Write API (gRPC), rows first land in the __UNPARTITIONED__ partition, and BigQuery later repartitions the data into the correct partition after enough unpartitioned data collects, with no service level agreement (SLA) on timing.
For ingestion-time partitioned tables, the Storage Write API infers the destination partition from the current system UTC time. For a daily partitioned table, that inference can be overridden with a partition decorator such as table1$20250601 to stream to the June 1, 2025 partition of table1. For ingestion-time and time-unit column partitioned tables, in-flight rows in __UNPARTITIONED__ can be excluded by filtering out NULL values with the pseudocolumns _PARTITIONTIME or _PARTITIONDATE.
Exam tip: Streamed rows sit in __UNPARTITIONED__ with no SLA for repartitioning, so exclude them with a _PARTITIONTIME IS NOT NULL or _PARTITIONDATE IS NOT NULL filter when a query must read only fully partitioned data.
BigQuery-native storage enables performance optimizations that are not possible with BigLake tables that reference external files. If BigLake queries cannot meet SLA requirements and cannot be tuned, converting the data to BigQuery storage and repointing authorized views to the new tables generally causes no disruption to consumers or changes to their queries. Once data is in BigQuery storage, consumers can use materialized views that have different partitioning and clustering than the base tables and can use BigQuery BI Engine to accelerate dashboards.
Data freshness can be tuned to reduce latency and cost for downstream analytics. By default, a query against a table with streamed data includes rows streamed up to query execution time for the freshest results. When real-time freshness is not required, set the max_staleness option on the destination table or set the @@max_staleness_override system variable for a query or session, such as INTERVAL 10 MINUTE, to allow BigQuery to serve recent snapshots for dashboards and reporting.
Efficient read paths for analytics and machine learning balance filtering, governance, and throughput when consumers read BigQuery or Cloud Storage data directly. Direct access to base tables is generally not recommended because schema changes require versioning and maintenance, but it can be practical for stable schemas where performance or cost matters. When new columns are added and backfilled for all rows, migration of the base table can be avoided because the change is backward compatible. Direct storage access is most viable for stable, small-sized datasets measured in gigabytes, and copying data between projects through Pub/Sub notifications is generally discouraged because multiple copies increase storage cost and lineage-tracking overhead.
| API | Filtering and access control | Choose when |
|---|---|---|
| BigQuery Storage Read API | Supports filtering and fine-grained access control, and can read BigQuery and BigLake tables | The consumer is a stable analytics or machine learning workload that benefits from column pruning and row filtering |
| Cloud Storage JSON or XML APIs | Shares a bucket directly with no BigQuery filtering or fine-grained control | The consumer cannot use the SQL interface or needs a data format not supported by the Storage Read API |
Managing schema design and evolution in BigQuery (opens in a new tab) means shaping tables for fast analytics and machine learning while keeping them compatible as sources change. Simply migrating tables as-is works for early iterations and avoids rebuilding indexes, vacuuming, or downtime from version upgrades. Keeping the old warehouse model long-term leaves technical debt, limits query optimization, and misses native features such as nested and repeated fields, partitioning, and clustering.
Partitioning in BigQuery divides table data into segments called partitions that make data easier to manage and query. Tables can be partitioned on a TIMESTAMP or DATE column, or BigQuery can add pseudocolumns to automatically partition data during ingestion. Queries that touch smaller partitions scan only a subset of data, which improves performance and reduces cost by minimizing bytes read. Partitioning does not change the existing structure of tables, so partitioned tables should be considered even when the schema is not otherwise modified.
Clustering in BigQuery automatically sorts data by the values of a few columns you specify and colocates related rows in optimally sized blocks. BigQuery uses no indexes and instead relies on massively parallel architecture, where more machines are added to scan data and aggregate results concurrently as more data is processed. With clustered columns, BigQuery can better estimate query cost, skip scans of unnecessary blocks, and calculate aggregates faster because blocks colocate records with similar values. Examine queries for columns frequently used for filtering and create tables with clustering on those columns.
Denormalization with nested and repeated fields is BigQuery's native way to represent related data inside a single table instead of spreading it across many joined tables. Internally, BigQuery organizes data with the Dremel model and stores it in a columnar storage format called Capacitor, which aligns well with nested structures. Changing a migrated schema to use nested and repeated fields is an evolutionary choice that reduces the number of joins and matches BigQuery's internal representation.
| Schema model | Structure | Relationship to BigQuery |
|---|---|---|
| Star schema | Denormalized fact table with metrics such as order amount, discount, and quantity plus keys to surrounding dimension tables such as customer, supplier, and region | Supported by BigQuery, but not native |
| Snowflake schema | Similar to star schema, but with normalized dimension tables | Supported by BigQuery, but not native |
| Nested and repeated fields | Related records stored together inside parent rows for a more natural representation | Native BigQuery representation, fewer joins required |
Schema evolution in BigQuery can be managed by exposing data products through authorized views, authorized functions, and authorized datasets instead of giving direct access to base tables. This boundary lets producers optimize underlying tables and queries without breaking consumer queries, because consumers query the view using SQL. When schemas must change, producers can use dataset versioning, view versioning, or a hybrid of the two:
catalog and orders unchanged but versions the dataset name, for example sales_v1 to sales_v2.sales unchanged but versions view names, for example catalog_v1 to catalog_v2.For machine learning workloads, reusable features reduce redundant engineering across projects. For tabular and structured datasets, Agent Platform Feature Store (opens in a new tab) promotes reusability and streamlines feature management through BigQuery. Unstructured data can stay in Cloud Storage and be governed through BigQuery object tables, while curated datasets can be shared securely across teams with BigQuery sharing.
Exam tip: Partitioning divides data by time or ingestion pseudocolumns without changing table structure, while clustering sorts data by filter columns to skip blocks — use partitioning to prune bytes scanned and clustering to speed filtered queries.
Selecting a data ingestion method for BigQuery (opens in a new tab) means matching how data arrives to how quickly it must be queryable. Batch loading collects large volumes of data and loads it in fewer, larger jobs for efficiency, while streaming ingestion writes continuously arriving records so they are available for real-time analytics and AI/ML pipelines. The BigQuery Storage Write API (gRPC) unifies both patterns in a single high-performance API that supports high throughput and exactly-once delivery. In Cloud Dataflow (opens in a new tab), Managed I/O automatically chooses the best write method for batch or streaming jobs, while the BigQueryIO connector allows advanced performance tuning.
Batch loading into BigQuery is the practice of accumulating data and loading it with fewer, larger jobs to stay within table-operation limits. A common pattern writes input data to staging files in Cloud Storage and then runs a BigQuery load job to load the data into a BigQuery table. Instead of running a separate load job for every few rows, pipelines wait until thousands of rows accumulate and can load many files in one job by using wildcards. This approach counts as one table operation even though the job contains much more data, which helps avoid quotaExceeded errors from the fixed daily limit and refill rate.
Streaming ingestion into BigQuery writes records directly to BigQuery storage as events arrive, without using intermediate staging files. A recommended architecture for endpoint logs and event data sends events to Pub/Sub, where a streaming Dataflow pipeline consumes them and streams them directly to BigQuery for ad hoc queries and near-real-time dashboards. Because end-to-end freshness is critical in this design, ingestion latency matters more than packing data into large files. Pub/Sub also acts as a durable buffer with message retention, so backed-up events can be processed after an outage with a low recovery point objective.
The BigQueryIO connector in Dataflow provides four write methods that cover batch loading, streaming, and legacy behavior. FILE_LOADS stages data in Cloud Storage and then runs a load job, and it is the default for bounded PCollection objects most common in batch pipelines. STREAMING_INSERTS uses the legacy streaming API and is the default for unbounded PCollection objects, but it is not recommended for new projects. STORAGE_WRITE_API and STORAGE_API_AT_LEAST_ONCE both write directly from Dataflow workers to BigQuery storage over gRPC and are preferred for streaming jobs.
| Method | How data flows to BigQuery | Delivery and use |
|---|---|---|
| STORAGE_WRITE_API | Data flows directly from Dataflow workers to BigQuery storage over gRPC | Exactly-once semantics; choose when duplicates are not acceptable |
| STORAGE_API_AT_LEAST_ONCE | Data flows directly from Dataflow workers to BigQuery storage over gRPC | At-least-once semantics with lower latency; duplicates are possible |
| FILE_LOADS | Data flows from Dataflow workers to Cloud Storage staging files, then a load job moves the files to a BigQuery table | Batch default; choose for large bounded loads |
| STREAMING_INSERTS | Data flows from Dataflow workers through the legacy streaming API to a BigQuery table | Legacy default for unbounded data; avoid for new projects |
Choose STORAGE_WRITE_API when integrity matters more than minimal latency, and choose STORAGE_API_AT_LEAST_ONCE when the pipeline can tolerate duplicates or can remove them later for lower latency and cost. Small to medium pipelines under about 1,000 vCPUs may benefit from STORAGE_WRITE_API, especially when a dead-letter queue is needed or the shared slot pool for FILE_LOADS is exhausted.
The BigQuery Storage Write API organizes writes around a stream, where a stream writes data to a BigQuery table and multiple streams can write concurrently to the same table. The default stream is designed for continuously arriving data, requires no explicit creation, makes data available immediately for query, and supports at-least-once semantics. Application-created streams provide more control at the cost of complexity, including exactly-once semantics through stream offsets and additional ACID properties. Using the API requires the bigquery.tables.updateData permission, which is included in roles such as bigquery.dataEditor, bigquery.dataOwner, and bigquery.admin.
Application-created streams use two visibility types that control when written data can be read. Pending type buffers records in a pending state until the stream is committed, and the commit makes all pending data available atomically, which makes it useful for batch workloads as an alternative to load jobs. The pending flow follows an ordered sequence:
Committed type makes records available for reading immediately as they are written, which supports streaming workloads that need minimal read latency. To obtain exactly-once delivery, the client creates a committed-type stream and supplies a record offset with each append, and BigQuery performs the write only if the offset matches the next append offset. The gRPC form is recommended over REST for new workloads because it is more efficient and less expensive, with up to 2 TiB per month free for ingestion.
The gRPC Storage Write API provides several advantages for continuous, high-throughput pipelines:
Exam tip: Exactly-once delivery requires a committed-type stream with client-supplied offsets; omitting the offset appends to the current end of the stream, so retrying a failed append can create a duplicate.
A reliable real-time pipeline from Pub/Sub through Dataflow to BigQuery handles failures, bad records, and throughput spikes without losing important data. The Dataflow pipeline should retry BigQuery inserts with truncated exponential backoff, while Pub/Sub retention buffers records during brief BigQuery or network outages. If the outage outlasts the buffer, the client must persist data locally and resume inserts after availability returns. Poison records that fail with a non-retryable error or exceed the maximum retries should be routed to a dead-letter queue or table for investigation, and Storage Write API failures can be collected with getFailedStorageApiInserts for that purpose. For FILE_LOADS, a load failure fails the whole job and raises a runtime exception without per-row error details, so row-level isolation is a reason to prefer the Storage Write API.
Throughput planning for the Storage Write API starts with estimating bytes per second and confirming quota in advance, since quota governs concurrent connections and throughput. For example, 100 million endpoints producing 1,500 records per minute can require about 2.5 GB per second, which must fit within provisioned quota. Smoothing uneven workloads into steadier throughput simplifies capacity planning, and pipelines should be prepared to handle 429 resource-exhausted errors during short spikes. Writing to the default stream can provide higher throughput and does not count against the quota limit for creating write streams when exactly-once guarantees are not required.
__UNPARTITIONED__ partition with no SLA for repartitioning.STORAGE_WRITE_API provides exactly-once delivery and STORAGE_API_AT_LEAST_ONCE provides lower-latency at-least-once delivery, while FILE_LOADS is the batch default and STREAMING_INSERTS is legacy.Use partitioning to divide table data into segments by date, ingestion time, or a time-unit column so queries that filter on the partition key read only relevant segments and scan fewer bytes. Use clustering to automatically sort data by frequently filtered columns and colocate related rows in optimally sized blocks so BigQuery can skip unnecessary blocks and calculate aggregates faster.
Rows first land in the __UNPARTITIONED__ partition, and BigQuery later repartitions the data into the correct partition after enough unpartitioned data collects, with no service level agreement on timing. For ingestion-time partitioned tables the destination partition is inferred from the current system UTC time unless overridden with a partition decorator. In-flight rows in __UNPARTITIONED__ can be excluded by filtering out NULL values with the pseudocolumns _PARTITIONTIME or _PARTITIONDATE.
Both methods write directly from Dataflow workers to BigQuery storage over gRPC, but STORAGE_WRITE_API provides exactly-once semantics while STORAGE_API_AT_LEAST_ONCE provides at-least-once semantics with lower latency where duplicates are possible. Choose STORAGE_WRITE_API when integrity matters more than minimal latency, and choose STORAGE_API_AT_LEAST_ONCE when the pipeline can tolerate duplicates or can remove them later.
Last updated