PRODUCTS

KEYWORDS

Dolt vs DoltLite Storage Comparison

Dolt is the world’s first version-controlled SQL database, a MySQL-compatible server with Git-style branch, diff, and merge. DoltLite is a drop-in replacement for SQLite with the same Dolt-style version control features. Both get their versioning superpowers the same way. Underneath each one is a content-addressed chunk store full of Prolly Trees.

So they’re the same storage engine, right? Not even close. They share the math and almost nothing else. Dolt’s storage engine is Golang, adapted from Noms. DoltLite’s is new C, grafted under SQLite’s B-tree interface using Dolt’s source as a cheat sheet. Same blueprint, two completely independent implementations, and the differences are where it gets interesting.

This whole article can be summarized in one table. Continue reading to learn the “why” for each dimension.

DimensionDoltDoltLite
LanguageGolangC
LineageAdapted from NomsFork of SQLite
Base data structureProlly TreeProlly Tree
Chunk boundariesxxHash32 + Weibull, 4KB targetxxHash32 + Weibull, 4KB target
Content addressesSHA-512, truncated to 20 bytesBLAKE3, truncated to 20 bytes
Chunk compressionSnappy, plus zstd archivesNone
On diskA directory: table files, journal, archivesA single file
DurabilityChunk journalAppend-only commit batches, sealed manifests
ConcurrencyOne server process, many connectionsMany processes, one writer file lock
Garbage collectionAutomatic, online, generationalVACUUM or dolt_gc(), full rewrite
RemotesClone, push, pull: DoltHub, S3, GCS, fileClone, push, pull: file, HTTP (brand new)
MaturityGA, years in productionAlpha

Language#

Dolt is written in Golang. When we started Dolt in 2018, we wanted a memory-safe, garbage-collected language with great concurrency primitives for building a database server. Go is that. It’s also the language Noms was written in, and Dolt’s storage engine started life as a Noms adaptation, so Go was chosen for us as much as by us.

DoltLite is written in C because SQLite is written in C. DoltLite works by replacing SQLite’s B-tree layer, so the new storage engine has to speak SQLite’s internal C interfaces natively. That constraint comes with a prize. Like SQLite, DoltLite embeds anywhere C goes, which is everywhere. The storage engine and version control features are now about 50,000 lines of new C. At launch I bragged that only 8 lines of original SQLite code were changed. Three and a half months and a thousand PRs later, it’s about 2,000 changed lines. Nearly three-quarters of those are the Windows port, not the storage engine. The Prolly Tree surgery itself is still almost entirely additive, tucked behind #ifdef guards. vdbe.c has 459 added lines and only 19 changed ones.

Lineage#

Dolt’s storage engine descends from Noms, the decentralized database from Attic Labs that invented the Prolly Tree. We forked the Noms storage layer and spent years rewriting and hardening it into what ships in Dolt today. You don’t have to squint looking at Dolt’s chunk store to see Noms, it’s right there in the code and on disk.

DoltLite descends from SQLite, the most deployed database on earth. Everything from the B-tree interface up is line-for-line SQLite. That means the parser, the planner, the whole SQL engine. Below that interface, the B-tree is gone and a Prolly Tree chunk store sits in its place. That swap is what made our B-tree vs Prolly Tree bake-off possible. Same SQL engine, different storage engine, clean comparison.

Base Data Structure#

The base data structure is the same for both Dolt and DoltLite. Both engines store every table and index as a Prolly Tree. A Prolly Tree is a B-tree whose node boundaries are determined by the content of the data instead of insertion order. That makes the tree history-independent. Two databases with the same rows produce byte-identical trees no matter what order the rows arrived in. History independence is the foundation for structural sharing between versions, fast diff, and mergeable tables. Prolly Trees are the secret sauce, and both engines use the same recipe.

Chunk Boundaries#

Chunk boundaries are also the same, and this one is on purpose, down to the constants. Both engines decide where to end a chunk by hashing each key with xxHash32 (salted by tree level) and testing it against a probability curve shaped like a Weibull distribution with K=4. Chunks are never smaller than 512 bytes, never larger than 16KB, and target 4KB. The curve makes chunk sizes cluster tightly around the target instead of decaying geometrically, which keeps the tree balanced and the diffs small.

DoltLite copied Dolt’s splitter math deliberately. The boundary function is the tree shape, and the tree shape is what makes structural sharing work, so there was no reason to invent a second one. If you compare Dolt’s node_splitter.go against DoltLite’s chunker, you’re reading the same function in two languages.

Content Addresses#

Both engines name every chunk by a hash of its contents, truncated to 20 bytes. Twenty bytes is a deliberate choice, not a lazy one. Bigger addresses mean fewer child pointers fit in each internal node. Less fan-out means a deeper tree and slower everything. Twenty bytes is the balance point between collision resistance and wide trees.

But the hash function differs. Dolt uses SHA-512, inherited from Noms. DoltLite uses BLAKE3, which is very fast and ships as a clean, vendorable C implementation. BLAKE3 is faster and more modern and we’ve considered it for Dolt in the past. But changing the hashing algorithm is a storage format change that was a bridge too far for Dolt but made sense in the fresh build of DoltLite.

Can a Dolt database and a DoltLite database share chunks? No, and the hash function is only the last of the reasons. A chunk’s bytes are built from each engine’s own encodings. Dolt serializes rows with its own tuple codec inside FlatBuffers-framed nodes; DoltLite fills its nodes with SQLite record encodings and collation-aware sort keys. Same rows, different bytes. Different bytes mean different chunk boundaries, different chunks, and then different addresses on top. History independence holds within each engine, not across them. The two systems can’t push and pull to each other. They’re siblings, not clones.

Chunk Compression#

Dolt compresses every chunk with Snappy before it hits disk, and its archive format goes further. Chunks that survive garbage collection get repacked with zStandard dictionary compression, grouped by the commit graph so similar chunks share a dictionary. Archives shave an additional 30-50% off and have been on by default since Dolt 1.75.

DoltLite compresses nothing. Chunks are written raw. The one storage trick it does have is symbolic zero-tails. A chunk that ends in a run of zeros stores the run as a count instead of the bytes. Don’t expect that to change. SQLite doesn’t compress either, and DoltLite follows SQLite’s lead. Keep the file format simple and let the disk be cheap. The simplicity pays for itself, too. There’s a real debugging benefit to being able to open the file in a hex editor and read your chunks. I did exactly that chasing a garbage collection bug just this week.

On Disk#

A Dolt database is a directory. Inside .dolt you’ll find a manifest, table files holding the chunks, a journal, and archives. Storage that expects to hold terabytes wants to be spread over multiple files. Garbage collection can swap table files atomically. Archives can be rebuilt one at a time. The manifest ties the current set together.

A DoltLite database is a single file, because a SQLite database is a single file and that contract is sacred. The file opens with a 168-byte manifest header, followed by chunk data, a chunk index, and an append-only log of commit batches. Copy the file, mail it to a friend, and they have your database with all its history. A lock file appears next to it while connections are open, same as SQLite’s journal files, and garbage collection folds everything back into the one file.

Durability#

Dolt gets its ACID guarantees from the chunk journal. New chunks append to a journal file continuously. A commit is durable when its journal record lands. Garbage collection later migrates journal contents into table files.

DoltLite appends commit batches directly to the tail of the database file. Each batch is chunk records followed by a manifest record sealed with a BLAKE3 self-hash, aligned to sector boundaries on non-atomic media. Recovery walks the tail, adopts the last sealed manifest, and truncates any torn garbage past it. Different plumbing, same shape. Both engines are append-only at commit time. Both crash-recover by scanning for the last good record. Both settle debts at GC time.

Concurrency#

This row is the two engines’ parentage showing. Dolt inherits the MySQL model. One dolt sql-server process owns the database directory and any number of clients connect to it. Concurrency control lives inside the server.

DoltLite inherits the SQLite model. Any number of processes open the same file directly. A write transaction takes an exclusive file lock for its duration. Everyone else gets SQLITE_BUSY and retries. Readers keep a pinned snapshot for the length of their transaction. The snapshot even survives a concurrent garbage collection replacing the file, thanks to a POSIX party trick. The reader’s file descriptor keeps the old inode alive. Multi-process coordination on a shared file is the hard mode of concurrency, and it’s where most of DoltLite’s gnarliest bugs have lived.

Garbage Collection#

Version-controlled databases generate garbage. Rolled-back transactions, deleted branches, and superseded working sets all leave dead chunks behind. Both engines need GC.

Dolt’s is automatic, online, and generational. It runs in the background while the server serves traffic. It only revisits old generations when you ask for dolt gc --full.

DoltLite’s is simpler and blunter. Mark everything reachable from refs and open sessions. Rewrite the live chunks into a fresh file. Atomically rename it over the original. You trigger it with plain VACUUM, the primitive SQLite users already know, or its DoltLite-flavored alias SELECT dolt_gc(). It’s stop-the-world, but the world is a single file on a local disk, so the stop is short.

Remotes#

Dolt has had the full Git-style remote lineup for years. Clone, push, pull, and fetch work against DoltHub, S3, GCS, OCI, or a directory on disk.

When DoltLite launched, it was single-player. Not anymore. dolt_clone(), dolt_push(), and dolt_pull() now work over HTTP, and DoltLite ships a built-in remote server to be the other end. It’s the newest storage-adjacent code in the project, so calibrate expectations accordingly, but the single-player caveat is officially retired.

Maturity#

Dolt hit 1.0 in 2023 and has run in production at real companies for years. The storage format is stable and forward-compatible, and we benchmark and test it against that promise continuously.

DoltLite is alpha and moving fast, with over a thousand merged PRs in under four months of existence. The storage format can and will break between versions. That trade is the whole point. DoltLite’s storage engine gets to learn from a decade of Dolt and Noms lessons and skip straight to the good designs. But it hasn’t earned the years of scar tissue yet. It’s earning them at a rate of about twelve PRs a day.

Same Blueprint, Different Buildings#

If you remember one thing from this article, make it this. The dimensions that define what these databases are are the same: Prolly Trees, content addressing, 4KB Weibull chunks. The dimensions that define how they live are completely different: language, file layout, concurrency, garbage collection. The math is portable. The engineering around the math is shaped by what each database wants to be. Dolt is a server you run for your team. DoltLite is a file you embed in your app.

Want a versioned MySQL? Dolt. Want a versioned SQLite? DoltLite. Want to argue about chunk boundaries? Come by our Discord.