SKU必须嵌套在商品主文档中,采用数组存储子文档,避免拆分集合;attrs须为扁平对象、字段名固定且值受枚举约束;高频查询字段需单独建索引;库存更新必须使用原子操作如$inc。

SKU文档必须嵌套在商品主文档里,别单独建集合
电商场景下,sku 和 product 是强一致性关系:库存扣减、价格变更、上下架都要求原子操作。如果把 sku 拆成独立集合,一次库存更新就得跨集合事务(MongoDB 4.0+ 才支持),性能差、出错率高,而且分页查商品+SKU时要多次查询或用 $lookup,延迟明显上升。
实操建议:product 文档里用数组字段 skus 存储所有 SKU,每个元素是完整子文档:
{
"_id": "prod_123",
"name": "iPhone 15",
"skus": [
{
"sku_id": "sku_123-a",
"attrs": { "color": "Black", "capacity": "256GB" },
"price": 7999,
"stock": 42,
"status": "in_stock"
},
{
"sku_id": "sku_123-b",
"attrs": { "color": "White", "capacity": "128GB" },
"price": 6999,
"stock": 18,
"status": "in_stock"
}
]
}
- 避免用
attrs数组存键值对(如[{k:"color",v:"Black"}]),查询和索引效率低,且无法用点号语法直接过滤 -
attrs字段必须是扁平对象,不能嵌套层级过深——MongoDB 索引对嵌套超 2 层的字段支持弱,color.capacity可建索引,spec.detail.color就难优化 - 所有 SKU 共享的字段(如
weight、shipping_days)放外层product,不重复存进每个sku子文档
动态属性要用固定字段名 + 预定义枚举值,别用自由字符串当 key
用户可能传 "颜色"、"Color"、"colour" 表达同一属性,后端没做归一化就直接入库,会导致前端筛选失效、聚合统计不准、甚至同款 SKU 被当成不同规格。
实操建议:服务端强制映射为统一英文字段名,并限制取值范围:
- 字段名固定为
color、size、capacity等,不接受运行时任意字符串 - 值必须来自后台配置的枚举表,比如
color只允许"black"、"white"、"blue"(小写+连字符,无空格) - 前端选属性时,拉取的是带 label 的枚举项,但提交给后端的
attrs必须是规范 key-value 对,例如{"color": "black", "size": "m"} - 数据库加校验:用
validator规则约束skus.$.attrs结构,防止非法字段混入
高频查询字段必须单独建索引,别指望靠 _id 或全文索引扛住压力
用户按颜色筛、按价格区间排序、按库存状态过滤——这些操作若没对应索引,集合稍大(>10 万商品)就会慢到超时,explain() 一看全是 COLLSCAN。
关键索引组合要提前想清楚:
-
skus.attrs.color单独建索引:支持「只看黑色款」这种筛选 -
skus.price单独建索引:配合$elemMatch实现「价格在 5000–8000 的所有 SKU」 - 复合索引
{ "skus.attrs.color": 1, "skus.attrs.size": 1, "skus.status": 1 }:覆盖多条件联合筛选场景 - 别建
skus.attrs整体索引——它是个对象,MongoDB 不会自动展开其内部字段,索引无效
注意:数组字段上的索引会产生“索引条目爆炸”,skus 平均含 5 个 SKU,那一条 product 文档会在 skus.price 索引里生成 5 条记录,控制单商品 SKU 数上限(比如 ≤20),否则索引体积和写入开销陡增。
库存更新必须用 findAndModify 或 updateOne 带 $inc,禁止先读再写
并发下单时,“读库存→判断是否充足→扣减→写回”这四步一旦拆开,必然超卖。MongoDB 的原子操作不是可选项,是保命线。
正确姿势只有两种:
- 扣减单个 SKU:
updateOne({ _id: "prod_123", "skus.sku_id": "sku_123-a" }, { $inc: { "skus.$.stock": -1 } }),配合arrayFilters精准定位 - 批量扣多个 SKU(如下单含 3 个不同 SKU):用
findOneAndUpdate+$set+ 多个$[elem]占位符,但前提是所有 SKU 在同一个product文档里——这也是为什么不能拆集合 - 务必检查返回的
matchedCount和modifiedCount:如果为 0,说明库存不足或 SKU 不存在,不是网络错误
容易被忽略的一点:skus.$.stock 更新后,应用层如果还缓存着旧的 product 文档,下次读可能拿到脏数据。要么禁用该文档级缓存,要么更新后主动清缓存,别指望 MongoDB 自动通知。










