在MongoDB中,存储null值与根本不存储该键有何区别?

65
我认为,当你创建一个Mongo文档并且有一个字段{key: value}可能有时不会有值时,你有两个选项:
  1. 写入{key: null},即在该字段中写入null值
  2. 根本不在该文档中存储键
这两个选项都很容易查询。对于第一个选项,您可以查询{key : null},而对于另一个选项,则可以查询{key : {$exists : false}}
我真的想不出这两个选项之间有任何差异会对应用场景产生任何影响(除了选项2略少一些存储空间)。
有人能告诉我是否有任何理由更喜欢其中的一种方法,以及为什么吗?
编辑后,我也想到了索引在这两种情况下的行为可能不同,即可以为选项2创建稀疏索引。

3
就价值而言,“略少的存储空间”在大规模应用中可能更为重要,特别是当你处理内存中工作集的大小时。 - Christopher
5个回答

43

实际上,你还有第三种可能性:

key: ""(空值)

你忘了一个关于 null 值的特殊情况。 查询 key: null 将检索所有 key 为 null 或者 key 不存在的文档。

当查询 $exists:false 时,将仅检索字段 key 不存在的文档。

回到你确切的问题,这取决于你的查询和数据表示。 如果你需要保留,例如,用户设置一个值然后取消设置,你应该将字段保留为空或 null。如果你不需要,可以删除这个字段。


17
个人会避免使用键名为“”的键,因为这会暗示它是一个字符串。如果您检索到这样的字段并执行类似于isKeyExists(key)的操作,则即使它是空字符串,此操作也将返回true。因此,假设您原本期望的是一个布尔值,这种情况就容易出错了。 - Sani Yusuf
你甚至有第四种可能性:key: undefined。查询将会不同,请参见 https://stackoverflow.com/questions/68255363/or-with-if-and-in-mongodb/68255564#68255564 - Wernfried Domscheit

22
请注意,由于MongoDB不使用字段名字典压缩,field:null会占用磁盘空间和内存,而根本不存储键的情况下则不会消耗任何资源。

13
这个答案在2018年还有效吗? - ankitjaininfo
5
抱歉回复晚了!我认为情况已经不同了。启用WiredTiger存储和snappy压缩后,我认为不应该会有明显的感觉。 - Samuel García
是的,即使在2022年仍然有效。但正如Samuel所述,额外的空间应该可以忽略不计。 - Wernfried Domscheit

10

这真的取决于:

  • 你的场景
  • 你的查询方式
  • 你的索引需求
  • 你的语言

我个人选择存储null键,这样更容易集成到我的应用程序中。我使用带有Active Record的PHP,并使用null值使得我的生活变得轻松许多,因为我不必将字段依赖性的压力放在应用程序上。此外,我不需要编写任何复杂的代码来处理设置不存在变量的魔术问题。

我个人不会存储空字符串"",因为如果你不小心,你可能会有两个空值null"",然后你将会很难具体查询。所以我个人更喜欢使用空值null代替空字符串。

至于空间和索引:这取决于可能没有此列的行数,但我怀疑你不会真正注意到由于几个额外的包含空值的文档而导致的索引大小增加。我的意思是存储差异很小,特别是如果相应的键名也很小的话。这同样适用于大型设置。

我不确定$existsnull之间的索引使用情况,然而null可能是一种更标准化的方法来查询存在性,因为请记住MongoDB是无模式的,这意味着你没有必要在文档中拥有那个字段,这又产生了两个空值:不存在和null。所以最好选择一种或另一种。

我选择null


谢谢...但我并不认为使用null会使应用程序代码更容易(至少在我使用的C#驱动程序中不是这样)。在C#驱动程序中,不存在的键自动反序列化为null,因此没有复杂的代码。 - Zaid Masud
@ZaidMasud 是的,在C#中也有驱动程序问题和强类型语言。这就是为什么这个问题很难回答,因为没有跨所有平台的标准,不像CSS、HTML或OOP等,一切都取决于你自己。 - Sammaye

2

你可能想考虑的另一个要点是使用类似Hibernate OGM这样的OGM工具时。

如果你正在使用Java,Hibernate OGM支持JPA标准。因此,如果您可以编写JPQL查询,则在想切换到OGM工具支持的其他NoSQL数据存储时理论上会更加容易。

JPA不为Mongo中的$exists定义等效项。因此,如果您的集合中有可选属性,则无法编写相应的JPQL。在这种情况下,如果属性的值存储为NULL,则仍然可以编写有效的JPQL查询,例如下面的示例:

SELECT p FROM pppoe p where p.logout IS null;

在 MongoDB 中查询 null 包括缺失的值。来自官方文档:"{ item : null }" 查询匹配包含值为 null 的 item 字段的文档 或者不包含 item 字段 的文档。 - Zaid Masud
MongoDB确实支持查询空值,但重点是与JPQL的功能等效性。如果是缺少的属性,将无法编写等效的JPQL。 - Vinod
我明白,我只是在说我认为 SELECT p FROM pppoe p where p.logout IS null; 会在结果中包含缺失的属性 - Zaid Masud

0

就磁盘空间而言,差异微不足道。如果您需要在此字段上创建索引,请考虑部分索引

在具有{ partialFilterExpression: { key: { $exists: true } } }的索引中,大小可能比普通索引小得多。

还应该注意到,查询看起来不同,例如这样的值:

db.collection.insertMany([
  { _id: 1, a: 1 }, 
  { _id: 2, a: '' }, 
  { _id: 3, a: undefined }, 
  { _id: 4, a: null }, 
  { _id: 5 }
])
db.collection.aggregate([
   {
      $set: {
         type: { $type: "$a" },
         ifNull: { $ifNull: ["$a", true] },
         defined: { $ne: ["$a", undefined] },
         existing: { $ne: [{ $type: "$a" }, "missing"] }
      }
   }   
])
   
{ _id: 1, a: 1,         type: double,    ifNull: 1,    defined: true,  existing: true }
{ _id: 2, a: "",        type: string,    ifNull: "",   defined: true,  existing: true }
{ _id: 3, a: undefined, type: undefined, ifNull: true, defined: false, existing: true }
{ _id: 4, a: null,      type: null,      ifNull: true, defined: true,  existing: true }
{ _id: 5,               type: missing,   ifNull: true, defined: false, existing: false }

或者使用 db.collection.find()

db.collection.find({ a: { $exists: false } })
  { _id: 5 }

db.collection.find({ a: { $exists: true} })
  { _id: 1, a: 1 }, 
  { _id: 2, a: '' }, 
  { _id: 3, a: undefined }, 
  { _id: 4, a: null }

db.collection.find({ a: null })
  { _id: 3, a: undefined }, 
  { _id: 4, a: null },
  { _id: 5 }

db.collection.find({ a: {$ne: null} })
  { _id: 1, a: 1 }, 
  { _id: 2, a: '' }, 

db.collection.find({ a: {$type: "null"} })
  { _id: 4, a: null }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接