「MongoDB 不需要建表,加字段直接存就行」——这句话是 MongoDB 最出圈的卖点,也是最容易被误解的一条。它听起来像是「结构可以随便长」,但真实情况是:schemaless 不是「没有结构」,而是「结构被推到了应用层」。数据库不再替你把关字段类型和必填,这份责任全转给了代码。
短期看这是优势,业务迭代快、不用跑 DDL;长期看它是隐性债。一个跑了三年的集合,往往同时存在三代甚至更多代结构的文档:最老的一批没有 createdAt,中间一批 status 是字符串,最新一批 status 是枚举。这种「结构漂移」会让查询、索引和数据治理越来越痛。这一篇讲清楚模式演进的真实代价,以及怎么在「灵活」和「可控」之间找到平衡。
先把机制边界说清楚
MongoDB 对文档结构的态度,经历了一个明显的设计转向:
- 早期(3.x 以前):完全 schemaless,数据库不校验任何结构。
- 3.6+:引入
$jsonSchema文档校验,可以在集合上定义结构约束。 - 5.0+:校验能力增强,支持更复杂的约束和更细的控制级别。
所以「MongoDB 无 schema」这个说法,在今天已经不准确了。准确的说法是:MongoDB 默认 schemaless,但你可以选择性地启用校验。灵活和治理不是二选一,而是一根光谱——你可以在不同集合上选不同的严格程度。
模式演进的真实代价
一个集合随业务演进,自然会出现多代结构并存。这本身不致命,致命的是「没有约定,各写各的」。
第一个代价是查询复杂度上升。 老 status 是字符串、新 status 是数字,查询要写 {$or: [{status: "active"}, {status: 1}]}。老文档没有 createdAt,按时间排序要处理空值。这些兼容代码会越积越多,成为应用层的「结构债」。
第二个代价是索引选择性下降。 同一个字段在不同代文档里类型不同,索引可能只覆盖一部分文档。比如 status 字段,老文档是字符串、新文档是数字,建在 status 上的索引可能只对数字部分生效,字符串文档走不到索引。
第三个代价是脏数据渗入。 没有校验时,一个笔误(CreatdAt 拼错、amout 拼错、status: "actve")就会被静默存进去,从此这个文档的结构就和正常文档不一样。这类脏数据排查起来极痛苦,因为它们「看起来正常」,只在特定查询时才暴露。
真正的开销在「批量改结构」
很多人以为 MongoDB 改结构「零成本」,这是只看了加字段的场景。加字段确实便宜——新文档直接带上新字段,老文档查的时候没有就给默认值。但下面这类结构变更,代价和关系库的 DDL 是同一个量级:
- 改字段类型:把
status从字符串改成数字,要把全集合扫一遍重写。没有 schema migration 工具时,得自己写脚本。 - 拆字段:把
fullName拆成firstName/lastName,同样要全集合更新。 - 批量补字段:给老文档批量补
createdAt,要updateMany,大集合上是个大操作。
这些操作在千万级文档上可能跑几十分钟到几小时,期间要错峰、要监控锁、要考虑复制集压力。所以「MongoDB 改结构免费」是个危险的错觉——加字段免费,改字段和删字段都不免费。
用 $jsonSchema 把灵活收敛成可控
MongoDB 的校验机制(validator + $jsonSchema)是治理结构漂移的正解。它的工作方式是在集合上挂一个 JSON Schema,新写入按 schema 校验,不满足就拒绝。
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email", "createdAt"],
properties: {
name: { bsonType: "string" },
email: { bsonType: "string" },
status: { enum: ["active", "inactive", "banned"] },
createdAt: { bsonType: "date" },
tags: { bsonType: "array" }
}
}
}
})
关键的两个控制旋钮:
validationLevel:strict(校验所有写入,含更新)、moderate(只校验新增文档和满足既有约束的更新,老文档不强制)、off。生产环境推荐moderate,既能约束新数据,又不会因为老文档不合规而阻塞更新。validationAction:error(不合规直接拒绝)、warn(只记日志不拒绝)。先用warn观察一段时间,确认没有误伤,再切成error。
这套机制让 MongoDB 既能保留「快速加字段」的灵活性,又能在关键字段上加上护栏。它的设计哲学是「渐进收紧」:从 schemaless 起步,随着业务稳定,逐步给核心集合加校验,而不是一开始就锁死。
惰性迁移:让集合在演进中始终可用
处理老文档的标准姿势是惰性迁移(lazy migration),而不是一次性全表重写:
- 读时给默认值:应用读到老文档的
createdAt为空,就当作某个默认时间处理。新写入的文档一定带createdAt。 - 写时补字段:老文档被更新时,顺手补上新字段。这样不用专门跑迁移脚本,数据在正常读写中慢慢趋同。
- 必要时批量补:如果某个字段必须全局存在(比如要做索引),再跑
updateMany批量补,但要错峰、分批、监控。
惰性迁移的好处是集合在演进过程中始终可读可写,不会因为「正在迁移」而停服。它的代价是过渡期内结构不统一,应用要能容忍这种不一致。
判断框架
- 核心业务集合:上线校验(
moderate+warn观察后切error),把结构债挡在写入端。 - 实验性、快速迭代的集合:可以暂时 schemaless,但要明确「什么时候收紧」的触发点(比如上线生产、数据量过万)。
- 加字段:零成本,直接加,应用读时给默认值。
- 改字段类型 / 拆字段:不免费,按 DDL 对待,要迁移脚本和错峰。
- 老文档:优先惰性迁移,必须全局一致时再批量补。
- 枚举字段:用
$jsonSchema的enum约束,防止笔误渗入。 - 任何「这个字段以后会不会变」的疑问,提前想好兼容策略,别等结构漂移了再补救。
无 schema 的真正含义,是「结构治理的责任从数据库转移到了团队」。用得好,它是快速迭代的加速器;用得放任,它就是三年后那笔最难还的技术债。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。
我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。
如果你想继续跟完这套「图解 MongoDB」,欢迎关注公众号 「十三Tech」。后续会按文档模型、索引优化、存储引擎、高可用和分片集群这条线更新。

