应删字段就删,避免留null;null与字段缺失语义不同:前者参与索引、可被find({f: null})匹配,后者需exists: false查询且不入索引;仅当需明确表达“有定义但为空”时才存null,否则用$unset删除字段。

null)和缺失字段在语义、查询行为、索引效率、驱动解析上完全不同,不能混用。
字段为 null 和字段不存在是两回事
MongoDB 严格区分 {"name": null} 和 {}(或 {"age": 25} 这种不含 name 的文档)。这直接影响:
-
find({name: null})只匹配显式设为null的文档,不匹配name字段根本不存在的文档 -
find({name: {$exists: false}})才能查出字段缺失的文档 - 如果字段上有索引,
null值会参与索引条目;缺失字段则完全不入索引(除非用了sparse: true) - Mongoose 默认把未赋值字段设为
undefined,序列化时直接丢弃——但若你手动赋null,它就会被存成null
什么时候该用 null?
仅当业务需要明确表达「该字段有定义,但当前值为空」时才存 null。典型场景:
- 表单中用户主动清空了「公司名称」,你想记录「用户选择不填」而非「尚未填写」
- 状态字段如
archivedAt,null表示「未归档」,而缺失该字段可能意味着「该文档不支持归档」 - 与外部系统对接时,对方要求字段必须存在,哪怕值为
null
否则,一律让字段自然消失——Mongoose 中不赋值、Driver 中不写入该 key 即可。
$unset 是删除字段的正确方式
想移除已有字段,别用 $set: {field: null},那只是改值,不是删字段:
- 错误:
db.users.updateOne({_id: ObjectId("...")}, {$set: {middleName: null}})→ 文档里多了一个"middleName": null - 正确:
db.users.updateOne({_id: ObjectId("...")}, {$unset: {middleName: ""}})→ 字段彻底消失 - Mongoose 中对应:
doc.middleName = undefined; await doc.save()或doc.set("middleName", undefined)
注意:$unset 的 value 任意(习惯写 "" 或 1),MongoDB 只看 key。
数组字段里存 null 是危险操作
这是最容易踩的坑:往数组字段 push null,比如 members: [ObjectId("..."), null]:
- Mongoose 会把它当作合法元素存进去,但后续
$pull、$in查询行为异常 -
db.conversations.find({members: {$in: [null]}})可能不命中,因为 BSONnull和 JavaScriptnull在某些驱动版本下序列化不一致 - 更安全的做法是:用
$pull清掉无效 ID,或确保数组只含有效ObjectId,空数组就留[],不要塞null
尤其注意 Mongoose Schema 定义——如果字段类型是 [Schema.Types.ObjectId],传入 null 会被自动过滤掉;但如果误写成 { type: Schema.Types.ObjectId }(非数组),再塞进数组,结果就是一堆 null。











