✦ Yeedu Hits $0.53/TB in a TPC-DS Benchmark
Check-with-circle-green-icon
Blog
Yeedu Team
August 6, 2026

"Git for data" solved branching. It never solved merging.

"Git for data" solved branching. It never solved merging.

Database branching now creates an isolated, full copy of a production dataset in under a second, on Neon, PlanetScale, lakeFS and Dolt alike. What none of them do reliably is merge that branch back: schema diffing exists in limited form, but automatic reconciliation of divergent data, across row identity, foreign keys, and constraints, remains an unsolved problem every vendor quietly routes around.

That gap matters because branching is being sold as a git workflow for data, and git's whole value proposition rests on the second half: you branch so you can later merge with confidence. Take that away and we are left with a very fast way to create divergence, and no correspondingly fast way to resolve it.

Why branch creation got easy and merge did not

Copy-on-write made the creation half trivial. Neon streams Postgres WAL into a disaggregated storage tier, so a new branch just references the parent's existing pages and only writes new ones when data actually changes, which is why branch creation completes in under a second regardless of whether the parent holds a gigabyte or a terabyte.

lakeFS does the analogous thing at the object-store layer: branches are pointers into commits, not physical copies, which is how lakeFS avoids duplicating data even at multi-petabyte scale, branching instantly regardless of how large the underlying lake is.

Dolt takes a different route to the same result. It chunks tables into roughly 4KB sections using Prolly Trees, so identical chunks across branches are stored exactly once. That structural sharing is, per Dolt's own writeup, the thing that lets branching scale past a handful of environments instead of collapsing into full-copy sprawl once you're running thousands of ephemeral branches.

None of that engineering touches merge. Git's merge works because source files are line-oriented text with no referential integrity to preserve. Database rows aren't like that: a row has identity that may be synthetic or reused across branches, foreign keys create dependency chains where merging a child requires its parent to already exist, and unique or check constraints that each branch individually satisfies can conflict the instant you combine them.

Diff granularity compounds this: the level at which a system can diff is generally the level at which it can merge, and coarse, table-level diffs mean two branches touching the same table conflict even when they never touch the same row.

What does each platform actually do when two branches diverge?

Results vary sharply, and the variance is the story. Neon's branching documentation does not mention merge at all. Turso is explicit that you're on your own: "you need to handle merging any schema changes or data manually." PlanetScale's data branches don't sync data back to production under any circumstance, full stop.

PlatformBranch creationData-level mergeSchema-level mergeScope
NeonInstant, WAL/page-level CoWNot supportedNot supportedPostgres only
PlanetScaleFull copy (Vitess-managed)Not supportedThree-way schema diff (Vitess schemadiff)MySQL, schema-only
TursoInstantManual onlyManual onlylibSQL/SQLite
lakeFSInstant, pointer-basedCommit/object-level onlyN/AAny object store, petabyte scale
NessieInstantCommit-level onlyN/AIceberg tables only
DoltProlly-tree CoWRow-level, git-likeRow-level (schema treated as data)Requires Dolt-native storage
Copy-on-write branch creation followed by independent divergence with no corresponding merge path back
Branch creation is instant and cheap via copy-on-write; branches diverge under independent writes with no built-in path back to merge

Does PlanetScale's three-way merge actually merge data?

No. It merges schema, and only schema. The algorithm computes two diffs, main-to-branch-A and main-to-branch-B, then tests applying both orders and flags a conflict if either application fails or the two orderings produce different results.

It's built on Vitess's schemadiff library doing semantic SQL comparison rather than textual diffing, which is a real engineering advance over naive DDL replay. It still has nothing to say about the rows inside those tables.

The merge check runs in four steps:

  1. Compute diff1, the schema delta from main to branch A.
  2. Compute diff2, the schema delta from main to branch B.
  3. Apply diff1 then diff2, and separately apply diff2 then diff1, against a copy of main.
  4. Compare the two resulting schemas; if either application fails or the results disagree, flag a conflict instead of merging.
PlanetScale's three-way schema merge: diff1 and diff2 applied in both orders and compared for conflicts
diff1 and diff2 are applied in both orders against a copy of main; disagreement between the two results flags a conflict

The subtlety here is worth naming because it is the kind of bug that survives code review. If branch A adds a column and branch B adds a different column to the same table, PlanetScale flags it as conflicting, correctly, because a SELECT * with positional argument binding would resolve columns 3 and 4 differently depending on merge order.

Index ordering, by contrast, is intentionally ignored. That's a deliberate, documented tradeoff. It is also proof that even the most sophisticated schema-merge implementation in this space stops at the schema boundary.

Where the abstraction leaks in practice

Here's where the git analogy actively misleads us when we build CI workflows around it. Teams wire up per-PR preview branches on Neon through GitHub Actions or the Vercel integration, and the branch gets created and destroyed automatically as PRs open and close. That part works well.

What breaks is cost hygiene, and it is a direct consequence of how branch billing works, as we found. But storage bills at roughly $0.35/GB-month even while compute is fully suspended, and compute bills per CU-hour rather than per wall-clock hour, a metering model that punishes workloads sized like a fixed environment rather than a metered one.

Preview branches are the common failure mode: spun up automatically per PR, they're easy to leave running long after the PR merges, and each one keeps billing as an independent database until someone tears it down.

Write amplification is the other leak, and it shows up as a measurable performance number rather than an abstract caveat. On ZFS-backed copy-on-write setups, Postgres's 8KB page size against ZFS's default 128KB record size produces up to 16x write amplification.

Tuning the record size helps proportionally. Disabling full_page_writes in one benchmark took throughput from 6,000 to 10,325 TPS, a roughly 70% gain, which tells you the default configuration was leaving real performance on the table specifically because of branching's storage model.

Branching's limits as a staging replacement

At performance certification, the git analogy stops holding. Xata states this directly: a branch is the right place to verify a migration completes without error, and the wrong place to certify a latency SLO. Isolation guarantees correctness testing, not production-equivalent load behavior, and conflating the two is an easy mistake we make once branch creation feels as cheap and instant as it does.

Why does this get worse specifically for large mutable OLTP tables?

Because that is exactly where merge conflicts compound fastest and where teams are most tempted to skip staging in favor of a "just branch it" workflow. The longer two branches diverge before merging, the higher the odds of conflict, and high-write OLTP tables diverge fast by definition.

lakeFS and Nessie sidestep this by scoping merge to the commit or object level rather than the row level, which works for immutable lakehouse files but does not generalize to a customers table taking writes every second. Nessie in particular is scoped to Iceberg tables only, so it can't version arbitrary mutable state the way a relational branch tool implies it should.

What actually solves data-level merge, and what does not yet

Dolt is the one system in this comparison that merges data, not just schema, because its Prolly Tree structure gives it row-level, content-addressed diffing analogous to git's line-level diffing. The cost is scope: Dolt only works if your data lives inside Dolt's own storage format, so it is a poor fit for teams whose data already sits in Postgres, MySQL, or an existing lakehouse and isn't moving.

The frontier of the problem is now visible in academic work rather than vendor blogs. A recent paper, GitLake, proposes lifting single-table Iceberg snapshots into lakehouse-wide commits, branches, and merges, explicitly because per-table snapshotting does not compose across a multi-table warehouse.

It is motivated by agentic pipelines, where multiple automated writers work isolated branches concurrently and a human reviews before an atomic, all-or-nothing publish. The authors back their design with a preliminary Alloy formal model of the merge abstraction, which is a tell in itself: correctness of branch-merge semantics is still open enough that people are reaching for formal verification rather than shipping and hoping.

Branching is still a genuinely useful primitive for what we've found it's actually good at: throwaway environments, migration dry runs, PR previews. It stops being a git analogy the moment two branches need to become one again, and every platform in this space still asks a human to do that part by hand.

Back to blogs
Join our Insider Circle
Get exclusive content crafted for engineers, architects, and data leaders building the next generation of platforms.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
No spam. Just high-value intel.
Back to blogs