嵌套数组适合小数据量低频更新场景,如20,000种商品每月最多100次调价;直接存pricehistory数组最轻量,$arrayElemAt可快速取最新/上次价格,十年仅约600KB,安全可控。

用嵌套数组存价格历史,适合小数据量低频更新
20,000种商品、每月最多100次价格变动——这种场景下,把 pricehistory 直接作为数组嵌在商品文档里,是最轻量、查询最直接的方案。MongoDB 的 $arrayElemAt 和聚合管道能快速取最新/上一次价格,不用连表或跨集合查。
- 每次更新只改
currentprice字段 +push新记录到pricehistory数组,原子性强 - 查询“当前价 vs 上次价”只需一条聚合:
{$project: {currentprice: {$arrayElemAt: ['$pricehistory', -1]}, previousprice: {$arrayElemAt: ['$pricehistory', -2]}}} - 注意数组无限增长:如果未来价格变动变频繁(比如日更),单文档可能超 16MB 限制;目前每月100次、按每条记录 50 字节算,十年才约 600KB,完全安全
- 别用
$addToSet——价格可能重复,且时间戳才是唯一性依据;必须用$push保证时序
用独立 history 集合做克隆,适合要审计、回滚或高写入吞吐
当需要完整保留每次修改前后的快照(比如法律合规要求“谁在什么时间把价格从 X 改成 Y”),或者商品本身字段多、更新频繁,就该把历史版本抽成独立集合。这不是简单存变更字段,而是整个文档的克隆 + 元信息。
- 历史文档结构建议包含:
product_id(索引)、version(自增或时间戳)、snapshot(深拷贝原商品文档)、updated_by、updated_at - 写操作分两步:先
updateOne更新主文档,再insertOne插入克隆快照;用事务包裹(4.0+)保证原子性,否则可能丢历史 - 避免在克隆时漏掉字段:别手写字段列表,用
JSON.stringify(doc)再解析(Node.js)或 PyMongo 的copy.deepcopy(),防止引用污染 - 查询某个时间点的商品状态?用
find({product_id: 'p1', updated_at: {$lte: ISODate('2025-06-15')} }).sort({updated_at: -1}).limit(1)
Change Stream 不是历史存储,只是实时通知通道
有人想靠 MongoDB 的 changeStream 自动攒出历史库,这容易踩坑:它不保证持久、不重放、断连后丢失事件。它只该用作触发器,不是存储层。
-
changeStream默认只监听最近 oplog 范围内的变更(通常几分钟到几小时),老数据查不到 - 如果下游服务挂了5分钟,这期间的价格更新就彻底丢失,无法补全
- 正确用法:监听
update操作 → 提取fullDocument和updateDescription→ 写入你自己的 history 集合或发到 Kafka - 务必在监听启动时加
{fullDocument: 'updateLookup'},否则fullDocument是 null;同时处理replace类型(整文档替换)和update类型(局部更新)
别忽略时间精度和时区陷阱
价格变动的时间戳看着简单,实际最容易出错的是 ISODate 的生成方式和比较逻辑。
- 前端传来的字符串时间(如
"2025-03-10T14:22:00")若没带时区,MongoDB 默认按 UTC 解析;但业务可能要求按本地时区归档,结果查询“今天的价格”总差8小时 - 统一用
new Date()(JS)或datetime.utcnow()(Python)生成 UTC 时间戳,存进数据库;展示时再转本地时区 - 聚合中用
$dateToString格式化时间时,别漏timezone参数,否则{$dateToString: {format: '%Y-%m-%d', date: '$updated_at'}}默认输出 UTC 日 - 查“过去7天所有调价”?用
{$gte: {$dateSubtract: {startDate: '$$NOW', unit: 'day', amount: 7}}},别用字符串拼日期,易出错










