---
title: "Partitioning Relational Databases at Scale"
description: "Lessons from GitHub's MySQL partitioning: schema domains, boundary linters, consistency checks, cutover mechanics, and production rollout."
canonical: "https://tmtuan.dev/en/articles/partitioning-relational-databases-at-scale"
language: "en"
image: "https://tmtuan.dev/_img/articles/partitioning-relational-databases-at-scale/partitioning-relational-databases-at-scale-cover.3f6638ad-og.jpg"
updated: "2026-10-09"
tokens: 2804
---

# Partition Relational Databases Before You Move Data

## Context

A large relational database rarely becomes difficult because one table suddenly stops fitting on a machine. The harder problem is coupling. Years of joins, transactions, associations, and background jobs turn one database into a shared failure domain. Moving a table can then break an assumption hidden in application code, even when the data copy itself is straightforward.

GitHub described this transition in a 2021 engineering article. GitHub.com began as a Ruby on Rails application backed mostly by one MySQL database. Over time, the team added feature-specific databases, read replicas, and ProxySQL, but the main `mysql1` cluster still served core data for users, repositories, issues, and pull requests. Scaling that cluster required larger machines, and an incident on it could affect many unrelated features.\[1\]

The most reusable lesson is not a specific sharding product. GitHub first made database boundaries explicit in the application, then moved data. That ordering converted a risky infrastructure project into a sequence of contracts that could be linted, measured, and migrated independently.

## What the source says

GitHub introduced **schema domains** as virtual partitions. A domain groups tables that are frequently joined or updated in the same transaction. Every table had to belong to one domain, and a YAML configuration recorded that ownership. A schema linter checked that the configuration matched the actual database schema.\[1\]

Two more linters enforced the boundary.

The query linter ran in development and test environments. A query referencing tables from different domains raised an error, so developers found new coupling before it reached production. CI prevented accidental regressions. Existing violations could be annotated as exemptions, which made legacy dependencies visible without requiring the migration team to fix all of them at once.\[1\]

The transaction linter addressed a more subtle risk. A transaction that updates tables in two domains is safe while both domains live on one MySQL cluster, but that atomicity disappears after they move apart. GitHub sampled this linter in production to find real cross-domain transactions with low overhead. The team could then change code paths or adjust the data model. When consistency was essential, data was extracted into tables owned by the same domain.\[1\]

Once a domain was virtually isolated, GitHub could move it physically. The article describes two mechanisms:

- **Vitess vertical sharding.** Applications connected through VTGate, which routed MySQL-protocol traffic. VTTablet managed database instances, and VReplication copied tables between clusters.\[1\]\[2\]
- **A custom write cutover.** MySQL replication copied data to a destination cluster while ProxySQL kept writes routed to the old primary. During cutover, the old primary became read-only, the script captured its final GTID, waited until the destination had executed that GTID, stopped replication, changed routing, and enabled writes again.\[1\]\[3\]

GitHub reported that the six operational cutover steps completed in tens of milliseconds for its busiest tables when executed at the lowest-traffic period. That figure describes the prepared authority switch, not the preceding replication, validation, or complete migration. The process moved 130 busy tables supporting repositories, issues, and pull requests. Between 2019 and 2021, the combined query rate grew from an average of 950,000 queries per second on `mysql1` to 1.2 million across several clusters, while average load per host fell by half.\[1\]

These operational results come from GitHub's first-party engineering retrospective, not an independently reproduced benchmark. They show higher aggregate traffic alongside lower average load per host, but do not prove that partitioning caused the traffic growth or quantify the incident reduction.\[1\]

## What I would validate before adopting it

### 1\. Domain boundaries must follow invariants, not team names

A service ownership chart is not automatically a safe database partition. Tables belong together when a correctness rule requires a local join or transaction, or when their access patterns make independent movement impractical. GitHub's example keeps tightly coupled feature tables in one schema domain.\[1\]

I would build an inventory from query logs, ORM associations, foreign keys, transaction traces, and batch jobs before proposing domains. The initial map should be treated as a hypothesis. A production transaction linter can reveal dependencies that static code inspection misses.

### 2\. An exemption must be debt with an owner

Annotations let migration proceed incrementally, but they can also normalize permanent boundary violations. Each exemption should record the affected domains, owner, traffic level, consistency requirement, and intended removal strategy. CI should reject new unapproved exemptions, while a dashboard tracks the existing backlog.

The remediation depends on semantics. A cross-domain join may become two reads joined in application memory. An ORM association may use separate queries instead of a SQL join. GitHub contributed `disable_joins` support for Rails associations for this reason. In other cases, duplicating a small piece of data inside the destination domain is safer because it preserves a local transaction.\[1\]

These options are not equivalent. Application-side joins can increase round trips and memory use. Data duplication introduces synchronization and repair work. The right choice follows from cardinality, freshness requirements, and failure behaviour rather than a blanket ban on joins.

### 3\. Transaction analysis matters more than query analysis

A query linter proves that one statement stays inside a domain. It does not prove that an entire business operation does. A request can issue several individually valid statements against separate domains inside what the application believes is one transaction.

Before moving data, I would classify each cross-domain transaction:

1. **Must remain atomic:** co-locate the required state or redesign the domain boundary.
2. **Can tolerate asynchronous consistency:** use an outbox, idempotent consumer, and reconciliation process.
3. **Can be reordered safely:** split the operation and define retry semantics explicitly.

This classification must include failure cases. What happens if the first domain commits and the second times out? Which operation is safe to retry? How is partial completion detected and repaired?

### 4\. Replication catch-up is necessary but not sufficient

Matching the final GTID proves that the destination has applied the source's replicated transaction history up to the cutover point.\[3\] It does not by itself prove that routing, permissions, schema, sequences, background workers, or read-after-write behaviour are correct.

I would rehearse the cutover with production-shaped traffic and verify row counts, checksums, replication lag, write error rate, connection draining, and rollback routing. The runbook needs explicit abort conditions before writes move. After routing changes, synthetic writes should confirm that the new primary accepts traffic and replicas serve expected reads.

GitHub accepted a handful of failed writes during a very short read-only window.\[1\] That trade-off may be reasonable for one product and unacceptable for another. The error budget and retry behaviour must be decided before choosing this cutover pattern.

## How I would apply this in production

I would run the migration as four gates rather than one database project.

**Gate 1: map coupling.** Assign every table to a proposed domain. Collect cross-domain queries and transactions without blocking traffic. Rank violations by request volume, write frequency, and consistency risk.

**Gate 2: enforce the virtual partition.** Turn query violations into development and CI failures. Sample transaction violations in production. Require reviewed, temporary exemptions for legacy paths. A domain is ready only when its unapproved violation count is zero and its remaining exemptions have migration plans.

**Gate 3: shadow the physical move.** Replicate the domain to its destination. Compare counts and checksums, replay representative reads, and measure destination capacity. Exercise cutover and rollback repeatedly. Freeze schema changes for the affected tables during the final window or provide a tested way to apply them to both sides.

**Gate 4: cut over and observe.** Choose either a proxy-driven write cutover or a migration layer such as Vitess based on topology and operational experience. Keep the source intact for rollback until the destination passes defined health checks. Monitor write errors, replication position, latency, connection saturation, and domain-specific business invariants rather than CPU alone.

The important architectural outcome is not merely smaller databases. It is a system where boundaries are executable: ownership is declared, invalid access fails early, legacy coupling is measurable, and every physical move has a known consistency model. Once those properties exist, partitioning becomes repeatable instead of heroic.

## Sources

\[1\] [https://github.blog/engineering/infrastructure/partitioning-githubs-relational-databases-scale](https://github.blog/engineering/infrastructure/partitioning-githubs-relational-databases-scale) — Partitioning GitHub's relational databases to handle scale \[2\] [https://vitess.io/docs/reference/features/sharding](https://vitess.io/docs/reference/features/sharding) — Vitess sharding reference \[3\] [https://dev.mysql.com/doc/refman/8.0/en/replication-gtids-concepts.html](https://dev.mysql.com/doc/refman/8.0/en/replication-gtids-concepts.html) — MySQL Reference Manual: GTID concepts

## Structured data (JSON-LD)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Article","@id":"https://tmtuan.dev/en/articles/partitioning-relational-databases-at-scale#article","headline":"Partition Relational Databases Before You Move Data","description":"Lessons from GitHub's MySQL partitioning: schema domains, boundary linters, consistency checks, cutover mechanics, and production rollout.","datePublished":"2026-10-09","dateModified":"2026-10-09","inLanguage":"en","url":"https://tmtuan.dev/en/articles/partitioning-relational-databases-at-scale","mainEntityOfPage":"https://tmtuan.dev/en/articles/partitioning-relational-databases-at-scale","image":"https://tmtuan.dev/_img/articles/partitioning-relational-databases-at-scale/partitioning-relational-databases-at-scale-cover.3f6638ad-og.jpg","author":{"@type":"Person","@id":"https://tmtuan.dev/#person","name":"Tran Manh Tuan","url":"https://tmtuan.dev/en"}},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://tmtuan.dev/en"},{"@type":"ListItem","position":2,"name":"Articles","item":"https://tmtuan.dev/en/articles"},{"@type":"ListItem","position":3,"name":"Partition Relational Databases Before You Move Data","item":"https://tmtuan.dev/en/articles/partitioning-relational-databases-at-scale"}]}]}
```
