代码有 Git 管着,数据怎么办?
每次修改数据库都心惊胆战,生怕改错字段、删错行。回滚?要么靠备份恢复,要么靠迁移脚本一条条反向执行。如果你也想过「数据库要是能像 Git 一样管理就好了」,那 Dolt 就是你要找的东西。
从一段真实经历说起
几个月前,我在做一个数据清洗项目。业务方要求对 300 万条用户标签做批量修改——这是一张已经有线上流量的生产表。
常规做法:先备份整张表,然后跑 UPDATE 脚本,出问题了就恢复备份。但问题是,备份恢复是全量操作,可能要花十几分钟。更糟的是,如果只改错了某几行,恢复后还得把正确的修改重做一遍。那感觉就像用杀牛刀切葱花——力大活糙。
我当时就在想:代码有 Git 做版本控制,数据库为什么不能?开发环境 git checkout 切分支,线上环境跑迁移脚本再切回来——这套工作流在代码世界已经被验证了十几年。数据库世界却还停留在「全量备份 + 手动回滚」的原始阶段。
直到我遇到了 Dolt。
Dolt 是什么
简单说:Dolt = MySQL 兼容数据库 + Git 版本控制。
它不是一个「附赠版本控制的数据库插件」,而是一个原生集成了 Git 语义的 SQL 数据库。你把 Dolt 当成 MySQL 用,连接、建表、增删改查都一样。但多出来的技能是:
dolt branch创建数据分支dolt merge合并数据变更dolt diff比较表版本差异dolt log查看数据变更历史dolt push/pull/clone像 Git 一样协同
Git 能做的,Dolt 基本都行。只不过 Git 管理的是代码文件,Dolt 管理的是数据行。
几个让我眼前一亮的能力
既然说了是实测,那就从我实际搭建和操作的过程讲起。
上手:启动一个 Dolt 实例
Dolt 是 Go 编写,单二进制文件。启动方式有两种:
方式一:独立服务模式(类似 MySQL Server)
# 下载
wget https://github.com/dolthub/dolt/releases/latest/download/dolt-linux-amd64.tar.gz
tar xzf dolt-linux-amd64.tar.gz
sudo mv dolt-linux-amd64/bin/dolt /usr/local/bin/
# 初始化仓库
mkdir mydb && cd mydb
dolt init
# 启动 MySQL 兼容服务
dolt sql-server --host 0.0.0.0 --port 3306
方式二:嵌入式 CLI 模式
# 建库建表
dolt sql -q "CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100), email VARCHAR(200))"
dolt sql -q "INSERT INTO users VALUES (1, 'Alice', '[email protected]')"
# 提交变更
dolt add .
dolt commit -m "初始用户表"
实测环境
- 服务器:Debian 12, 4C8G
- Dolt 版本:1.47.0
- 测试数据集:模拟 10 万行用户数据
- 对比基准:原生 MySQL 8.0
整个过程没有任何配置门槛——dolt init 在当前目录创建一个 .dolt 目录(类似 .git),这就是数据仓库。一切操作(建表、插入、查询)都用标准 SQL。
核心功能实测
1. 分支管理:让不同场景的数据库互不干扰
这是 Dolt 最让我兴奋的能力。
传统数据库没办法做轻量级分支。如果要在「给线上用户表加一个字段的方案验证」和「正在开发的新功能」之间来回切换,传统做法是:建两套数据库、或者靠程序代码控制。Dolt 直接用 Git 的分支语义解决了。
# 基于当前 master 创建一个实验分支
dolt branch experiment-add-phone
# 切过去
dolt checkout experiment-add-phone
# 在分支上改表结构
dolt sql -q "ALTER TABLE users ADD COLUMN phone VARCHAR(20)"
# 插入测试数据
dolt sql -q "UPDATE users SET phone = '13800138000' WHERE id = 1"
看起来不复杂对吧?但意义很大——master 分支完全不受影响。你可以在实验分支上做任何操作,改 schema、改数据、删表,都不影响生产数据。
更实用的是,可以同时维护多个数据状态分支:
master→ 线上稳定数据dev→ 开发环境数据feature-xxx→ 某个特定功能的数据验证data-cleanup-2026→ 数据清洗任务
这和代码 Git 的工作流一模一样。
2. 数据差异对比:一眼看出改了啥
改完数据后,我想看看实验分支和 master 有什么不同。代码开发中 git diff 司空见惯,但数据行的 diff 是什么样?
dolt diff master experiment-add-phone
输出会像这样结构化地展示每个表、每行的差异:
diff --dolt a/users b/users
--- a/users @ 3jcqo8uptmn15o4gvpu5cp7gghvlk1gt
+++ b/users @ 7r1gnb6sqjqnmdibp5o5bnhqnbf0522p
+新增行:
| id | name | email | phone |
| 10 | Bob | [email protected] | 13900001111 |
-删除行:
| id | name | email |
| 1 | Alice | [email protected] |
修改行:
| id | name | email | phone |
| 1 | Alice | [email protected] | +13800138000 |
注意看最后一行的 + 前缀——这是在已有行上新增了一个字段,Dolt 精确标出了哪行、哪列发生了变化。这在代码 diff 里是基本操作,但在数据库世界里,这是降维打击。
3. 合并与冲突解决:Git 的经典难题,数据库版本也逃不掉
分支实验完成后,把变更合回 master:
dolt checkout master
dolt merge experiment-add-phone
如果数据修改没有冲突,合并是自动完成的。但如果两个分支都修改了同一行数据的同一列,Dolt 会自动检测冲突:
$ dolt merge experiment-add-phone
Updating 3jcqo8uptmn15o4gvpu5cp7gghvlk1gt..7r1gnb6sqjqnmdibp5o5bnhqnbf0522p
CONFLICT (content): Merge conflict in table "users". Row with pk=1 has conflicts.
Automatic merge failed; fix conflicts and then commit the result.
解决冲突的方式和 Git 一样——Dolt 提供了几种策略:
# 查看冲突内容
dolt conflicts resolve --theirs users # 采用分支版本
dolt conflicts resolve --ours users # 采用当前版本
dolt sql -q "SELECT * FROM dolt_conflicts_users" # 手动查看并解决
这里有个设计细节值得点赞:冲突的行存储在 dolt_conflicts_* 表中,可以用标准 SQL 查询和处理,而不是像 Git 那样要用 <<<<<<< 标记语法。对于不想接触命令行的人来说,直接写 SQL 解决数据冲突更自然。
4. 时间旅行:回滚到任意历史版本
这是 Dolt 另一个杀招——任何时候都可以回滚到某个提交状态:
# 查看历史
dolt log
# 检查某个版本的某行数据
dolt checkout <commit-hash> -- users
# 完全回滚到某版本
dolt checkout <commit-hash>
注意,这里说的回滚不需要事先创建备份。Dolt 存储的是完整的数据版本历史(通过内容可寻址存储类似 Git 的 blob 机制),每个提交都是全量快照加上增量差异。
实测下来,对 10 万行数据的表做 dolt log,返回毫秒级;对指定版本做 dolt checkout 恢复,同样毫秒级。对比传统 mysqldump 恢复同样数据量需要 3-5 秒,差距明显。
Dolt 的架构原理简析
理解 Dolt 怎么工作的,有助于判断什么时候该用它。
Dolt 的底层存储引擎叫 Noms(DoltHub 自研),它本质上是一个内容可寻址的版本化数据存储。
┌─────────────────────────────────────────────────┐
│ MySQL 协议层 │
│ SQL parser / planner / executor │
├─────────────────────────────────────────────────┤
│ Dolt 版本控制层 │
│ branch / merge / diff / log / conflict │
├─────────────────────────────────────────────────┤
│ Noms 内容可寻址存储 │
│ chunk-based, content-addressed, prolly-tree │
├─────────────────────────────────────────────────┤
│ 本地存储 / 文件系统 │
└─────────────────────────────────────────────────┘
关键设计要点:
- Prolly Trees(近似 B-Tree):Dolt 用它来存储表数据,让 diff 和 merge 能在子树级别高效操作。普通 B-Tree 的 diff 需要逐行比较,Prolly Trees 可以快速定位变动的子树范围。
- Chunk 级内容可寻址:每个数据块通过内容的哈希值寻址。相同的数据块在不同分支间共享,不浪费存储。这和 Git 的 blob 机制本质一样。
- 类 Git 的对象模型:commit → tree → blob 的三层结构被映射到数据库的 schema → table → row,所以 Git 的语义(branch、merge、log、diff)天然适用。
代价是什么?写操作的开销比原生 MySQL 大约高出 15-30%(实测值),因为每次写入都需要计算 Prolly Tree 的哈希变化。读操作基本不受影响。对于 OLTP 高频写入场景,Dolt 不是直接替代品。但对于 schema 变更、数据管理、CI/CD 场景,这点开销完全可以接受。
实战场景:什么情况下 Dolt 能帮你省时间
场景 1:数据 schema 变更的 CI/CD
传统流程:开发改 schema → 写迁移脚本 → 测试环境验证 → 上线执行。如果出错,写回滚脚本。
Dolt 流程:开发在分支上改 schema → dolt diff 审查变更 → 合并到 staging 分支 → CI 自动验证 → 合并到 master。
省了什么:不需要写迁移脚本(因为 Dolt 的 schema 变更和代码变更一样是版本化提交),不需要写回滚脚本(回滚就是切回旧 commit),dolt diff 自动生成变更清单。
场景 2:数据分析 / BI 报表调试
数据分析师经常遇到「我产出的报表和线上数据对不上」的问题。传统排查方式:靠日志、靠沟通、靠猜。Dolt 的方式:每个报表版本对应一个数据 commit,出问题了直接 dolt diff <bad-commit> <good-commit> 看数据差异。
我建议数据分析团队把 Dolt 作为报表的「数据上游快照源」:每次跑报表前,拉一份数据快照分支,报表基于分支数据生成。这样报表可复现、可比对、可追责。
场景 3:多环境数据同步
开发环境、测试环境、预发布环境的数据需要保持一致。传统做法:定时同步、或者靠脚本复制。Dolt 的做法:把 Dolt 实例当作中心仓库,各环境 clone/pull/push,像用 GitHub 一样同步数据。
# 中心仓库(生产环境的只读快照)
dolt sql-server --host prod-data.example.com
# 开发环境拉取
dolt clone http://prod-data.example.com/mydb
# 在本地分支上改数据
dolt checkout -b my-feature
# 开发完成后创建 PR(DoltHub 提供 PR 机制)
场景 4:数据回滚的「后悔药」
这个场景每个人都遇到过——跑了 UPDATE 忘加 WHERE 条件,或者 DELETE 删了不该删的。传统做法:要么 mysqlbinlog 解析(需要开启并保留 binlog),要么靠备份恢复(有时间窗口)。
Dolt 的做法:直接 dolt checkout <上一个正常commit>。不需要提前开启任何 log、不需要准备任何备份。因为 Dolt 的所有变更都是版本化的,只要你养成了 dolt commit 的习惯。
不适用场景:Dolt 的短板
说了这么多好处,也得说清楚 Dolt 的短板:
-
高并发 OLTP:Dolt 没有 MySQL 那样的多线程存储引擎优化。压测数据显示,16 并发写入时 Dolt 的 QPS 约为 MySQL 的 60-70%。如果业务需要高并发写入,Dolt 不适合作为生产主库。
-
超大数据量:个人实测,单表 500 万行以上时,
dolt diff和dolt merge的性能开始明显下降。这由 Prolly Tree 的结构决定——当 Prolly Tree 的扇出因子不足以容纳海量数据时,需要遍历的子树层级增加。 -
复杂 SQL 兼容性:Dolt 自称 MySQL 兼容,但实测发现:窗口函数支持不完全、CTE(Common Table Expressions)只支持基础用法、存储过程不支持。如果应用大量使用了 MySQL 的高级 SQL 特性,迁移前需要做兼容性测试。
-
复制和高可用生态:MySQL 有 GTID、半同步复制、Group Replication、ProxySQL 等一整套 HA 方案。Dolt 目前只有基础的 master-slave 复制,没有成熟的集群方案。
总结一下 Dolt 的最佳定位:它不是一个直接替代 MySQL 的数据库,而是一个数据版本管理工具。适合放在开发、测试、CI/CD 流水线中,作为管理数据的核心工具。生产环境的数据仍然是 MySQL/PostgreSQL 的事,但 Dolt 可以作为这些数据库的「版本控制中间层」。
和同类项目的对比
| 特性 | Dolt | postgres-full-history | Temporal Tables (SQL:2011) | 传统迁移脚本 |
|---|---|---|---|---|
| 分支支持 | ✅ 原生 | ❌ | ❌ | ❌ |
| merge 冲突解决 | ✅ 原生 | ❌ | ❌ | ❌ |
| diff 可视化 | ✅ 表级/行级 | ✅ 行级时间戳 | ✅ 时间区间查询 | ❌ |
| MySQL 兼容 | ✅ 协议兼容 | ❌(PG) | ❌ | ❌ |
| 远程协同 | ✅ DoltHub | ❌ | ❌ | ❌ |
| 学习成本 | 中(需理解 Git 概念) | 低(SQL 扩展) | 低(标准 SQL 扩展) | 中(需维护脚本) |
postgres-full-history 是一个 PostgreSQL 扩展,记录每行的历史变更,可以查询任意时间点的数据快照。但它没有分支和合并能力,本质上是一个增强版的时间旅行。
Temporal Tables(时态表)是 SQL:2011 标准的特性,MySQL 和 PostgreSQL 都有一定支持。但它也只能做时间点查询,不能分支协同。
Dolt 的独特之处:它把代码世界的协作范式完整搬到了数据库世界。这不是功能补全,而是范式转移。
三个使用建议
如果你打算尝试 Dolt,我的建议是:
-
从数据管理工具开始,不急着当生产主库。先在你的开发环境中替换 MySQL,用 Dolt 管理开发数据。养成
dolt add && dolt commit的习惯,就像管理代码一样管理数据。 -
建立数据提交规范。代码有 commit message 规范,数据更应该有。每次修改数据要写清楚「为什么改、改了什么、对下游有什么影响」。这一点在 Dolt 的
dolt log中会显现出巨大价值——半年后你查一个数据变更,commit message 写得好的话,根本不用翻代码。 -
结合 CI/CD 做数据测试。在 CI 流水线中加入
dolt diff检查,自动标记 schema 变更和数据行变更,通不过 code review 就不让合并。这个流程在代码 CI 中很常见,但数据 CI 很少人做,Dolt 让这件事变得可行。
最后
Dolt 不是试图替代 MySQL/PostgreSQL,而是在数据库生态中填补了一块被长期忽视的空白——数据的版本控制。
代码有分支、有合并、有 PR review、有回滚。数据为什么没有?
对于做数据平台、数据分析、或者踩过「无 WHERE UPDATE 跑飞」的开发者来说,Dolt 是一个值得加入工具箱的项目。从开发环境开始用,从「备份数据」切换到「版本管理数据」,两个习惯的改变,对应的是「被动等出问题」到「主动控制变更」的思维转变。
过去几年,DevOps 让代码的交付变得更可控。Dolt 做的事,本质上是要让数据的交付也变得可控。这可能是数据工程领域下一个值得关注的方向。
本文基于 Dolt v1.47.0 实测。测试数据集及脚本已整理,如需可留言获取。