6 Lessons From 20 Years of SQL Migration Tools (Applied to a NoSQL Migrator in 2026)

6 Lessons From 20 Years of SQL Migration Tools (Applied to a NoSQL Migrator in 2026)

20 年 SQL 迁移工具经验总结(应用于 2026 年的 NoSQL 迁移器)

Flyway has been around since 2010. Liquibase since 2006. dbmate is newer but built on the same lineage. Between them, that’s decades of hard-won engineering on one narrow problem: how do you change a database schema safely, repeatedly, without losing anyone’s data. Flyway 自 2010 年起就已存在,Liquibase 则始于 2006 年,dbmate 虽然较新,但也建立在相同的血统之上。它们共同代表了数十年来在解决一个狭窄问题上所积累的宝贵工程经验:如何在不丢失任何数据的前提下,安全且可重复地变更数据库模式(Schema)。

Here’s the part that surprised me: none of that engineering ever made it over to NoSQL-to-SQL migrations. Not because it’s secret knowledge — it’s all open source, well documented, battle-tested in production at companies most of us have heard of. It just never got ported. 令我惊讶的是:这些工程经验从未被引入到 NoSQL 到 SQL 的迁移中。这并非因为它们是什么秘密知识——它们全是开源的、文档齐全的,并且在许多我们耳熟能详的公司生产环境中经过了实战检验。它们只是从未被移植过来。

Every NoSQL migration tool I’ve looked at (fireway, mad-migration, a handful of others) was built from scratch, by someone solving their own one-time problem, without borrowing the patterns that already exist one layer over. So before designing anything new, I went and read how the old tools actually work. Here’s what’s worth stealing. 我研究过的每一个 NoSQL 迁移工具(如 fireway、mad-migration 等)都是从零开始构建的,由开发者为了解决自己的一次性问题而编写,没有借鉴上一层已经存在的成熟模式。因此,在设计任何新东西之前,我先去研究了旧工具的工作原理。以下是值得借鉴的经验。

1. Convention over configuration

1. 约定优于配置

Flyway resolves execution order from the filename alone: V2__add_orders_table.sql. No config file, no database of what-runs-when. The number in the name is the order. Applied: every migration step should produce a versioned, predictably-named file, sitting in your own filesystem, reviewable in git — not hidden state living only inside the tool’s memory. Flyway 仅通过文件名来确定执行顺序,例如 V2__add_orders_table.sql。无需配置文件,也无需记录执行状态的数据库。文件名中的数字即代表顺序。应用到实践中:每一个迁移步骤都应生成一个版本化、命名可预测的文件,存放在你自己的文件系统中,并可在 Git 中进行审查,而不是将状态隐藏在工具的内存中。

2. Dirty state as a safety feature, not an annoyance

2. 将“脏状态”视为安全特性,而非干扰

If a Flyway migration fails partway through, it marks the schema as dirty and refuses to run anything else until a human looks at it and confirms it’s safe to continue. The interesting bit isn’t the tracking — it’s the refusal. Most tools I’ve seen err toward “keep trying” when something goes wrong. Flyway’s answer is the opposite: stop, and make a human decide. That’s a better default when the thing you might silently corrupt is someone’s production data. 如果 Flyway 的迁移在中途失败,它会将模式标记为“脏状态”(dirty),并拒绝执行任何后续操作,直到人工介入确认可以安全继续。有趣之处不在于追踪,而在于这种“拒绝”。我见过的大多数工具在出错时倾向于“继续尝试”。Flyway 的做法恰恰相反:停止,并让用户做出决定。当操作对象是可能被静默损坏的生产数据时,这是一个更好的默认策略。

3. Dump the schema after every step

3. 在每一步之后导出 Schema

dbmate writes a plain, readable db/schema.sql after each migration — the full current state, always diffable in git. This sounds almost too simple to be a “lesson,” but it changes what trust looks like. Instead of a log you have to parse, you get a file you can just read. Every step should leave something concrete behind, not just a status message that scrolls off your terminal. dbmate 在每次迁移后都会写入一个简单、可读的 db/schema.sql 文件——记录完整的当前状态,且始终可以在 Git 中进行差异对比。这听起来简单得不像是一个“经验”,但它改变了信任的建立方式。你得到的不再是需要解析的日志,而是一个可以直接阅读的文件。每一步都应该留下具体的东西,而不是仅仅在终端滚动消失的状态消息。

4. Stay a small, boring binary

4. 保持为一个小型、无聊的二进制文件

dbmate is deliberately minimal: one binary, plain SQL, one environment variable for the connection string. No ORM, no plugin system, no framework opinions leaking into your schema. Boring is a feature here. The moment a migration tool needs its own plugin ecosystem to be useful, it’s stopped being a migration tool and started being a platform — a much harder thing to keep maintained for free. dbmate 特意保持极简:一个二进制文件、纯 SQL、一个用于连接字符串的环境变量。没有 ORM,没有插件系统,没有框架的偏见渗透到你的 Schema 中。“无聊”在这里是一种特性。一旦迁移工具需要依赖自己的插件生态系统才能发挥作用,它就不再是迁移工具,而变成了平台——这在免费维护的情况下要困难得多。

5. Rollback is the user’s job, and the tool should say so

5. 回滚是用户的职责,工具应当明确这一点

In Liquibase Community, you write your own rollback scripts. The tool doesn’t invent a magic “undo” button. That’s more honest than it sounds. A generic “undo” is often not actually safe — depending on what happened in between, reversing a migration cleanly can be a genuinely different (and harder) problem than applying it. Pretending otherwise is how people end up trusting a rollback that quietly makes things worse. 在 Liquibase 社区版中,你需要编写自己的回滚脚本。该工具不会发明一个神奇的“撤销”按钮。这比听起来更诚实。通用的“撤销”往往并不真正安全——取决于中间发生了什么,干净地反转一次迁移可能比执行它本身更复杂(也更困难)。假装可以实现通用撤销,只会让人们最终信任一个可能会悄悄让情况变得更糟的回滚操作。

6. Separate the “what changes” from the “how it runs”

6. 将“变更内容”与“执行方式”分离

Liquibase keeps the changelog (SQL, YAML, JSON, XML — your choice) as a distinct artifact from the execution engine. You can read, review, and version the changelog without running anything. For anything involving inferred structure — which is exactly what NoSQL-to-SQL migration needs — this separation matters even more. The proposed schema should be something a human reads and edits calmly, in a file, before anything touches a real database. Not something that flashes by in a CLI prompt and gets approved on reflex. Liquibase 将变更日志(SQL、YAML、JSON、XML 任选)作为独立于执行引擎的制品。你可以在不运行任何东西的情况下阅读、审查和版本化变更日志。对于任何涉及结构推断的任务——这正是 NoSQL 到 SQL 迁移所需要的——这种分离尤为重要。拟议的 Schema 应该是人类在文件里冷静阅读和编辑的内容,而不是在 CLI 提示符中一闪而过并被条件反射式批准的东西。

Why none of this made it to NoSQL migrations before now

为什么这些经验至今未被引入 NoSQL 迁移

Two honest reasons, not one. First: relational schema migration has a stable shape — SQL is a standard, so a tool built for it generalizes to any project using Postgres or MySQL. NoSQL-to-SQL doesn’t have that shape. Firestore, MongoDB, and DynamoDB each model data differently enough that a tool built around one doesn’t transfer cleanly to another, which makes the payoff for polishing one tool much smaller. 有两个诚实的原因。第一:关系型数据库的模式迁移具有稳定的形态——SQL 是标准,因此为它构建的工具可以推广到任何使用 Postgres 或 MySQL 的项目。而 NoSQL 到 SQL 没有这种形态。Firestore、MongoDB 和 DynamoDB 的数据建模方式差异巨大,导致为一个数据库构建的工具无法平滑迁移到另一个,这使得打磨单一工具的收益小得多。

Second, and more interesting: even DBeaver — a database client with 8M+ users and real funding — treats NoSQL support as a paid feature, not something that ships free. That’s a signal worth sitting with: the market for polished relational tooling is large and stable enough to sustain 20 years of open-source maintenance. The market for NoSQL migration tooling, so far, hasn’t been. 第二点更有趣:即使是拥有 800 多万用户且有实际资金支持的数据库客户端 DBeaver,也将 NoSQL 支持视为付费功能,而非免费提供。这是一个值得深思的信号:成熟的关系型数据库工具市场足够大且稳定,足以支撑 20 年的开源维护。而到目前为止,NoSQL 迁移工具市场还做不到这一点。

None of that is a reason to skip these lessons. It’s the reason nobody applied them yet. I’ve been building a Firestore-to-Postgres migrator (Centauri Migrate) using exactly the six patterns above — versioned steps, a dirty-state guard, a readable schema dump after inference, dry-run by default, and a changelog you review before anything runs. Early days, 84 tests, not yet run against a real production dataset — but the architecture isn’t guessing at this part, it’s borrowed from tools that already proved it works. 但这并不是忽略这些经验的理由,这只是为什么至今没人应用它们的原因。我正在构建一个 Firestore 到 Postgres 的迁移器(Centauri Migrate),完全采用了上述六种模式——版本化步骤、脏状态保护、推断后的可读 Schema 导出、默认干运行(dry-run),以及在执行前进行审查的变更日志。目前处于早期阶段,有 84 个测试用例,尚未在真实的生产数据集上运行——但其架构并非凭空猜测,而是借鉴了那些已经证明行之有效的工具。