「MongoDB 是不是比 MySQL 先进?」这个问题我被问过很多次。先把结论放前面:它不是 MySQL 的升级版,是另一种数据模型。选错了,后面每加一个字段都要开会;选对了,一个文档就能替掉三张表。
这篇不吹概念,用一份电商订单数据把两种建模方式摆在一起,再讲清楚哪五个边界上 MongoDB 更顺手、哪些场景千万别碰,最后给一份上云落地清单。
一、先看数据形状:一份订单的两种存法
一笔订单长这样:订单号、用户、金额、状态、三个商品(每个带 SKU、单价、数量、规格快照)、一条收货地址、两条状态变更记录。
关系型建模:orders 主表 + order_items 子表 + order_status_log 流水表 + addresses 地址表,查一次完整订单要 4 表 join。
文档型建模:一个 orders 集合,商品、地址、状态变更全部内嵌成数组:
db.orders.insertOne({
orderNo: "SO202610180001",
userId: "u_88123",
amount: NumberDecimal("428.00"),
status: "PAID",
items: [
{
sku: "SKU-A1", price: NumberDecimal("199.00"), qty: 1, spec: {
color: "黑", size: "M" } },
{
sku: "SKU-B7", price: NumberDecimal("129.00"), qty: 1, spec: {
color: "白", size: "L" } },
{
sku: "SKU-C3", price: NumberDecimal("100.00"), qty: 1, spec: {
color: "灰", size: "S" } }
],
address: {
province: "浙江", city: "杭州", detail: "文三路 xxx 号", receiver: "张**" },
statusLog: [
{
from: "CREATED", to: "PAID", at: new Date("2026-10-18T10:01:00Z") },
{
from: "PAID", to: "SHIPPED", at: new Date("2026-10-18T15:20:00Z") }
],
createdAt: new Date()
})
一次 findOne({orderNo: "SO202610180001"}) 拿到整单,没有 join,没有 N+1。这就是文档库真正值钱的地方:读路径和业务对象天然同构。
代价也要说清楚:内嵌数组不是无限的。单个 BSON 文档硬上限 16MB,超过就得拆集合或用 GridFS 存大对象;数组元素持续增长(比如评论、日志)会导致文档不断变大、频繁迁移,这类"无界增长"的数组应该拆成独立集合。
二、五个边界:MongoDB 更顺手的场景
| 边界 | MongoDB 占优的原因 | 典型业务 |
|---|---|---|
| 结构随业务变 | 无固定 schema,加字段不用 DDL、不停机 | 商品属性、活动配置、表单收集 |
| 一对多且总是整体读 | 内嵌文档一次取回,省掉 join | 订单、购物车、问卷答卷 |
| 半结构化 / 嵌套深 | 原生支持数组与嵌套对象、多级查询 | 埋点、设备上报、内容详情 |
| 写多读少、水平扩展 | 分片集群按分片键水平扩,理论扩容无上限 | 日志、消息、IoT 时序 |
| 需要地理位置查询 | 内置 2dsphere 地理索引 |
附近的门店、骑手调度 |
再补一句容易被忽略的:地理位置查询在 MySQL 里要靠 POINT + 空间索引绕一大圈,MongoDB 直接建索引就能算距离,这是硬差距。
反过来,这五类需求 MySQL 更稳:强约束的多表一致性(对账、账务)、复杂报表与多表聚合、需要外键约束保证数据不脏、团队只熟悉 SQL、单表数据量不大但 join 极多。报表类负载不要硬上 MongoDB,聚合管道能写,但维护成本远高于一句 SQL。
三、索引:文档库的性能全在这一步
MongoDB 的索引模型和关系库接近,但有几个专属类型必须会用。以下命令都在 mongosh 里执行:
// 1. 业务唯一键:订单号
db.orders.createIndex({
orderNo: 1 }, {
unique: true })
// 2. 复合索引:按用户 + 时间倒序查"我的订单"
db.orders.createIndex({
userId: 1, createdAt: -1 })
// 3. 多键索引:数组字段自动展开(items.sku 是数组里的字段)
db.orders.createIndex({
"items.sku": 1 })
// 4. 部分索引:只给未完成订单建索引,索引体积能小一个量级
db.orders.createIndex(
{
status: 1, createdAt: -1 },
{
partialFilterExpression: {
status: {
$in: ["CREATED", "PAID"] } } }
)
// 5. TTL 索引:临时数据到期自动删(这里是 30 天)
db.sessions.createIndex({
createdAt: 1 }, {
expireAfterSeconds: 2592000 })
验证是否真的走索引,别凭感觉,用 explain:
db.orders.find({
userId: "u_88123" }).sort({
createdAt: -1 }).explain("executionStats")
关注三个数:stage 是 IXSCAN 还是 COLLSCAN(后者就是全表扫)、totalKeysExamined 与 nReturned 是否接近(差太多说明索引选择性差)、executionTimeMillis。
几条硬规矩:
- 单个集合索引上限 64 个,索引不是越多越好,每个索引都要在写入时维护;
- 一个复合索引里最多包含一个数组字段,两个数组字段并行索引会直接报错;
- 数组字段建的是多键索引,它不能用于覆盖查询(
covered query),因为同一文档会被展开成多条索引项; - 文本索引一个集合只能建一个,多语言检索场景要提前规划字段。
四、事务与一致性:别拿"文档库不支持事务"当结论
这是流传最广的过时说法。MongoDB 4.0 起副本集支持多文档事务,4.2 起分片集群支持分布式事务。写法上和关系库很像:
const session = db.getMongo().startSession()
session.startTransaction({
readConcern: {
level: "snapshot" },
writeConcern: {
w: "majority" }
})
try {
const orders = session.getDatabase("shop").orders
const stock = session.getDatabase("shop").inventory
orders.updateOne({
orderNo: "SO202610180001" }, {
$set: {
status: "PAID" } }, {
session })
stock.updateOne({
sku: "SKU-A1" }, {
$inc: {
available: -1 } }, {
session })
session.commitTransaction()
} catch (e) {
session.abortTransaction()
throw e
} finally {
session.endSession()
}
但用之前先想清楚两件事:
- 单文档操作本身就是原子的,绝大多数订单场景把关联数据内嵌到一个文档里,根本不需要事务。事务是兜底手段,不是默认姿势。
- 事务有开销,长事务会持有锁与快照,官方建议单个事务尽量控制在较短时间、少量文档内;跨分片事务成本更高。能用一次
updateOne带$inc解决的,别开事务。
写关注(writeConcern)也要按业务定:w: "majority" 保证多数节点落盘后才返回,安全但有延迟;w: 1 快,主节点挂掉时极端情况可能回滚。资金类写必须 majority。
五、上阿里云:三种架构怎么挑
阿里云云数据库 MongoDB 版提供单节点、副本集、分片集群三种形态,选择逻辑其实很清晰:
| 架构 | 节点构成 | 适用 | 注意 |
|---|---|---|---|
| 单节点 | 单实例 | 测试环境、学习、内部非核心系统 | 无高可用,节点故障即不可用 |
| 副本集 | 1 Primary + 多个 Secondary + 1 Hidden + 可选 ReadOnly | 读多写少、突发活动业务(绝大多数业务系统) | Primary 故障自动选举;Hidden 不计入服务,Secondary 不可用时由它接管 |
| 分片集群 | Mongos 代理 + Shard + ConfigServer(后两者均三节点副本集) | 高并发读写、单机容量到顶 | 单分片最大 3TB / 72000 IOPS,官方标称单分片可承载约 15 万 QPS(读写比 95:5) |
几点实测经验:
- 副本集的读能力可以横向加,加 ReadOnly 节点承接报表、导出这类重读请求,别让它们压 Primary;
- 分片键必须在建库前想好。要满足高基数、分布均匀、查询常用三个条件;用单调递增的
_id或时间戳做分片键会形成写热点,全部打到最后一个分片。老版本分片键不可改,新版本虽然支持重分片,但那是停机窗口级别的大动作; - 分片集群的唯一索引必须包含分片键,这条限制会直接影响索引设计,建表前先确认。
备份这块省心:默认每天自动备份,免费支持 7 天内任意时间点恢复,备份文件存在 OSS,还提供最多 2 倍于存储空间的免费备份空间。误删数据这种事故,时间点恢复比什么预案都实在。
迁移上云走 DTS,注意三个前置条件:源库 Oplog 至少保留 7 天(否则增量追不上)、单条数据不超过 16MB(超了任务直接失败)、目标是分片集群时要先清孤立文档并预分片,否则数据全挤在一个分片里,迁移完性能还不如自建。
连接串大致长这样,副本集一定要带 replicaSet 参数,否则驱动无法做主从切换:
mongodb://<user>:<password>@<主节点域名>:<端口>,<从节点域名>:<端口>/<database>?replicaSet=<副本集名>&authSource=admin
白名单最多支持 1000 条规则,生产环境请按网段收敛,不要图省事填 0.0.0.0/0。
六、什么时候不要选 MongoDB
- 业务是账务、对账、库存扣减这类强事务强约束场景,团队又没有文档建模经验;
- 主要负载是多表关联报表,BI 直接连库跑 SQL;
- 数据模型非常稳定、字段几乎不变,关系库的约束能帮你挡住大量脏数据;
- 团队没有人会看
explain、不会设计索引——文档库对索引设计的要求只会更高,不会更低。
七、一句话判断
数据结构像"一张表单",选中 MongoDB;数据结构像"一张网",选 MySQL。 订单、内容、埋点、设备上报是表单;账务、权限、审批流是网。
新用户首次购买包年包月实例有优惠(指定规格 699 元 / 3 个月,或全系规格 6 折 / 年,限一次),具体规格与价格请以官方页面实时展示为准。真正决定成本的不是首单价,而是你有没有为这份数据选对模型——选错了,后面每一次改结构都在还债。