写在前面sql
关于什么是索引以及惟一索引这里就不作说明了,不清楚的能够自行谷歌或者百度。是什么引发我写这篇文章呢,这来自于以前项目中的一个问题。shell
咱们用的是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仍是会对该条记录索引,即便该字段为被插入。ui
我喜欢看官方文档,下面给出MongoDB官方文档说明:this
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.
其实已经说得很清楚了,稍微会点英语应该都能看懂,下面仍是给出翻译版:spa
若是文档没有惟一索引中索引字段的值,则索引将为此文档存储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) }
可是这又正如咱们前面说的那样,打破了数据原有的数据结构。哎,有得有得。固然咱们还能够从业务层面去解决,好比注册时对其查询等操做,固然会耗必定性能,无论你是那空间换时间,仍是拿时间换空间总得付出一个,别作一个太贪心的人。