写在前面

关于什么是索引以及唯一索引这里就不做说明了,不清楚的可以自行谷歌或者百度。是什么引起我写这篇文章呢,这来自于之前项目中的一个问题。

我们用的是MongoDB数据存储用户信息,用户表中曾经用户注册是通过手机号注册的,所以很理所当然的给手机号加上了唯一索引(Unique),这是没有什么毛病。

后期,我们需求改了。你也可以想到变成了既可以手机号注册又可以邮箱注册,这个时候由于手机号加了Unique索引,事实上这时候是会出现问题的。

func init() { phoneIndex := mgo.Index{ Key: []string{"phone"}, Unique: true, } col := db.Collection(&User{}) col.EnsureIndex(phoneIndex)}

当然这问题其实也容易想到,当用户通过邮箱注册此时手机号填空的时候,第一次没什么问题,下个用户再以这种方式注册的时候便会提示建立在phone上的索引值重复,很正常嘛,因为插入了两个空值,注意这里是空字符串,而不是null。

于是我们尝试修改,由于MongoDB是文档型灵活的数据库,少插多插一两个字段不受影响,所以我们尝试修改User实体Phone字段的入口,当phone是空字符串的时候,不让插入此字段。

于是,我们便在phone字段中加入了omitempty标签(我们微服务用Go语言写的)。下面展示User一部分内容:

type User struct { Email string `bson:"email"` Salt string `bson:"salt"` Phone string `bson:"phone,omitempty"` IDCard string `bson:"idcard"` RealName string `bson:"realname"` AuthStatus int `bson:"auth_status"`}

可以看到phone字段后加了omitempty标签,表示当该字段为空的时候不插入。这还是会出现问题,那么既然还是会出问题为什么会想到这么解决呢?

这源于对Mysql的使用经验,习惯性的以为MongoDB和Mysql那样,对null的值会不做其索引。也就是说,在Mysql中,若在多条记录中Phone值为Null是被允许的。

上面那种做法,还是会报错,提示插入了重复的值,只不过这时不是空字符串,而是null。所以有时候就不要把Mysql那套拿来了,Mysql是可以的,但Mongo不行。mongo还是会对该条记录索引,即使该字段为被插入。

我喜欢看官方文档,下面给出MongoDB官方文档说明:

If a document does not have a value for the indexed field in a unique index, the index will store a null value for this document. Because of the unique constraint, MongoDB will only permit one document that lacks the indexed field. If there is more than one document without a value for the indexed field or is missing the indexed field, the index build will fail with a duplicate key error.

其实已经说得很清楚了,稍微会点英语应该都能看懂,下面还是给出翻译版:

如果文档没有唯一索引中索引字段的值,则索引将为此文档存储null值。由于唯一约束,MongoDB只允许一个缺少索引字段的文档。如果有多个文档没有索引字段的值或缺少索引字段,则索引构建将失败并出现重复键错误。

也就是说这个字段哪怕在文档中没有,那么该字段将会存null值,该字段上也不能同时出现两个null值,这就是为什么上面那种做法还是行不通的原因。

其实上面那种做法也打破了数据结构,虽然手机号未填,但数据库中也不应该缺少这个字段,尽管是非关系数据库,毕竟还得考虑下业务设计。

解决方式

是不是就没有解决方式了呢?当然有,Mongo提供了Sparse Index,被翻译为稀疏索引。下面是创建稀疏索引的例子:

db.getCollection("test").createIndex({"phone":1}, {sparse:true})

执行上面的语句后,不会去索引不存在phone字段的文档。也就是说存在才对其索引,那么此时和Unique索引结合起来就可以派上用场了。Unqiue是唯一,Sparse是存在才索引。所以,当phone或email为空的时候我们可以不将其插入这是可以实现的。

db.getCollection("test").createIndex({"phone":1}, {sparse:true,unique:true})

上面是是mongo shell语法,通常我们一般通过代码中建立索引,修改如下(当然User结构体中Phone字段omitempty标签还是要有的):

func init() { phoneIndex := mgo.Index{ Key: []string{"phone"}, Unique: true, Sparse: true, } col := db.Collection(&User{}) col.EnsureIndex(phoneIndex)}

但是这又正如我们前面说的那样,打破了数据原有的数据结构。哎,有得有得。当然我们还可以从业务层面去解决,比如注册时对其查询等操作,当然会耗一定性能,不管你是那空间换时间,还是拿时间换空间总得付出一个,别做一个太贪心的人。