宽表模型需围绕聚合查询路径反向建模,固化关联字段与预计算逻辑,主键应为事实粒度(如{device_id, hour_ts}),维表字段须嵌入、指标须前置计算,优先使用timeseries集合并确保分片键匹配查询模式。

宽表模型不是“加字段”就能解决的,而是要围绕聚合查询路径反向建模——提前把 $lookup、$unwind、$group 里反复用到的关联字段和计算逻辑,固化进单个文档结构中。
宽表设计必须从聚合管道倒推
很多人先建好原始集合再写聚合,结果发现 $lookup 太慢、$unwind 后数据爆炸、$group 内存溢出。根本原因是没在建模阶段锁定聚合的“事实主键”和“维度字段”。比如统计每台设备每小时的平均温度+告警次数,宽表的主键就该是 {device_id, hour_ts},所有维度(设备型号、所属产线、区域标签)和指标(avg_temp、alert_count、sample_count)都应直接落在这个文档里。
- 避免在聚合时动态
$lookup维表:维表字段(如product_line、region)应在写入宽表时就查好并嵌入 - 拒绝“原始数据+实时计算”思路:像
avg_temp这类指标,应在流式写入阶段就按窗口预计算,而不是留到查询时用$avg算 - 警惕数组膨胀:如果一个设备一小时有上万条原始采样,不要把全部
samples数组塞进宽表;只保留聚合结果 + 1–3 条典型样本用于下钻
时间序列宽表优先用 Time Series Collection
MongoDB 5.0+ 的 timeSeries 集合不是“锦上添花”,而是大数据量时序宽表的基础设施。它底层自动按时间分块、压缩存储、优化范围扫描,比普通集合做 {$match: {ts: {$gte: ..., $lt: ...}}} 快 3–8 倍。
- 创建时必须指定
timeField(如"ts")和可选的metaField(如"device_meta"),后者就是你嵌入的设备维度信息 -
metaField支持通配符索引:db.metrics.createIndex({"device_meta.$**": 1}),能高效支撑按任意嵌套维度过滤 - 不支持在
timeSeries集合上直接$lookup,所以维表数据必须在写入前就合并进metaField或根级字段
嵌入维表数据时注意更新一致性
宽表里嵌入的维表字段(比如 device_name、line_status)一旦源维表变更,宽表不会自动同步——这是最常被忽略的“数据漂移”源头。
- 对低频变更字段(如设备型号、固件版本),可在写入宽表时快照固化,接受最终一致性
- 对高频变更字段(如产线实时状态),需配合 Change Streams 监听维表变更,用
updateMany批量修正宽表中对应device_id的记录 - 绝对不要在宽表里存“逻辑外键”(如只存
line_id而不嵌入line_name),否则又退回了需要$lookup的老路
文档大小与分片键必须协同设计
宽表文档容易超 16MB 限制,尤其当嵌入日志片段或原始样本时。但更隐蔽的问题是分片键选择不当导致聚合倾斜——比如用 device_id 分片,但所有查询都带 date 范围,MongoDB 就无法路由到目标分片,被迫全表扫描。
- 分片键首选复合字段,例如
{date: 1, device_id: 1},确保按日期范围查询时能精准定位分片 - 对写入热点(如新设备集中上线),在
device_id后追加哈希后缀(如device_id_shard),打散写入压力 - 用
db.collection.stats()定期检查avgObjSize和maxSize,一旦接近 8MB 就要预警:要么裁剪嵌入内容,要么拆成“宽表+明细表”双层结构
真正难的不是把字段堆进一个文档,而是判断哪些字段必须冗余、哪些计算必须前置、哪些关联必须切断——这些决策点藏在聚合管道的第一行 $match 条件和最后一行 $project 输出里,而不是 ER 图上。











