Skip to main content

Command Palette

Search for a command to run...

mongodb联合索引

Updated
4 min read

1. 需求

  • 对mongo的数据进行分类,比如按照type进行分类;比如type的value: v1,v2,v3
  • 需要支持按照主键进行排序输出;主要是因为要支持列举

2. 索引方案

// 为type和_id来创建联合索引,type放在前面
db.v4.createIndex({type:1, _id:1})

3. 实际效果

数据分布:

  • 总量为:1000w
  • type=v1: 100w
  • type=v2: 600w
  • type=v3: 300w

3.1 查询type=v1并且按照_id进行排序

查询语句:

db.v4.find({type:"v1"}).sort({type:1,_id:1}).explain("allPlansExecution")

执行计划和执行详情

"winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "type" : 1,
                    "_id" : 1
                },
                "indexName" : "type_1__id_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "type" : [ ],
                    "_id" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "type" : [
                        "[\"v1\", \"v1\"]"
                    ],
                    "_id" : [
                        "[MinKey, MaxKey]"
                    ]
                }
            }
        }
"executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 1000000,
        "executionTimeMillis" : 3990,
        "totalKeysExamined" : 1000000,
        "totalDocsExamined" : 1000000,
        "executionStages" : {
            "stage" : "FETCH",
            "nReturned" : 1000000,
            "executionTimeMillisEstimate" : 3874,
            "works" : 1000001,
            "advanced" : 1000000,
            "needTime" : 0,
            "needYield" : 0,
            "saveState" : 7829,
            "restoreState" : 7829,
            "isEOF" : 1,
            "invalidates" : 0,
            "docsExamined" : 1000000,
            "alreadyHasObj" : 0,
            "inputStage" : {
                "stage" : "IXSCAN",
                "nReturned" : 1000000,
                "executionTimeMillisEstimate" : 767,
                "works" : 1000001,
                "advanced" : 1000000,
                "needTime" : 0,
                "needYield" : 0,
                "saveState" : 7829,
                "restoreState" : 7829,
                "isEOF" : 1,
                "invalidates" : 0,
                "keyPattern" : {
                    "type" : 1,
                    "_id" : 1
                },
                "indexName" : "type_1__id_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "type" : [ ],
                    "_id" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "type" : [
                        "[\"v1\", \"v1\"]"
                    ],
                    "_id" : [
                        "[MinKey, MaxKey]"
                    ]
                },
                "keysExamined" : 1000000,
                "seeks" : 1,
                "dupsTested" : 0,
                "dupsDropped" : 0,
                "seenInvalidated" : 0
            }
        },
        "allPlansExecution" : [ ]
    },

结论: 通过type + _id组合索引可以达到我们的需求,排序本身在遍历的过程中就已经完成,不需要二次排序

3.2 查询type=v1并且按照_id逆序

查询语句

//这边有两种方式来进行查询,区别在于type和_id的排序顺序问题
db.v4.find({type:"v1"}).sort({type:1,_id:-1}).explain("allPlansExecution")
db.v4.find({type:"v1"}).sort({type:-1,_id:-1}).explain("allPlansExecution")

执行计划

// plan-1
"winningPlan" : {
            "stage" : "SORT",
            "sortPattern" : {
                "type" : 1,
                "_id" : -1
            },
            "inputStage" : {
                "stage" : "SORT_KEY_GENERATOR",
                "inputStage" : {
                    "stage" : "FETCH",
                    "inputStage" : {
                        "stage" : "IXSCAN",
                        "keyPattern" : {
                            "type" : 1,
                            "_id" : 1
                        },
                        "indexName" : "type_1__id_1",
                        "isMultiKey" : false,
                        "multiKeyPaths" : {
                            "type" : [ ],
                            "_id" : [ ]
                        },
                        "isUnique" : false,
                        "isSparse" : false,
                        "isPartial" : false,
                        "indexVersion" : 2,
                        "direction" : "forward",
                        "indexBounds" : {
                            "type" : [
                                "[\"v1\", \"v1\"]"
                            ],
                            "_id" : [
                                "[MinKey, MaxKey]"
                            ]
                        }
                    }
                }
            }
        }

// plan-2 
"winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "type" : 1,
                    "_id" : 1
                },
                "indexName" : "type_1__id_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "type" : [ ],
                    "_id" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "backward",
                "indexBounds" : {
                    "type" : [
                        "[\"v1\", \"v1\"]"
                    ],
                    "_id" : [
                        "[MaxKey, MinKey]"
                    ]
                }
            }
        },

结论: type和_id的排序的方向需要一致,在我们的场景中其实type是确定的,所以只要查询语句写的ok是符合要求的

3.3 type=v1 && _id > xxx

这个查询的需求在于list请求,每个请求会带上_id来保证接个下次的请求继续查询

查询语句

db.v4.find({type:"v1",_id:{"$gte":"0c82507c-d67e-11ed-8157-6c92bf361376"}}).sort({type:1,_id:1}).explain()

执行计划

"winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "type" : 1,
                    "_id" : 1
                },
                "indexName" : "type_1__id_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "type" : [ ],
                    "_id" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "type" : [
                        "[\"v1\", \"v1\"]"
                    ],
                    "_id" : [
                        "[\"0c82507c-d67e-11ed-8157-6c92bf361376\", {})"
                    ]
                }
            }
        },
        "rejectedPlans" : [
            {
                "stage" : "SORT",
                "sortPattern" : {
                    "type" : 1,
                    "_id" : 1
                },
                "inputStage" : {
                    "stage" : "SORT_KEY_GENERATOR",
                    "inputStage" : {
                        "stage" : "FETCH",
                        "filter" : {
                            "type" : {
                                "$eq" : "v1"
                            }
                        },
                        "inputStage" : {
                            "stage" : "IXSCAN",
                            "keyPattern" : {
                                "_id" : 1
                            },
                            "indexName" : "_id_",
                            "isMultiKey" : false,
                            "multiKeyPaths" : {
                                "_id" : [ ]
                            },
                            "isUnique" : true,
                            "isSparse" : false,
                            "isPartial" : false,
                            "indexVersion" : 2,
                            "direction" : "forward",
                            "indexBounds" : {
                                "_id" : [
                                    "[\"0c82507c-d67e-11ed-8157-6c92bf361376\", {})"
                                ]
                            }
                        }
                    }
                }
            }
        ]
//这边出现了另外一个执行计划,想走_id索引,然后过滤type,在排序; 不过从最终执行计划的积分发现,这种性能会比较差

//这个是被拒绝的计划的得分
score(1.0002) = baseScore(1) + productivity((0 advanced)/(101 works) = 0) + tieBreakers(0.0001 noFetchBonus + 0 noSortBonus + 0.0001 noIxisectBonus = 0.0002)

//这个是走联合索引的得分;因为在我们场景中这个索引的得分是一定会高的,因为底层的到的数据就是有效数据并且排序
score(2.0003) = baseScore(1) + productivity((101 advanced)/(101 works) = 1) + tieBreakers(0.0001 noFetchBonus + 0.0001 noSortBonus + 0.0001 noIxisectBonus = 0.0003)

结论: 符合要求

3.4 type=1 && _id按照某一个前缀

查询语句

db.v4.find({type:"v1",_id:{"$regex":"^0c82507c"}}).sort({type:1,_id:1}).explain()

执行计划

    "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "type" : 1,
                    "_id" : 1
                },
                "indexName" : "type_1__id_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "type" : [ ],
                    "_id" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "type" : [
                        "[\"v1\", \"v1\"]"
                    ],
                    "_id" : [
                        "[\"0c82507c\", \"0c82507d\")",
                        "[/^0c82507c/, /^0c82507c/]"
                    ]
                }
            }
        }

结论:符合预期,场景可能想找某一个共同前缀的信息吧;

4. 联合索引的存储成本

hd_listexporter_shard1:PRIMARY> db.v4.stats()
{
    "ns" : "test.v4",
    "size" : 850000000,
    "count" : 10000000,
    "avgObjSize" : 85,
    "storageSize" : 849999872,
    "capped" : false,
    "nindexes" : 2,
    "totalIndexSize" : 1371037969,
    "indexSizes" : {
        "_id_" : 430000000,
        "type_1__id_1" : 941037969
    },
    "ok" : 1
}

新加的联合索引目前是_id_的两倍多,如果现实场景可能会随着type本身的长度比例上可能会有偏差

5. 结论

联合索引在我来看其实本质是就是将type + _id两个字段按照某一种组合再次创建索引的过程的,而且联合索引本身有有顺序的,比如A,B两个字段创建了索引,你单独查询B是不能命中索引的,需要单独对B来创建索引解决你这样问题的。不过看到最终的存储成本,发现远比我想象中的要大一些,先留个疑问,下次看源码的时候再来解释。

More from this blog

Ai时代的工具链

本周是black Friday,我订阅了几个AI服务,还是蛮贵的...不过这样基本上构成我目前整体的知识阅读的过程,随着Ai的不断发展,工具链的替换可能是很重要的一个过程的。我主要订购了以下几个工具: Memo: 这个工具的主要作用是将视频/audio转srt,并且带有ai翻译的工具;当然我觉得它做的非常好的是,它把整个链路做的非常好的,并且可以用本地的资源做audio->text;而且它自带了很多的ai功能,比如对字幕进行进一步的AI的处理,提问,summarize和思维导图等等;目前我主要...

Nov 30, 20251 min read

做了一个噩梦

今天凌晨4点多起来看了一眼丈母娘的发烧是否ok...就导致我有点睡不着的,刷了一会推特之后又开始睡觉了,于是就开始做了一个很可怕的梦。 噩梦 那天,我不知道是在哪里..我带着女儿和我弟出去玩的,貌似是一个风景山区。于是我就带着女儿和弟弟出去玩的;我们走啊走, 沿着一条路一直走..突然看到一个小道有一家饭店的,这个饭店是比较特殊,有很多海鲜的;我看上了一只大龙虾,我问多少钱的,他说大概就70rmb就可以的。。。我觉得很划算的,我心想:我买下来,到时候把老婆叫过来一起吃的,并且告诉她这个才70rmb...

Nov 24, 20251 min read

子女教育-2

下面我分享一个推特上的一个关于子女教育的推 哈哈哈哈,李诞这个视频我看过 我给你分享几个我和我女儿之间的小故事 第一个故事 我经常给小朋友说:你们现在上学的成绩不重要,你们现在数学考试都是语文脑筋急转弯,语文考试都是历史背诵,一点用都没有,你出了社会就知道,社会根本没有选择题,社会要有选择题就好了,最难的是你遇到困难,你连门都找不到。我第一次这样讲的时候是女儿小学4年级,那时候我女儿听的一愣一愣的,她不明白,但是觉得我的理论和学校的不一样,很狂妄,但是她很喜欢,哈哈哈哈。 她什么时候真正明...

Nov 13, 20251 min read

被诈骗-马来西亚

最近我在国内,我老婆在马来;最近在计划搬家的,找的那个房子不包含一些必要的家具,于是我老婆就必须要买点家具的,主要是沙发和餐桌..我们本来计划是说去ikea去买,但是我老婆觉得ikea的家具不便宜,并且款式一般的,最终问了中介找了一个二手平台找找看不错的家具。 我老婆挑了两个家具的,我看了一下价格也不算便宜的,但是我老婆喜欢的,于是我就说你觉得ok那就购买吧。我还顺便问了一下,这个家具能不能线下看一下货的,但是我老婆说这货在很远的地方的,大概是300公里的一个城市的。那我就说这个包邮吗,我老婆说...

Nov 13, 20251 min read

当下和最近想做的事情

1. Current 当下 最近依然还在中国,已经回来快一个月了. 最近一直在忙着带丈母娘看病和住院的。索性一切都还在可控范围内的,丈母娘由于糖尿病控制的很差导致本身的冠心病也复发. 这次去浙江省人民医院去做了造影检查和支架植入的手术的,不过这一切都比我预估的要顺利,我就怕她由于长时间没吃药和高血糖的持续的时间太长了,会带来严重的问题,不过好在没有发生最坏的事情的。 因为做了手术,所以这段时间我和我老婆的姐姐每人轮换的陪床,不过陪床真的好累的,因为睡得很不好的,特别的累。不过好在都结束了,而且丈...

Nov 9, 20251 min read

Keep Move - 永不止步

39 posts