不推荐按天建独立集合,因其会显著增加应用层复杂度:需手动处理路由、索引同步、备份恢复、跨日查询及权限管理;且驱动和ORM不支持动态集合名,易出错;MongoDB原生shard key与TTL索引已可支撑亿级消息场景。

单集合 + 复合索引 + TTL 是当前最稳妥的方案,别折腾按天分桶。
为什么不要按日期建多个 messages_20240520 这类集合?
手动分集合表面看能控制单集合大小,实际会把问题甩给应用层:路由逻辑要自己写、每个集合都要单独建索引、备份恢复得遍历所有集合、跨日查询得 union all 或多次请求、权限管理也得按集合粒度配——这些全都是硬伤。
更关键的是:mongosh 和主流驱动(如 pymongo、mongoose)都不支持动态集合名的批量操作;ORM 映射一碰到 messages_${date} 就容易崩。
真正该依赖的是 MongoDB 原生能力:shard key 分片 + TTL index 自动过期,亿级消息已验证可行。
游标分页必须用 created_at 和 _id 复合条件
聊天场景高并发写入,skip/limit 分页必然漏数据或重复——新消息插入时 offset 会漂移;只用 created_at 游标也不行,毫秒级时间戳+多线程写入会导致大量重复值,边界难以判定。
实操要点:
- 查询语句必须同时带
created_at和_id边界:db.messages.find({created_at: {$lt: lastMsg.created_at}, _id: {$lt: lastMsg._id}}).sort({created_at: -1, _id: -1}).limit(20) - 对应索引必须严格匹配排序顺序:
db.messages.createIndex({created_at: -1, _id: -1}) - 客户端必须缓存上一页最后一条的
created_at和_id,服务端做双重校验,防时钟不同步或写入延迟导致游标错位
投影字段和 _id 的隐式时间戳价值别浪费
默认查整条文档再过滤字段,网络传输、序列化、内存占用全在放大开销。尤其当消息含大字段(如 base64 图片、长文本)时,性能断崖下跌。
实操建议:
- 查询时强制
projection:{content: 1, sender_id: 1, created_at: 1, _id: 1},其他字段一律不取 -
_id是ObjectId,前 4 字节就是秒级时间戳,升序即时间递增,降序即倒序;它天然去重、全局唯一,比拼接字符串时间戳更可靠 - 避免用
$gte+$lt查“某天全部消息”再分页——这本质是范围扫描,数据量越大越慢
TTL 删除对写入影响有限,但要注意字段类型
TTL 删除由后台线程异步执行,日常不会卡住写入。但有两个硬约束:
-
expireAfterSeconds对应的字段必须是Date类型,不能是字符串或数字时间戳,否则索引不生效 - 索引必须建在该字段上,且不能是复合索引的一部分(TTL 索引必须是单字段)
- 删除不是实时的,延迟通常在 60 秒内,业务需接受这个窗口
真正容易被忽略的是:TTL 索引一旦建错,旧数据不会自动清理,而新数据又持续写入——几个月后磁盘悄悄爆掉,监控却没报警。

















