MySQL and MariaDB to BigQuery

Move operational data into BigQuery without shipping the cleanup downstream.

A useful pipeline does more than copy columns. JoinLayer lets you define the BigQuery row, filter and shape MySQL data, add reference context, preview the result, validate the route, and understand every run from launch to recovery.

The first MySQL-to-BigQuery pipeline often looks successful long before it becomes useful. The connection works. Rows arrive. The run is green. Then an analyst opens the table and finds customer IDs instead of customer names, prices in cents, internal status codes, and no clear answer for what a replay will do.

The transport succeeded. The data product did not.

The warehouse received the rows—and inherited the problem

Application databases are shaped for transactions. An orders table should be fast to update, strict about foreign keys, and close to the code that owns it. BigQuery consumers need something different: stable names, understandable values, enough business context to answer a question, and a contract that does not change accidentally.

A raw replica can be exactly right for audit and recovery. It is a weak default for a landing table that finance, product, support, or operations will use directly. Without an explicit row contract, every consumer repeats the same cleanup and each version drifts a little further from the others.

The valuable decision is not “Can we copy this table?” It is “What should one trustworthy destination row mean?”

Start with the row people actually need

Suppose the source says order 10482 belongs to customer 701 and product 19. Those identifiers are useful inside the application. They do not tell a warehouse user who bought what, which segment owns the revenue, or whether P means paid or pending.

MySQL source row

{
  "id": 10482,
  "customer_id": 701,
  "product_id": 19,
  "total_cents": 12900,
  "status": "P",
  "created_at": "2026-09-09T08:41:12Z"
}

BigQuery landing row

{
  "order_id": 10482,
  "customer_name": "Northwind Labs",
  "customer_segment": "growth",
  "product_name": "Workspace Plus",
  "total_amount": 129.00,
  "order_status": "paid",
  "created_day": "2026-09-09"
}

That change is not a warehouse model or an aggregate. The order is still the source fact. JoinLayer simply makes the landing contract explicit: map the fields that belong, derive simple values, filter records that do not, and join the reference fields the destination needs.

A pipeline should become understandable before it becomes large

In JoinLayer, the route begins with two tested connections and one concrete source table. The BigQuery dataset already exists; the pipeline names the target table inside it. Credentials stay inside the encrypted connection rather than leaking into mappings, prompts, or run configuration.

The mapper is where the destination row becomes visible. Rename id to order_id. Turn cents into a readable amount. Map an internal status to the value consumers expect. Add the customer and product reference tables, select only the fields that should cross the boundary, and decide what a missing match means.

Then preview the result. A preview is more useful than a successful connection test because it exposes the actual contract: field names, nulls, types, lookup matches, filters, and derived values. Validation checks that contract against the selected destination before a large run makes mistakes expensive.

What remains outside this workflow?

JoinLayer does not create a BigQuery project or dataset, invent warehouse metrics, or perform destructive schema migrations. Those remain explicit infrastructure and modeling decisions.

Reference data changes the economics of the route

Enrichment can be as simple as looking up 1,000 products or as demanding as matching millions of accounts for every historical row. The right mode depends on freshness, source-database load, lookup width, and execution capacity.

Direct lookup reads current reference values in execution batches. It is usually the better starting point when the lookup key is indexed, the table is modest, and freshness matters more than removing every repeated query.

Materialized lookup loads selected reference fields into a managed DuckDB cache near execution. It reduces repeated traffic against the customer database and can improve high-volume enrichment, but the cache consumes declared capacity and is only as fresh as its refresh policy.

Whichever mode you choose, keep the contract narrow. Loading four useful columns is cheaper and easier to govern than copying an entire customer table “just in case.” A missing match must also be intentional: keep the row with nulls or configured fallbacks, skip it, or fail the run.

BigQuery does not choose your consistency contract for you

JoinLayer always preserves the same pipeline order: read, transform and enrich, write to the destination, save the durable checkpoint, then confirm source progress. The checkpoint never moves ahead of a failed target write.

The target guarantee still depends on the BigQuery write mode. Storage Write is the recommended gRPC path. It uses application-owned committed streams and durable offsets to provide exactly-once append for the same deterministic JoinLayer batch. Legacy insertAll is best-effort streaming. Batch load uses deterministic GCS objects and load-job identities but remains an at-least-once append contract.

These are append-event guarantees, not relational upsert. If the table represents mutable business state, consumers need a deduplicating view keyed by stable identity until a BigQuery merge mode exists.

Schema evolution should be visible, not surprising

The target dataset and permissions must already be in place. From the mapped row, JoinLayer can create a missing table or add missing nullable columns. Partitioning and clustering are explicit create-time choices.

When a later change is incompatible with the existing type or table layout, the workspace policy decides what happens. The route either blocks for review or moves to a versioned table such as orders_v2. The original table stays available; JoinLayer does not quietly drop, rename, narrow, or rewrite it.

History and realtime are separate decisions on this route

A full load moves existing rows. A scheduled run repeats a finite snapshot on a timetable. Realtime reads new MySQL changes from row-based binlogs with full row images. Realtime alone does not load the table's existing history.

Important current boundary: coordinated realtime-with-history requires a PostgreSQL or MySQL target. For BigQuery, plan the historical load and CDC transition as separate steps and verify the cutover with controlled keys and counts.

That boundary matters. A large history load can take hours or days, while the binlog keeps moving. Treating the transition as “press run twice” invites gaps or duplicates. The safe launch is designed, observed, and reconciled—not assumed.

A failure should leave evidence, not guesswork

Each run records its mode, worker, stage, row counters, failure code, and actionable diagnostic. That separates a source permission problem from a lookup miss, a schema blocker, a target rejection, or lost worker ownership.

Recovery starts with that evidence. If a batch load reached BigQuery but staging cleanup failed, a blind retry may write data twice. If a realtime worker disappeared, starting a second competing run is not a repair. Check durable state, target-side evidence, and the selected write contract before deciding how to continue.

Before the first production run

Make both systems ready.Test MySQL and BigQuery access. For CDC, verify row-based binlogs, full row images, retention, and a pipeline-scoped replication identity. For batch load, configure the GCS staging bucket.
Review the row and the table.Confirm every mapped, derived, filtered, and enriched field; lookup miss behavior; sensitive-field exclusions; target schema; partitioning; clustering; and schema-evolution policy.
Record the guarantees you chose.Name the run mode, BigQuery write mode, replay and deduplication expectations, and the history-to-realtime transition. “BigQuery target” alone is not a consistency contract.
Verify before and after launch.Inspect preview rows, pass validation, confirm capacity, then watch durable run state and counters. Sample destination keys, nulls, enriched values, and duplicate behavior before handing the table to consumers.

The destination should be useful on its first day

A reliable MySQL-to-BigQuery route is not defined by how quickly a connector turns green. It is defined by whether the team can explain the row, the lookup behavior, the target schema, the write guarantee, and the recovery path before production data depends on them.

That is the layer JoinLayer is built to own: the source and destination stay under customer control, while the contract between them becomes visible, previewable, enforceable, and diagnosable.

Related reading