mongodb优化查询【转】

查询优化器

MongoDB的查询计划会将多个索引并行去执行,最先返回101的结果就是胜者,其他查询计划就会被终止,执行优胜的查询计划
这个查询计划将会被缓存,接下来相同的语句查询条件都会使用它

  • 何时查询计划才会变
  1. 建立索引时
  2. 每执行1000次查询之后,查询优化器就会重新评估查询计划
  3. 较大的数据波动

explain 使用

db.getCollection('db_name').explain('executionStats').aggregate([....])
// 得到的结果
{
    "stages" : [ 
        {
            "$cursor" : {
                "query" : {
                    .....
                },
                "fields" : {
                    ......
                },
                "queryPlanner" : {
                    ......
                    },
                    "winningPlan" : {                  // 优胜的方案
                        "stage" : "FETCH",
                        "filter" : {
                            "$and" : [ 
                                {
                                    "is_deleted" : {
                                        "$eq" : 0.0
                                    }
                                }
                            ]
                        },
                        "inputStage" : {
                            "stage" : "IXSCAN",
                            "keyPattern" : {
                                "acc_opening_date" : 1
                            },
                            "indexName" : "acc_opening_date_1",
                            "isMultiKey" : false,
                            "multiKeyPaths" : {
                                "acc_opening_date" : []
                            },
                            "isUnique" : false,
                            "isSparse" : false,
                            "isPartial" : false,
                            "indexVersion" : 2,
                            "direction" : "forward",
                            "indexBounds" : {
                                "acc_opening_date" : [ 
                                    "[new Date(1493625600000), new Date(1609350406000)]"
                                ]
                            }
                        }
                    },
                    "rejectedPlans" : [    // 拒绝的方案
                        {
                            "stage" : "FETCH",
                            "filter" : {
                                "$and" : [ 
                                    {
                                        "is_deleted" : {
                                            "$eq" : 0.0
                                        }
                                    }, 
                                    {
                                        "acc_opening_date" : {
                                            "$lte" : ISODate("2020-12-30T17:46:46.000Z")
                                        }
                                    }, 
                                    {
                                        "acc_opening_date" : {
                                            "$gte" : ISODate("2017-05-01T08:00:00.000Z")
                                        }
                                    }
                                ]
                            },

                        }, 
                        {
                            "stage" : "FETCH",
                            "filter" : {
                                "$and" : [ 
                                    {
                                        "acc_opening_date" : {
                                            "$lte" : ISODate("2020-12-30T17:46:46.000Z")
                                        }
                                    }, 
                                    {
                                        "acc_opening_date" : {
                                            "$gte" : ISODate("2017-05-01T08:00:00.000Z")
                                        }
                                    }
                                ]
                            },
                            "inputStage" : {
                                "stage" : "IXSCAN",  
                                "keyPattern" : {
                                    "is_deleted" : 1  //  is_deteted颗粒度这么大的索引不应该建立,难怪要被拒绝
                                },
                                "indexName" : "is_deleted_1",  
                                "isMultiKey" : false,
                                "multiKeyPaths" : {
                                    "is_deleted" : []
                                },
                                "isUnique" : false,
                                "isSparse" : false,
                                "isPartial" : false,
                                "indexVersion" : 2,
                                "direction" : "forward",
                                "indexBounds" : {
                                    "is_deleted" : [ 
                                        "[0.0, 0.0]"
                                    ]
                                }
                            }
                        }
                    ]
                },
                "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 1,     // 返回的结果数量
                    "executionTimeMillis" : 1,  // 运行的时间
                    "totalKeysExamined" : 1,  // 扫描的索引数量
                    "totalDocsExamined" : 1,  // 扫描的文档数量
                    "executionStages" : {
                        "stage" : "FETCH",                           //  step2: 用is_deleted字段从上一阶段的结果中过滤出相应结果
                        "filter" : {
                            "$and" : [ 
                                {
                                    "is_deleted" : {
                                        "$eq" : 0.0
                                    }
                                },
                            ]
                        },
                        "nReturned" : 1,
                        "executionTimeMillisEstimate" : 0,
                        "works" : 3,
                        "advanced" : 1,
                        "needTime" : 0,
                        "needYield" : 0,
                        "saveState" : 1,
                        "restoreState" : 1,
                        "isEOF" : 1,
                        "invalidates" : 0,
                        "docsExamined" : 1,
                        "alreadyHasObj" : 0,
                        "inputStage" : {
                            "stage" : "IXSCAN",                        //  step1: 用acc_opening_date字段索引搜索出相应结果
                            "nReturned" : 1,
                            "executionTimeMillisEstimate" : 0,
                            "works" : 2,
                            "advanced" : 1,
                            "needTime" : 0,
                            "needYield" : 0,
                            "saveState" : 1,
                            "restoreState" : 1,
                            "isEOF" : 1,
                            "invalidates" : 0,
                            "keyPattern" : {
                                "acc_opening_date" : 1                                    
                            },
                            "indexName" : "acc_opening_date_1",
                            "isMultiKey" : false,
                            "multiKeyPaths" : {
                                "acc_opening_date" : []
                            },
                            "isUnique" : false,
                            "isSparse" : false,
                            "isPartial" : false,
                            "indexVersion" : 2,
                            "direction" : "forward",
                            "indexBounds" : {
                                "acc_opening_date" : [ 
                                    "[new Date(1493625600000), new Date(1609350406000)]"
                                ]
                            },
                            "keysExamined" : 1,
                            "seeks" : 1,
                            "dupsTested" : 0,
                            "dupsDropped" : 0,
                            "seenInvalidated" : 0
                        }
                    }
                }
            }
        }, 
        {
            "$lookup" : {
                "from" : "clients",
                "as" : "clients",
                "localField" : "idp_user_id",
                "foreignField" : "idp_user_id",
                "unwinding" : {
                    "preserveNullAndEmptyArrays" : false
                }
            }
        }, 
        {
            "$project" : {
               ......
        }
    ],
    "ok" : 1.0,
    "operationTime" : Timestamp(1604133360, 3),
    "$clusterTime" : {
        "clusterTime" : Timestamp(1604133360, 3),
        "signature" : {
            "hash" : { "$binary" : "RbWJfLtWiuIthJ5C3oiKbGIt1iY=", "$type" : "00" },
            "keyId" : NumberLong(6854528299859705857)
        }
    }
}

  • 查看方式:嵌套最内层往外的顺序看,不是从上到下。

原因:
explain 结果将查询计划以阶段树的形式呈现。
每个阶段将其结果传递给父节点,中间节点操作由子节点产生的文档或索引

  • 索引使用情况解读

stage 主要分为以下几种:
COLLSACN: 全盘扫描
IXSACN: 索引扫描
FETCH: 根据前面节点扫描出的文档,进一步过滤抓取
SORT: 内存进行排序
SORT_KEY_GENERATOR: 获取每一个文档排序所用的键值
LIMIT: 使用limit限制返回数
SKIP: 使用skip进行跳过
IDHACK: 针对_id进行查询
COUNTSCAN: count不使用index进行count
COUNT_SCACN: count使用index进行count
TEXT: 使用全文索引进行查询
SUBPLA:未使用到索引的$or查询
PROJECTION:限定返回字段

  • 所以不希望看到explain分析出现如下的stage:

COLLSCAN
SORT
COUNTSCAN
SUBPLA

  • 最好是如下的组合:

FETCH + IXSCAN
FETCH + IDHACK
LIMIT + ( FETCH + IXSCAN)
PROJECTION + IXSCAN
COUNT_SCAN

效率极低的操作符

  1. where和exists:这两个操作符,完全不能使用索引。
  2. ne和not: 通常来说取反和不等于,可以使用索引,但是效率极低,不是很有效,往往也会退化成扫描全表。
  3. $nin: 不包含,这个操作符也总是会全表扫描
  4. 对于管道中的索引,也很容易出现意外,只有在管道最开始时的match sort可以使用到索引,一旦发生过project投射,group分组,lookup表关联,unwind打散等操作后,就完全无法使用索引。

aggregate优化

  1. 我认为的准则是尽可能先缩小文档大?。ɡ纾?img class="math-inline" src="https://math.jianshu.com/math?formula=match%2C%EF%BC%89%EF%BC%8C%20%E7%84%B6%E5%90%8E%E5%86%8D%E6%8E%92%E5%BA%8F%EF%BC%88" alt="match,), 然后再排序(" mathimg="1">sort, limit,skip),最后进行其他复杂操作(lookup,project, group,unwind),因为这些操作打散后,完全无法使用索引.
    最佳顺序: match +sort + $limit + ...
  2. 千万别忘了$lookup连表的字段,两张表一定要建立索引
  • 优化案例1:limit提前缩小文档大小,减少内存计算
// 使用时间: 0.044 S
db.getCollection('clients').explain("executionStats").aggregate([ 
    { '$match': { is_deleted: 0 } },
    { '$sort': { gmt_create: -1 } }, 
    { '$lookup': 
        { from: 'client_infos', 
            localField: 'client_info_ids', 
            foreignField: '_id', as: 'client_infos' }
    }, 
    { '$lookup': 
        { from: 'accounts', 
        localField: 'account_id', 
        foreignField: '_id', as: 'account' } 
    }, 
    { '$unwind': '$account' }, 
    { '$skip': 0 }, 
    { '$limit': 10 }], {})

// 使用时间: 0.021s
db.getCollection('clients').explain("executionStats").aggregate([ 
    { '$match': { is_deleted: 0 } },
    { '$sort': { gmt_create: -1 } }, 
    { '$skip': 0 }, 
    { '$limit': 10 },
    { '$lookup': 
        { from: 'client_infos', 
            localField: 'client_info_ids', 
            foreignField: '_id', as: 'client_infos' }
    }, 
    { '$lookup': 
        { from: 'accounts', 
        localField: 'account_id', 
        foreignField: '_id', as: 'account' } 
    }, 
    { '$unwind': '$account' }, 
    ], {})

  • 优化案例2: 转换搜索的主表,使索引生效
// 使用时间: 3.64s
db.getCollection('clients').explain("executionStats").aggregate([ 
    { '$lookup': 
        { from: 'client_infos', 
            localField: 'client_info_ids', 
            foreignField: '_id', as: 'client_infos' }
    }, 
    { '$match': { 'client_infos.phone': 110 } },
    { '$lookup': 
        {
            from: 'accounts', 
            localField: 'account_id', 
            foreignField: '_id', 
            as: 'account',
        }
    }, 
    { '$unwind': '$account' },
    { '$skip': 0 }, 
    { '$limit': 10 }, 
    ], {})

// 使用时间:0.065s
db.getCollection('client_infos').aggregate([ 
    { '$match': { phone: 110, is_deleted: 0} },
    { '$skip': 0 }, 
    { '$limit': 10 }, 
    { '$lookup': 
        { from: 'clients', 
            localField: '_id', 
            foreignField: 'client_info_ids', as: 'clients' }
    }, 
    { '$unwind': '$clients' },
    { '$lookup': 
        { 
            from: 'accounts', 
            localField: 'clients.account_id', 
            foreignField: '_id', 
            as: 'account',
        }
    }, 
    { '$unwind': '$account' },
    ], {})

索引设计原则

  • 索引字段颗粒度越小越好

颗粒度为结果集在原集合中所占的比例
颗粒度小的,例如身份证号等唯一性质的,索引扫描能够很快定位出位置
相反字段颗粒度大的,例如枚举,例如布尔值,索引定位出的位置不够精准,到头来还得大部分扫描,因为多了索引扫描,最后速度可能还不如全盘扫描。

  • 字段更新频率小

索引的缺点之一就是修改时还需要维护索引,所以最好选择字段更新比较小的字段

  • 适当冗余设计

aggregate连表查询。如果查询字段在副表中,就无法使用到索引,如果这种连表查询频率较高,可考虑冗余设计。如上诉案例2,可通过冗余phone的字段提高查询效率,以及增加代码通用性

  • 索引数量控制

一个索引的字段超过7,8,需考虑合理性

查询优化原则

  • 减少带宽

按需取字段,避免返回大字段

  • 减少内存计算

减少中间存储,内存计算

  • 减少磁盘IO

增加索引,避免全盘扫描,优化sql

参考文档:
https://zhuanlan.zhihu.com/p/77971681
https://docs.mongodb.com/manual/core/aggregation-pipeline/#aggregation-pipeline-operators-and-performance
https://docs.mongodb.com/manual/core/aggregation-pipeline-optimization/
https://jira.mongodb.org/browse/SERVER-28140
https://stackoverflow.com/questions/59811200/lookup-wont-use-indexes-in-second-match-how-can-we-scale

作者:IT女神_
链接:http://08643.cn/p/f92919ae6c90

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容

  • 查询优化器 MongoDB的查询计划会将多个索引并行去执行,最先返回101的结果就是胜者,其他查询计划就会被终止,...
    IT女神_阅读 1,574评论 0 1
  • 本文主要记录近期学习 MongoDB 的一些内容,主要参考了官方文档 https://docs.mongodb.c...
    SheHuan阅读 794评论 0 1
  • 1. 介绍、安装、使用(简单写写,不做详细介绍) 1.1 介绍 Mongodb是属于NoSql的一种数据类型; M...
    Grace_ji阅读 1,541评论 0 0
  • 前言:一般查询可以通过find方法,但如果是比较复杂的查询或者数据统计的话,find可能就无能为力了,这时也许你需...
    岑吾阅读 357评论 0 7
  • 聚合操作将多个文档中的值组合在一起并对数据进行各种操作以返回计算结果。MongoDB提供了三种执行聚合的方法:聚合...
    戒糖少盐轻碳水阅读 632评论 0 1