Moving Most of Our Data from Cloud SQL to BigQuery

·5 min read

Listen to this article· 3 min

Photo by Iain on Unsplash

When I joined the team, we already had a working platform.

We had pipelines.

We had dashboards.

We had Cloud SQL.

We had GKE.

Nothing was obviously broken.

But after spending more time looking at how the platform was actually being used, I started asking a simple question:

Why are we using a relational database to hold data that is mostly being ingested once and then read for analytics?

That question eventually led to one of the bigger changes I worked on: moving the majority of our pipeline data from Cloud SQL to BigQuery, while keeping Cloud SQL for the smaller set of databases that actually needed transactional CRUD operations.

It wasn't a "Cloud SQL is bad, BigQuery is good" story.

It was a workload mismatch story.

Cloud SQL Was Doing Two Very Different Jobs

At the beginning, Cloud SQL was doing more than one job.

Some applications genuinely needed a relational database:

ApplicationCloud SQLCreate / Read / Update / Delete

That makes sense.

These workloads need things like:

  • frequent row-level updates
  • transactional behavior
  • application-driven CRUD
  • predictable access to individual records

Cloud SQL is designed for exactly this kind of relational application workload.

But other workloads looked completely different:

Data sourcePipelineCloud SQLDashboard

The pipeline was mostly loading data.

The dashboard was mostly reading data.

And the data was being used to generate:

  • Charts
  • Graphs
  • Aggregations
  • KPIs
  • Historical views
  • Insights

That is much closer to an analytical workload than a transactional one.

BigQuery is built around analytical processing and large-scale querying rather than OLTP-style, single-row application transactions. Google explicitly recommends systems such as Cloud SQL for OLTP use cases.

So we had a database doing a job it wasn't necessarily the best fit for.

And we were paying for the infrastructure too.

The Cost Problem

The other problem was cost.

Cloud SQL uses provisioned instances, and CPU and memory for dedicated-core instances are tied to the instance configuration. Instance pricing is charged while the instance is running.

That means if you provision an instance with more capacity than your workload actually needs, the bill doesn't politely ask:

"Hey, are you using those CPUs today?"

It just keeps running.

And in our case, a large portion of the data workloads didn't need a large always-on relational database.

That made the architecture worth questioning.

The problem wasn't simply:

Cloud SQL = expensive

It was:

The real mismatch
  1. 01Always-on relational capacity
  2. 02Mostly ingestion
  3. 03Mostly analytical reads

We were paying for a model that didn't match how the data was being used.

The Architecture Started to Make More Sense

The new direction was much cleaner.

Instead of using Cloud SQL as the main destination for all this data:

Before
  1. 01Pipelines
  2. 02Cloud SQL
  3. 03
    Consumers
    DashboardsServices

we moved toward:

After
  1. 01Pipelines
  2. 02BigQuery
  3. 03
    Consumers
    DashboardsAnalytics

And Cloud SQL stayed where it made sense:

ApplicationsCloud SQLCRUD workloads

So instead of forcing one database to do everything, we gave each workload a better home.

That was the core idea behind the migration.

I Led the Change

This wasn't just a technical migration.

I had to understand the existing architecture, identify which datasets were suitable, look at how they were produced, understand how dashboards consumed them, and work out how to move the data without breaking the applications around it.

The challenge was not simply:

Cloud SQLBigQuery

It was:

What actually had to move
  1. 01Existing pipelines
  2. 02Existing schemas
  3. 03Existing transformations
  4. 04Existing dashboards
  5. 05Existing permissions
  6. 06New analytical architecture

And that meant learning BigQuery properly rather than treating it like "another SQL database."

That was probably the biggest part of the journey.

My Mental Model of a Database Had to Change

Coming from application development, my database mental model was very relational:

TableRowsIndexesQueriesTransactions

BigQuery made me think differently.

The mental model became:

DatasetTablesLarge analytical scansAggregationsInsights

BigQuery uses columnar storage and separates storage from compute, which is one of the reasons it can scale analytical workloads without asking me to provision and maintain a database server in the traditional sense.

That was a major mindset shift.

With Cloud SQL, I was thinking:

"How big does the database instance need to be?"

With BigQuery, I started thinking:

"How should the data be organized so queries only process what they need?"

That led me into concepts like:

  • Partitioning
  • Clustering
  • Query scanning
  • Data volume
  • Table design
  • Cost per query

Partitioning became especially important because filtering on the partitioning column can reduce the amount of data scanned, which improves both performance and cost.

That was a completely different way of thinking about optimization.

Some Queries Needed to Be Rethought

One of the challenges was that not every query could simply be copied from Cloud SQL and expected to behave the same way.

Some application-style assumptions don't translate well into an analytical system.

BigQuery supports UPDATE, DELETE, and MERGE, but Google still recommends avoiding patterns that try to use BigQuery like an OLTP database, especially frequent single-row mutations.

Luckily, that wasn't really our main pattern.

Most of our pipelines were doing something closer to:

ExtractTransformLoadRead

rather than:

UPDATE row 123
UPDATE row 124
DELETE row 125
UPDATE row 126

That made BigQuery a much more natural fit.

The Migration Was Also a Data Engineering Lesson

Another thing I learned was that moving data is not the same as moving tables.

A table can exist in both systems and still behave differently.

We had to think about:

  • Schema
  • Data types
  • Historical data
  • Incremental loads
  • Transformation logic
  • Null handling
  • Timestamps
  • Dashboard queries
  • Permissions
  • Validation

And then there was the question nobody wants to discover after go-live:

"Why does the dashboard show a different number?"

So validation became important.

We needed confidence that:

These have to match
  • Cloud SQL result
  • BigQuery result

at least where the business logic was supposed to remain equivalent.

Google's own migration guidance recommends a staged, iterative approach rather than trying to move everything blindly in one step.

That matched what we were learning in practice.

The End Result Wasn't "Goodbye, Cloud SQL"

One thing I particularly liked about this migration is that we didn't turn it into a religious debate about technologies.

We didn't say:

"Everything must go to BigQuery."

Instead:

Need CRUD?
Cloud SQL
Need analytics?
BigQuery

That's a much healthier architecture.

Cloud SQL remained for the smaller number of databases that genuinely needed application-driven CRUD.

BigQuery became the better home for the larger analytical datasets powering pipelines, dashboards, graphs, and insights.

The result was also a more appropriate cost model for those workloads because we were no longer maintaining large relational database capacity just to store data that mostly needed to be loaded and queried.

What I Learned

The biggest lesson wasn't really BigQuery.

It was learning to look at how data is used, not just where it currently lives.

Before:

"We have a database, so let's put the data there."

After:

"What kind of workload is this data serving?"

That one question changes the architecture.

Transactional workload
Relational database
Analytical workload
Data warehouse

And sometimes the best optimization isn't reducing the size of your existing infrastructure.

Sometimes it's moving the workload to infrastructure designed for it.

From Full-Stack to Data Platform

This project was another important step in my transition from application development toward cloud and platform engineering.

I was no longer only building the dashboard consuming the data.

I was starting to work on the path that produced the data:

The path behind the dashboard
  1. 01Source
  2. 02Pipeline
  3. 03Transformation
  4. 04BigQuery
  5. 05GKE services / dashboards

That gave me a much better understanding of the whole system.

The dashboard isn't just a frontend.

Behind that graph is a pipeline.

Behind the pipeline is infrastructure.

Behind the infrastructure are IAM, storage, compute, networking, CI/CD, and cost decisions.

And once you start seeing that whole chain, you stop thinking of yourself as someone who only writes application code.

You start thinking about the platform that makes the application possible.

Final Thought

When we started, Cloud SQL wasn't "wrong."

It was simply being asked to do too many different jobs.

The migration taught me that good cloud architecture isn't about choosing the most modern service.

It's about matching the service to the workload.

For us, that meant:

CRUD
Cloud SQL
Analytics
BigQuery
Pipelines
Airflow / Composer
Applications
GKE
Delivery
GitLab

Suddenly the architecture made more sense.

And the funny thing is, I started this work thinking I was moving data from one database to another.

I ended up changing the way I think about data itself.

That's probably the more valuable migration.

…

Up to 50 claps per reader

All posts
Share