写在前面
关于什么是索引以及唯一索引这里就不做说明了,不清楚的可以自行谷歌或者百度。是什么引起我写这篇文章呢,这来自于之前项目中的一个问题。
我们用的是 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 uniqueindex, the index will store a null value for this document. Because ofthe unique constraint, MongoDB will only permit one document thatlacks the indexed field. If there is more than one document without avalue for the indexed field or is missing the indexed field, the indexbuild 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)
}
但是这又正如我们前面说的那样,打破了数据原有的数据结构。哎,有得有得。当然我们还可以从业务层面去解决,比如注册时对其查询等操作,当然会耗一定性能,不管你是那空间换时间,还是拿时间换空间总得付出一个,别做一个太贪心的人。