mongodb多字段唯一索引表设计

原始表设计是这样的:

_id, keyA, keyB, keyC, …其他字段忽略

其中 keyA, keyB, keyC 是联合唯一索引
keyA 有20万左右
keyB 对于每个keyA有800个左右
keyC 和keyB是一对多的关系,对于每个keyB有800个左右,
就是一个keyB绝大部分只会对应1个keyC,
偶尔会对应2-3个(这里忽略不计)

如果全部打平存储,我们的数据量是:(忽略keyB对应多个keyC的场景)
200000*800 = 1.6亿条

crud的操作也很简单,对各个主要字段进行更新就可以了。
因为业务场景,所以每次CRUD操作都会带上keyA,
所以我们可以用keyA作为片键
对于1.6亿条数据,我们就需要分4片集群,
每片2台物理机,加两台路由机器,加3台config(路由机跑掉),
共需要10台物理机

这样操作的优点和缺点:
优点:
1、数据结构简单清晰,程序员编码简单
2、CRUD操作的语句编写简单

缺点:
1、数据量大
2、机器消耗大10台物理机的投入
3、每次update操作都会造成索引的重排,性能可能不理想
4、当查询数据流量大,查询12个KeyA,就相当于查询上万多个keyB时
5、对于更新和读取,每片都需要在4千万的数据量上进行检索,
读取性能可能存在问题

下面我们对这个业务需求进行一下结构优化
_id,
keyA, //唯一索引
keyBs:{
keyB1:{
keyC1:1,
},
keyB2:{
keyC2-1:1,
keyC2-2:1,
},…
},
… 其他字段
这样改有几个优点和缺点:
优点:
1、数据的文档条数,从1.6亿减少为20万,所以在20万条数据量检索,
性能会比4千万好不少
2、索引只有keyA一个唯一索引,索引相对简单,对于update操作性能好
3、对于20万条记录,只需要一个3台机器的副本集就可以了,
相对机器消耗较少
4、当查询12个keyA时,流量消耗也相对较少

缺点:
1、数据结构较复杂,程序员编写程序难度大
2、相比原始方案,对于只查询一条或几条记录的情况,
新方案每次都会把整个内嵌文档都响应出去,性能差
3、由于Mongodb3的存储引擎是文档锁,在非常频繁的更新文档的场景下,
新方案对于同一个keyA瞬间并发很大的update,性能不佳
4、对于keyB和keyC,只能使用string类型,
如果是其他类型需要在程序获取到数据后手动转类型
5、无法单独对keyC进行操作,比如根据keyA更新掉某些keyC的内容
6、无法对KeyB进行单独更新操作,比如根据keyA更新掉某些keyB的内容,
而不传入keyC的内容
对于5,6这样的需求只能两段式提交了(不安全,非原子性操作)

那么新方案我们究竟改如何进行crud呢?
我们的数据库内容如下:
{
“_id” : ObjectId(“58a7aa5092bc315538faa2de”),
“a” : “1”,
“bs” : {
“b1” : {
“c1” : 1
},
“b2” : {
“c2” : 1
},
“b3” : {
“c3-1” : 1,
“c3-2” : 1
}
}
}

1、查询一个带 keyA=”1”,keyB=”b1”,keyC=”c1” 的场景:
db.getCollection(‘s2’).find({“a”:”1”, “bs.b1.c1”:{$exists:1}})

2、查询一个带 keyA=”1”,keyB=”b1 and b2 and b3” 的场景:
db.getCollection(‘s2’).find({“a”:”1”,
“bs.b1”:{$exists:1},
“bs.b2”:{$exists:1},
“bs.b3”:{$exists:1} })

2、查询一个带 keyA=”1”,keyB=”b1 or b4 or b5” 的场景:
db.getCollection(‘s2’).find({“a”:”1”,
$or:[
{“bs.b1”:{$exists:1}},
{“bs.b4”:{$exists:1}},
{“bs.b5”:{$exists:1}}
]
})

3、插入一个keyB,条件 keyA=”1”, keyB=”b4”
db.getCollection(‘s2’).update(
// query
{
“a” : “1”
},

// update 
{
    "$set":{
        "bs.b4":{}
    }
},

// options 
{
    "multi" : false,  // update only one document 
    "upsert" : false  // insert a new document, if no existing document match the query 
}

);

4、插入一个keyC,条件 keyA=”1”, keyB=”b4”, keyC = “c4”
db.getCollection(‘s2’).update(
// query
{
“a” : “1”
},

// update 
{
    "$set":{
        "bs.b4.c4":1
    }
},

// options 
{
    "multi" : false,  // update only one document 
    "upsert" : false  // insert a new document, if no existing document match the query 
}

);

5、插入一个新的 keyB 和 keyC,条件 keyA=”1”, keyB=”b5”, keyC = “c5”
db.getCollection(‘s2’).update(
// query
{
“a” : “1”
},

// update 
{
    "$set":{
        "bs.b5.c5":1
    }
},

// options 
{
    "multi" : false,  // update only one document 
    "upsert" : false  // insert a new document, if no existing document match the query 
}

);

6、删除一个keyC,条件 keyA=”1”, keyB=”b5”, keyC = “c5”
db.getCollection(‘s2’).update(
// query
{
“a” : “1”
},

// update 
{

     "$unset":{ "bs.b5.c5":1}

},

// options 
{
    "multi" : false,  // update only one document 
    "upsert" : false  // insert a new document, if no existing document match the query 
}

);

7、删除一个keyB,条件 keyA=”1”, keyB=”b4”
db.getCollection(‘s2’).update( // query
{
“a” : “1”
},

    // update 
    {
         "$unset":{ "bs.b4":1}
    },

    // options 
    {
        "multi" : false,  // update only one document 
        "upsert" : false  // insert a new document, if no existing document match the query 
    }
);

8、将一个keyB下面的keyC更新,
条件 keyA=”1”, keyB=”b3” , keyC=”c3-1” 更新为 keyC=”c3-3”
这个比较麻烦,其实就是删除一个,再插入一个
db.getCollection(‘s2’).update(
// query
{
“a” : “1”
},

// update 
{

     "$unset":{ "bs.b3.c3-1":1},
     "$set":{ "bs.b3.c3-3":1},

},

// options 
{
    "multi" : false,  // update only one document 
    "upsert" : false  // insert a new document, if no existing document match the query 
}

);

最后是性能对比,我在自己的pc机上对这些数据量进行一个简单的测试,
只做更新和查询的
考虑到插入数据时间太长,
我把方案1缩小40倍,数据量100万,方案2同样缩小40倍,5000条
1、插入100万python代码

-- coding: utf-8 --

#coding=utf-8
import datetime
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient(‘mongodb://127.0.0.1:27017/test’)
db = client.test
col = db.s1.with_options(write_concern=WriteConcern(w=1))

j = 0
for i in range(0, 1000000):
data = {
“a”: “a1”,
“b”: “b1”,
“c”: “c1”,
“t1”:”text1”,
“t2”:”text2”,
“t3”:”text3”,
“t4”:”text4”,
“t5”:”text5”,
“wt”:datetime.datetime.now(),
}

  #本来每片5万个keyA,缩小40倍就是1250个,所以没800次循环增加一个keyA
if i%800== 0:
    print("insrt {0}".format(i))
    j += 1
data["a"] = "a{0}".format(j)
data["b"] = "b{0}".format(i)
data["c"] = "c{0}".format(i)
result = col.insert_one(data)

2、下面同样缩小40倍,准备5000条方案2的数据,我们存入表s2

-- coding: utf-8 --

#coding=utf-8
import datetime
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient(‘mongodb://127.0.0.1:27017/test’)
db = client.test
col = db.s22.with_options(write_concern=WriteConcern(w=1))

for i in range(0, 5000):
data = {
“a”: “”,
“b”: {},
“t1”:”text1”,
“t2”:”text2”,
“t3”:”text3”,
“t4”:”text4”,
“t5”:”text5”,
“wt”:datetime.datetime.now(),
}
data[“a”] = “a{0}”.format(i)
for k in range(0,100):
data[“b”][“b{0}”.format(k)] = {}
data[“b”][“b{0}”.format(k)][“c{0}”.format(k)] = 1

result = col.insert_one(data)

1、测试1,更新操作,对keyA,keyB,keyC进行全条件更新,操作5万次,看运行时间
对于完全打平的情况

-- coding: utf-8 --

#coding=utf-8
import datetime
import time
import math
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient(‘mongodb://127.0.0.1:27017/test’)
db = client.test
col = db.s1.with_options(write_concern=WriteConcern(w=1))

s = time.mktime(datetime.datetime.now().timetuple())
for i in range(100000, 150000):
if i %10000== 0:
print(i)
xi = int(math.floor(i/800)) + 1
cond = {
“a”:”a{0}”.format(xi),
“b”:”b{0}”.format(i),
“c”:”c{0}”.format(i),
}
up = {
“$set”:{
“c”:”c{0}x”.format(i)
}
}
result = col.update_one(cond, up)
if result.matched_count == 0:
print(cond, “error”)
break

e = time.mktime(datetime.datetime.now().timetuple())
print(“time {0}s”.format(e-s))
运行结果,更新1万次
time 57.0s

对于新的方案

-- coding: utf-8 --

#coding=utf-8
import datetime
import time
import math
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient(‘mongodb://127.0.0.1:27017/test’)
db = client.test
col = db.s22.with_options(write_concern=WriteConcern(w=1))

s = time.mktime(datetime.datetime.now().timetuple())
for i in range(5, 15):
for j in range(0,5000):
if j %10000 == 0:
print(j)
key = “b.b{0}.c{0}”.format(i,i)
key2 = “b.b{0}.c{0}x”.format(i,i)
cond = {
“a”:”a{0}”.format(j),
}
cond[key] = {“$exists”:1}
up = {
“$unset”:{},
“$set”:{},
}
up[“$unset”][key] = 1
up[“$set”][key2] = 1

result = col.update_one(cond, up)
if result.matched_count == 0:
    print(cond, "error")
    break

e = time.mktime(datetime.datetime.now().timetuple())
print(“time {0}s”.format(e-s))
更新5万个数据,耗时
time 54.0s

2、测试查询,查询混合条件1万次
打平查询:

-- coding: utf-8 --

#coding=utf-8
import datetime
import time
import math
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient(‘mongodb://127.0.0.1:27017/test’)
db = client.test
col = db.s1.with_options(write_concern=WriteConcern(w=1))

s = time.mktime(datetime.datetime.now().timetuple())
for i in range(10000, 20000):
if i %1000 == 0:
print(i)
xi = int(math.floor(i/800)) + 1
cond1 = {
“a”:”a{0}”.format(xi),
}
cond2 = {
“a”:”a{0}”.format(xi),
“b”:”b{0}”.format(i),
}

result = col.find(cond1)
if not result[0]:
print(cond, “error”)
break

result = col.find(cond2)
if not result[0]:
print(cond2, “error”)
break

e = time.mktime(datetime.datetime.now().timetuple())
print(“time {0}s”.format(e-s))

运行时间:
time 27.0s

内嵌文档方案:

-- coding: utf-8 --

#coding=utf-8
import datetime
import time
import math
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient(‘mongodb://127.0.0.1:27017/test’)
db = client.test
col = db.s22.with_options(write_concern=WriteConcern(w=1))

s = time.mktime(datetime.datetime.now().timetuple())
for i in range(0, 2):
for j in range(0,5000):
if j %1000 == 0:
print(j)

cond1 = {
“a”:”a{0}”.format(j),
}
cond2 = {
“a”:”a{0}”.format(j),
}
cond2[“b.b{0}”.format(i)] = {“$exists”:1}

result = col.find(cond1)
if not result[0]:
print(cond, “error”)
break

result = col.find(cond2)
if not result[0]:
print(cond2, “error”)
break

e = time.mktime(datetime.datetime.now().timetuple())
print(“time {0}s”.format(e-s))

运行时间:
time 23.0s

总结一下:
在经过测试和优化之后,数据量从1.6亿减少到20万,服务器从10台减少到3台,但是性能还有些许提高
所以实践证明,我们的改造是可行的