GCP-220 · Lesson 2 of 12 · 20 min

Data formats, schemas and data types

▶ Video coming soon A video version of this lesson is on the way.

In the second session, on Thursday, October 8, Ravi opens a folder of sample files that arrived overnight: a store sales export, a returns export, a product catalog extract and a supplier price list. "Before we load anything," he says, "we need to know what we are holding." Choosing the right load settings, the right storage and the right column types all depends on understanding the format and the schema. Mistakes here are cheap to fix on day one and expensive to fix after a year of loads.

Structured, semi-structured and unstructured data

Lesson 1 introduced the three kinds of data. The distinction matters because each kind tends to have its own home on Google Cloud. Structured data, with fixed columns and types, fits tables in BigQuery or a relational database such as Cloud SQL. Semi-structured data, such as JSON with nested or optional fields, can be loaded into BigQuery using nested and repeated columns or the JSON data type, or kept in a document database such as Firestore. Unstructured data, such as images, PDFs and audio, lives as objects in Cloud Storage, and can be referred to from BigQuery through object tables when it needs to be analyzed.

The three kinds of data and where Birchline keeps each.
KindBirchline exampleTypical homeHow it is queried
StructuredStore sales, online ordersBigQuery tables; Cloud SQL for the shop itselfSQL over columns
Semi-structuredReturns export, web events, product attributesBigQuery nested columns or JSON columns; Firestore for app dataSQL with field paths, UNNEST and JSON functions
UnstructuredProduct photos, scanned supplier invoicesCloud StorageObject tables and AI functions, or outside BigQuery

Common file formats

Most data reaches Google Cloud as files, and BigQuery can load several file formats directly. They differ in whether they carry their own schema, whether they are compressed and splittable, and whether they are stored by row or by column.

File formats that BigQuery can load, and how they compare.
FormatLayoutCarries its schema?StrengthsWeaknesses
CSVText, one row per lineNo (a header row at most)Universal, easy to read and produceNo types, ambiguous quoting and delimiters, no nested data
JSON (newline-delimited)Text, one JSON object per lineNo, but fields are namedHandles nested and optional fieldsLarger than binary formats; types can be guessed wrongly
AvroBinary, by rowYes, embeddedSelf-describing, compact, good for streaming and exportsNot readable without tools
ParquetBinary, by columnYes, embeddedCompact, fast for reading a few columns, widely used in data lakesNot readable without tools; less suited to row-by-row writes
ORCBinary, by columnYes, embeddedSimilar to Parquet, common in Hadoop systemsLess common outside the Hadoop world

BigQuery's documentation notes that JSON must be newline-delimited for loading: each line holds one complete record. A file that contains a single large JSON array, as many applications export, has to be converted first. The returns portal does exactly that, so it is one of the reasons the returns data needs a light transformation before loading.

For loading speed, Google's documentation recommends Avro as the preferred format for loading into BigQuery because its data blocks can be read in parallel even when compressed, and Parquet and ORC also load efficiently. Compressed CSV and JSON files load more slowly because a gzip file cannot be split, so a single worker has to read it from start to finish. For Birchline's small store files that hardly matters. For the ten-year history export from the old warehouse, it matters a great deal, so Ravi plans to export that history to Parquet or Avro rather than to gzipped CSV.

Two grids of the same small sales table. On the left, labeled row layout (CSV, JSON, Avro), colored bands run across each row, showing that whole records are stored together. On the right, labeled column layout (Parquet, ORC), colored bands run down each column, showing that each column is stored together. A note under the column layout says reading two of eight columns touches only those two.
Row formats keep each record together; column formats keep each column together, which helps when a query reads only a few columns.

What a schema is

A schema describes the shape of the data: the names of the fields, their data types, whether they may be empty, and for nested data, how the fields fit inside each other. In BigQuery, every table has a schema, and each column has a name, a type and a mode. The mode is NULLABLE (the value may be missing), REQUIRED (it must be present) or REPEATED (the column holds an array).

A schema can come from three places. Self-describing formats such as Avro, Parquet and ORC carry it inside the file. For CSV and JSON, BigQuery can auto-detect a schema by sampling the file, or you can supply one explicitly, as a JSON schema file or in the load command. Auto-detection is handy for exploring a file, but Ravi insists on explicit schemas for anything that will be loaded every night. A sample that happens to contain only whole numbers will make a price column an INTEGER, and the first night a price of 24.99 arrives, the load fails.

The explicit schema file for the nightly store sales load (pos_sales_schema.json).
[
  {"name": "store_id",   "type": "STRING",    "mode": "REQUIRED"},
  {"name": "txn_id",     "type": "STRING",    "mode": "REQUIRED"},
  {"name": "txn_ts",     "type": "TIMESTAMP", "mode": "REQUIRED"},
  {"name": "sku",        "type": "STRING",    "mode": "REQUIRED"},
  {"name": "qty",        "type": "INT64",     "mode": "REQUIRED"},
  {"name": "unit_price", "type": "NUMERIC",   "mode": "REQUIRED"},
  {"name": "discount",   "type": "NUMERIC",   "mode": "NULLABLE"},
  {"name": "tender",     "type": "STRING",    "mode": "NULLABLE"}
]

BigQuery data types that matter for loading

Choosing the right type for each column prevents a whole category of errors. Mateo works through the store sales columns one by one with Ravi, and the reasoning is more useful than the final list.

BigQuery data types Mateo uses most often, with the mistakes each one prevents.
TypeUse forBirchline exampleCommon mistake
STRINGText, and codes that look like numbersstore_id "007", sku, postal codesMaking codes INT64 and losing leading zeros
INT64Whole numbers you count or addqtyUsing it for identifiers that are not quantities
NUMERICExact decimals, especially moneyunit_price, discountUsing FLOAT64 for money and getting rounding errors
FLOAT64Measurements where tiny rounding is acceptableDelivery distance, sensor readingsUsing it for currency
BOOLTrue or falseis_loyalty_memberLoading "Y" and "N" without converting
DATEA calendar date with no timebusiness_dateStoring dates as text like "10/08/2026"
TIMESTAMPAn exact moment, stored in UTCtxn_tsForgetting which time zone the source used
DATETIMEA date and time with no time zoneA store's local opening timeMixing it with TIMESTAMP in comparisons
JSONSemi-structured values whose fields varyRaw web event payloadsUsing it when the fields are known and stable
STRUCT and ARRAY (RECORD, REPEATED)Nested fields and lists inside a rowAn order with its order linesFlattening everything and duplicating order headers

Two choices deserve a closer look. First, money should be NUMERIC, not FLOAT64. A floating-point number cannot represent most decimal fractions exactly, so adding thousands of prices can produce totals that are off by a fraction of a cent. That is harmless for a temperature but not for a ledger that Finance reconciles to the cent.

Python Example
# Why money should not be stored as a floating-point number.
from decimal import Decimal

prices = [0.10] * 30_000          # thirty thousand items priced at ten cents
float_total = sum(prices)
exact_total = sum(Decimal("0.10") for _ in range(30_000))

print(f"Float total:   {float_total!r}")
print(f"Exact total:   {exact_total}")
print(f"Difference:    {Decimal(repr(float_total)) - exact_total}")
Output
Float total:   3000.0
Exact total:   3000.00
Difference:    0.00

What this code does

It adds up thirty thousand prices of ten cents twice: once with ordinary floating-point numbers and once with exact decimal numbers, like BigQuery's NUMERIC type. The exact total is precisely 3,000 dollars, while the floating-point total is slightly off because ten cents cannot be stored exactly in binary. The tiny difference shows why money columns should use an exact decimal type.

Second, timestamps need a known time zone. The store tills record local time without a zone. Birchline's stores span more than one time zone, so 9:00 at a store in Denver is not the same moment as 9:00 in Columbus. Loading the till time straight into a TIMESTAMP column would treat it as UTC and shift every sale by several hours. The team decides to load the till time as DATETIME in a raw column, add each store's time zone from the store reference table, and compute a proper TIMESTAMP in staging.

Nested and repeated data

BigQuery can store nested data directly. An online order with three order lines can be one row whose lines column is an ARRAY of STRUCTs, each holding a sku, a quantity and a price. Storing data this way avoids repeating the order header three times and often makes queries cheaper, because related data is read together. JSON files with nested objects load naturally into this shape, and Avro and Parquet carry nested schemas too.

One online order as a newline-delimited JSON record, with its order lines nested inside.
{"order_id": "W-1048812", "placed_at": "2026-10-07T14:22:05Z", "customer_ref": "C-88213",
 "lines": [{"sku": "TNT-2P-GRN", "qty": 1, "price": "289.00"},
           {"sku": "STK-AL-08",  "qty": 2, "price": "14.50"}]}

The alternative is the JSON data type, which stores a whole JSON value in one column and lets queries reach into it with JSON functions. It suits data whose fields change often or are not known in advance, such as the raw payload of a web event that the shop's developers keep extending. Where the fields are known and stable, typed nested columns are better, because each field gets a proper type and BigQuery can read just the fields a query needs.

Schema evolution

Sources change. The shop team adds a gift_message field; a supplier renames a column; the till software update adds a loyalty_id. A load that expects the old schema can fail, or worse, silently put values in the wrong columns. BigQuery allows some changes safely when you append data: adding new NULLABLE columns and relaxing a REQUIRED column to NULLABLE. These are controlled by load options that allow field addition and field relaxation. Other changes, such as renaming a column or changing its type, need a deliberate migration.

For CSV files, column order matters, because a CSV has no field names apart from an optional header. If a supplier inserts a new column in the middle, every column after it shifts one place. JSON and the binary formats match fields by name, so a new field does not move the others. This is one more reason Ravi asks partners to send newline-delimited JSON or Parquet when they can.

How common source changes affect loads, and what Birchline does about each.
Change in the sourceEffect on a CSV loadEffect on a JSON or Avro loadTeam response
New column added at the endLoad fails unless allowed or ignoredExtra field ignored or added if allowedAllow field addition in staging, review, then add to the schema
New column added in the middleLater columns shift; values land in the wrong columnsNo shift, fields are matched by nameReject the file and ask for the agreed layout
Column renamedValues load under the old nameOld column empty, new field ignoredUpdate the schema and the mapping deliberately
Type changed (price becomes text)Load errors on bad valuesLoad errors on bad valuesFix at the source or load into a text column and convert in staging

A data contract

To reduce surprises, the team writes a short data contract for each regular feed: a document agreed with the people who produce the data, stating the format, the schema, the delivery time, the file naming pattern and who to contact when something changes. It is not a Google Cloud feature, just good practice, but it turns schema changes from midnight surprises into planned work. The store systems team agrees the first contract for the till exports: CSV with a header, UTF-8 encoding, comma delimiter, one file per store per night named pos_STORE_YYYYMMDD.csv, delivered by 02:00 local time.

A one-page card titled Data contract: store sales export. It lists Producer: store systems team; Consumer: analytics team; Format: CSV with header, UTF-8, comma; Schema: eight named columns with types; File name: pos_STORE_YYYYMMDD.csv; Delivery: by 02:00 local; Change notice: 30 days; Contact: store systems on-call.
The data contract for the nightly store sales export.

Common mistakes

  • MistakeRelying on schema auto-detection for a nightly load.

    InsteadUse an explicit schema so that one unusual sample cannot change a column's type.

  • MistakeStoring prices as FLOAT64.

    InsteadUse NUMERIC for money so that totals are exact.

  • MistakeMaking store numbers and postal codes INT64.

    InsteadKeep codes that are not quantities as STRING to keep leading zeros and letters.

  • MistakeLoading local till times straight into a TIMESTAMP column.

    InsteadLoad them as DATETIME, add the store's time zone and convert in staging.

  • MistakeLoading a JSON file that holds one big array.

    InsteadConvert it to newline-delimited JSON, one record per line, before loading.

Worked problem

Choosing types for the returns export

A returns record has: return_id ("R-20261007-0042"), store_id ("012"), order_ref (sometimes empty), returned_at ("2026-10-07 16:40", store local time), refund_amount ("64.95"), reason_code ("SZ"), and items, a list of skus with quantities. Choose a BigQuery type and mode for each.

Show solution
  1. return_id: STRING, REQUIRED (an identifier, not a number).
  2. store_id: STRING, REQUIRED (keeps the leading zero).
  3. order_ref: STRING, NULLABLE (in-store returns without an online order leave it empty).
  4. returned_at: DATETIME, REQUIRED in raw; convert to TIMESTAMP in staging using the store's time zone.
  5. refund_amount: NUMERIC, REQUIRED (money).
  6. reason_code: STRING, REQUIRED.
  7. items: RECORD, REPEATED, with sku STRING and qty INT64 inside.

Answer: Identifiers and codes as STRING, money as NUMERIC, the local time as DATETIME until converted, and the item list as a repeated record.

Summary and key terms

  • Structured data fits tables, semi-structured data fits nested or JSON columns, and unstructured data lives in Cloud Storage.
  • CSV and JSON are text formats without embedded types; Avro, Parquet and ORC are binary and carry their own schema.
  • Avro is Google's preferred load format, and compressed CSV and JSON load more slowly because gzip files cannot be split.
  • JSON must be newline-delimited to load.
  • Use explicit schemas for regular loads, NUMERIC for money, STRING for codes, and handle time zones deliberately.
  • Nested and repeated columns store related data together; the JSON type suits fields that change often.
  • Appending can add NULLABLE columns or relax REQUIRED ones, but renames and type changes need planning.
  • A data contract agrees format, schema and delivery with the data's producer.
Schema
The names, types and modes of the fields in a dataset.
Mode
Whether a BigQuery column is NULLABLE, REQUIRED or REPEATED.
Auto-detection
BigQuery inferring a schema by sampling a CSV or JSON file.
Newline-delimited JSON
A file with one complete JSON record on each line.
Columnar format
A file layout that stores each column together, such as Parquet or ORC.
NUMERIC
BigQuery's exact decimal type, suited to money.
STRUCT and ARRAY
Nested fields and lists stored inside one BigQuery row.
Data contract
An agreement with a data producer on format, schema, delivery and change notice.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Why does Ravi avoid auto-detection for nightly loads?

    Show answer

    A sample can lead BigQuery to choose the wrong type, and the load then fails or misreads later data.

  2. Which formats carry their own schema?

    Show answer

    Avro, Parquet and ORC.

  3. Why are compressed CSV files slow to load in bulk?

    Show answer

    A gzip file cannot be split, so one worker must read it from start to finish.

  4. What happens to a CSV load if a column is inserted in the middle?

    Show answer

    Every later column shifts, so values land in the wrong columns.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz