字节面试深挖之mongo

2 篇文章 0 订阅
1 篇文章 0 订阅

前言

前一段时间接到了字节跳动的面试邀请,怀着略有些紧张的心情参与了面试。整体体验下来,面试难度是比较高的。面试官提出的问题需要对你所使用的工具、语言及框架有比较深入的理解才能够从容应答。

本篇着重解析笔者面试过程中被提问到的mongo相关的问题,当然也有其他方面的,但是全部放到一起显得没有重点,笔者会将自己遇到的其他面试题解析在之后陆续更新。

废话不多说,直接来看面试官问了什么吧~

更新

时间内容
2021-01-30跨年更新哈哈哈,更新了问题三

问题一

场景还原

面试官:项目中有用到索引吗?

:有用到的,基本上涉及到数据库的操作都用到了索引,针对一些查询频率比较高的场景,我一般会设置联合索引,减少回表操作…

面试官:嗯?那你们联合索引设置的字段够吗?万一使用联合索引查找的数据没有你需要的字段怎么办?

:啊这…

关于联合索引

首先需要明确一点,使用mongo联合索引和常用关系型数据库(e.g. mysql)联合索引的行为及结果是有很大差异

mongo

使用mongo建立联合索引时,在查询时即使使用到了这个联合索引,也是会直接查询到完整的记录的。

我们来看下mongo使用联合索引查询时的行为(以笔者本地已存在的一个数据库为例)

# 查看 USER_USER 表中的索引
db.getCollection('USER_USER').getIndexes()

返回结果

/* 1 */
[
    {
        "v" : 2,  # 版本号
        "key" : {
            "_id" : 1  # 索引字段(这是mongo的主键索引)
        },
        "name" : "_id_",  # 索引名称
        "ns" : "XM.USER_USER"  # namespace 命名空间
    },
    {
    	# 这是我们待会儿要用的索引
        "v" : 2,
        "unique" : true,  # 是否唯一
        "key" : {
            "orgID" : 1,
            "userName" : 1
        },
        "name" : "orgID_1_userName_1",
        "ns" : "XM.USER_USER",
        "sparse" : false,  # 是否为稀疏索引
        "background" : true  # 是否后台创建
    },
    {
        "v" : 2,
        "unique" : true,
        "key" : {
            "email" : 1,
            "orgID" : 1
        },
        "name" : "email_1_orgID_1",
        "ns" : "XM.USER_USER",
        "sparse" : false,
        "background" : true
    }
]

我们使用索引名称为orgID_1_userName_1的索引进行查询,该索引包含两个字段orgIDuserName,我们使用hint来指定使用的索引

# mongo查询语句
db.getCollection('USER_USER').find(
{"orgID": ObjectId("5f0c132ab0d5ac46d926c2d1"), "userName": "superadmin"}
).hint({"orgID": 1, "userName": 1})

返回结果(数据已脱敏)

/* 1 */
{
    "_id" : ObjectId("5f0c132ab0d5ac46d926c2e4"),
    "orgID" : ObjectId("5f0c132ab0d5ac46d926c2d1"),
    "userName" : "superadmin ",
    "_password" : "xxx",
    "_salt" : "xxx",
    "avatar" : "",
    "content" : "xxx",
    "created_dt" : ISODate("2020-07-13T15:54:18.471Z"),
    "created_id" : null,
    "edition" : 1,
    "email" : "xxxx",
    "mobile" : "",
    "name" : "xxx",
    "record_flag" : 1,
    "roleList" : [],
    "show_introduction" : true,
    "status" : 1,
    "updated_dt" : ISODate("2020-11-27T17:17:08.274Z"),
    "updated_id" : null,
    "wx_openid" : "",
    "wx_unionid" : "",
    "is_accept" : false,
    "is_inner_user" : false,
    "last_login_time" : ISODate("2020-09-16T14:45:40.208Z"),
    "register_ip" : "",
    "wx_nickname" : "",
    "wx_offi_openid" : ""
}

可以看到,全量的数据被导出了,并没有只返回联合索引包含的字段。

我们将这个查询语句explain一下看看

db.getCollection('USER_USER').find({"orgID": ObjectId("5f0c132ab0d5ac46d926c2d1"), "userName": "superadmin"}).hint({"orgID": 1, "userName": 1}).explain()

返回结果

/* 1 */
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "XM.USER_USER",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "$and" : [ 
                {
                    "orgID" : {
                        "$eq" : ObjectId("5f0c132ab0d5ac46d926c2d1")
                    }
                }, 
                {
                    "userName" : {
                        "$eq" : "superadmin"
                    }
                }
            ]
        },
        "queryHash" : "2952A946",
        "planCacheKey" : "53E1F7F6",
        "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "orgID" : 1,
                    "userName" : 1
                },
                "indexName" : "orgID_1_userName_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "orgID" : [],
                    "userName" : []
                },
                "isUnique" : true,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "orgID" : [ 
                        "[ObjectId('5f0c132ab0d5ac46d926c2d1'), ObjectId('5f0c132ab0d5ac46d926c2d1')]"
                    ],
                    "userName" : [ 
                        "[\"superadmin\", \"superadmin\"]"
                    ]
                }
            }
        },
        "rejectedPlans" : []
    },
    "serverInfo" : {
        "host" : "Lcj-MacPro.local",
        "port" : 27017,
        "version" : "4.2.8",
        "gitVersion" : "43d25964249164d76d5e04dd6cf38f6111e21f5f"
    },
    "ok" : 1.0
}

winningPlan属性可以看到,这条mongo查询语句确实使用到了名为orgID_1_userName_1的索引

我们修改一下查询语句再次查询

db.getCollection('USER_USER').find(
    {
        "orgID": ObjectId("5f0c132ab0d5ac46d926c2d1"), 
        "userName": "superadmin",
        "record_flag": 1  # 加了一个新的查询字段进去
    }
).hint({"orgID": 1, "userName": 1})

查询结果就不贴了,和上面的结果是一样的
接着我们explain一下看看

db.getCollection('USER_USER').find(
    {
        "orgID": ObjectId("5f0c132ab0d5ac46d926c2d1"), 
        "userName": "superadmin",
        "record_flag": 1  # 加了一个新的查询字段进去
    }
).hint({"orgID": 1, "userName": 1}).explain()

结果

/* 1 */
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "XM.USER_USER",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "$and" : [ 
                {
                    "orgID" : {
                        "$eq" : ObjectId("5f0c132ab0d5ac46d926c2d1")
                    }
                }, 
                {
                    "record_flag" : {
                        "$eq" : 1.0
                    }
                }, 
                {
                    "userName" : {
                        "$eq" : "superadmin"
                    }
                }
            ]
        },
        "queryHash" : "A27A492A",
        "planCacheKey" : "52AA5881",
        "winningPlan" : {
            "stage" : "FETCH",
            "filter" : {
                "record_flag" : {
                    "$eq" : 1.0
                }
            },
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "orgID" : 1,
                    "userName" : 1
                },
                "indexName" : "orgID_1_userName_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "orgID" : [],
                    "userName" : []
                },
                "isUnique" : true,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "orgID" : [ 
                        "[ObjectId('5f0c132ab0d5ac46d926c2d1'), ObjectId('5f0c132ab0d5ac46d926c2d1')]"
                    ],
                    "userName" : [ 
                        "[\"superadmin\", \"superadmin\"]"
                    ]
                }
            }
        },
        "rejectedPlans" : []
    },
    "serverInfo" : {
        "host" : "Lcj-MacPro.local",
        "port" : 27017,
        "version" : "4.2.8",
        "gitVersion" : "43d25964249164d76d5e04dd6cf38f6111e21f5f"
    },
    "ok" : 1.0
}

winningPlan下可以看到,这条查询仍旧能够使用到索引~

其实在mongo中,添加联合索引的作用是为了让所有数据按照联合索引的字段有序排列,比如一个(a, b)的联合索引,在mongo中对应的数据会先按照a进行升序排列,再按照b进行升序排列。通过类似的索引设置,在查询时,能够避免内存排序(因为他们本身就是有序的),从而大大减少查询时间(内存排序默认的算法为快排,时间复杂度是nlog(n))。

并且,在mongo中,索引并不会和数据放在一起,即没有所谓的"聚簇索引"的概念,索引与数据之间通过RecordId进行引用,即先查索引 --> 找到对应的RecordId --> 找到对应的数据。

关系型数据库(以mysql为例)

同样,我们在mysql中设置类似的联合索引(我已经事先创建了一个表并往里面添加了几条是数据)

# 给score表添加一个名为score_name的索引,该索引包含两个字段:student_name, Math
ALTER TABLE score ADD INDEX score_name(student_name, Math)

目前该表中我们自己设置的索引只有一个,之后分别通过如下sql对score表进行查询(下面简称查询1,查询2

# 在mysql当中,查询语句均为为声明式的,与mongo api式的调用存在很大差异
select student_name, Math from score;  # 查询1,使用到了索引
select student_name, Math,English from score;  # 查询2

首先看下原始数据(瞎造的不要在意细节)
在这里插入图片描述
我们使用查询1的语句进行查找,结果如下
在这里插入图片描述
结果符合预期,结果explain分析一下这条语句

explain select student_name, Math from score;

结果
在这里插入图片描述
关于explain的内容不多做分析,想要详细了解的可以看 这篇博客

从explain结果来看,本次查询确实使用到了名为score_name的索引。

接着执行查询2,结果如下
在这里插入图片描述
同样explain一下
在这里插入图片描述
可以很明显的看出,mysql并没有能够使用到联合索引,而是进行了FULL SCAN,这点和mongo相比是一个很明显的差异。

关于联合索引避免回表

这里笔者曾经犯了一个理解上的错误,认为只要建立了联合索引并正确使用,就能避免回表,但这在mongo中并不适用(原因参考上文描述)。

在类似mysql的关系型数据库当中,如果建立一个联合索引,那么在构成索引的B+树的叶子节点上,能够直接查询到对应的值,但是仅限联合索引中声明过的字段值,比如新建了索引(a, b, c), 想要查询(a, b, c, d)时,刚刚新建的索引就不适用了。

关于mysql联合索引,这篇文章 解释的非常好,笔者就不再重复赘述了。

问题二

场景还原

面试官:如果mongo中需要使用aggregate进行数据的聚合操作,但是由于数据量太大导致内存装不下怎么办?

:可以使用分页,每次只取出聚合的一部分数据;或者简化聚合语句,只聚合需要的字段…balabalabala…

面试官:(沉默一会…)有别的解决方案吗?

: (沉默…)

(最怕,空气突然安静~~~)

mongo中3种聚合手段

针对不同量级及业务场景的数据,mongo提供了3种聚合数据的方案

聚合命令 (group / count / distinct)

相比于其他两种聚合方式,直接使用聚合命令是最简单的,但是灵活度和性能上就要逊色不少。

  • group 可⽤于⼩数据量的⽂档聚合运算,⽤于提供⽐count、distinct更丰富的统计需求,可以使⽤js函数控制统计逻辑。
  • count : db.collection.count() 等同于 db.collection.find().count(), 不能适⽤于分布式环境,分布式环境推荐使⽤ aggregate
  • distinct: 可以使⽤到索引,语法⾮常简单,db.collection.distinct(field,query)field是去重字段(单个或嵌套字段名),query是查询条件

聚合框架 (aggregate)

aggregate 聚合框架是基于数据处理管道(pipeline)模型建⽴,⽂档通过多级管道处理后返回聚合结果;aggregate管道聚合⽅案使⽤的是mongodb内置的汇总操作,相对来说更为⾼效,在做mongodb数据聚合操作的时候优先推荐aggregate;

aggregate可以使用索引和一些管道操作的技巧来提升性能,其调用方式和聚合命令一样为api调用,管道操作类似linux当中的管道 “ | ”,即将上个命令执行的结果作为下条命令的输入,直到最后处理完成返回最终结果。

aggregate的限制

  • aggregate返回的结果集中的单个⽂档超过16MB命令会报错 ( 使⽤aggregate不指定游标选项或存储集合中的结果,aggregate命令会返回⼀个包涵于结果集的字段中的bson⽂件。如果结果集的总⼤⼩超过bson⽂件⼤⼩限制(16MB)该命令将产⽣错误)

  • 管道处理阶段有内存限制最⼤不能超过100MB,超过这个限制会报错误;为了能够处理更⼤的数据集可以开启allowDiskUse选项,可以将管道操作写⼊临时⽂件;aggregate的使⽤场景适⽤于对聚合响应性能需要⼀定要求的场景 (索引及组合优化)

聚合模型 (MapReduce)

MapReduce的强⼤之处在于能够在多台Server上并⾏执⾏复杂的聚合逻辑。

MapReduce是⼀种计算模型,简单的说就是将⼤批量的⼯作 ( 数据 ) 分解 ( MAP ) 执⾏,然后再将结果合并成最终结果(REDUCE)。MapReduce使⽤惯⽤的javascript操作来做mapreduce操作,因此MapReduce的灵活性和复杂性都会⽐aggregate pipeline更⾼⼀些,并且相对aggregate pipeline⽽⾔更消耗性能;MapReduce⼀般情况下会使⽤磁盘存储预处理数据,⽽pipeline始终在内存处理数据。

MapReduce的使⽤场景 使⽤于处理⼤数据结果集,使⽤javascript灵活度⾼的特点,可以处理复杂聚合需求

问题三

场景还原

面试官:有没有可能索引字段建立没有问题的情况下,数据库在查询时仍不会使用索引?

我:喵喵喵??(思维短路,开始想一些奇怪的思路去回答该问题)

什么情况下不会使用到索引

既然面试官已经强调了索引建立的没有问题,那么问题就很有可能出现在查询的阶段,那么什么样的查询会无法使用到索引呢

如果查询语句,索引都没问题,就一定会用索引吗?答案也是需要看情况~

条件查询

  • 使用了 or语句,并且or语句中的筛选字段没有建立索引
    e.g. SELECT * FROM [table_name] where id=[xx] or user_id= [xxx]
    其中,user_id没有建立索引,则会导致查询无法使用索引,解决的方案是尽量少使用or或者为or语句的筛选字段都建立索引。

  • 对于多列索引来说,如果不满足最左匹配原则,则无法使用索引

  • where语句中前使用聚合函数,表达式可能会导致引擎正确的无法使用索引

模糊查询

  • 如果like查询语句是以%开始的,则该列上的索引不会被使用,e.g.
    select * from [table_name] where [key] like '%xxx';
  • where字句的查询条件里有不等于号: where column !=...,或<>操作符,会使得引擎放弃使用索引而进行全表扫描。
  • innot in 使用不当也会导致全表扫描

一些特殊情况:

  • SELECT * FROM xxx
    你想要获取所有数据,即没有筛选边界,这个时候直接扫描全表是最有效率的。
  • 如果MySQL估计使用索引比全表扫描更慢,则不使用索引。例如,如果查询的字段分布较为均匀,使用索引查询的效果就不是太好:e.g. select * from table_name where key>1 and key<90;
  • 如果列为字符串,则where条件中必须将字符常量值加引号,否则即使该列上存在索引,也不会被使用。
    e.g. select * from table_name where key=1;如果key列的值保存的是字符串,即使key上有索引,也不会被使用。
  • 查询引擎对null值的处理效果不尽如人意,where子句中对字段进行 null 值判断(e.g. where mobile = null)不会走索引

这里有一个关于sql查询优化的博客,总结的简洁明了,可以用来做一个简单的了解

关于数据库查询优化器

针对一些特殊醒的情况,Mysql的查询优化器会对查询进行优化,使得查询更加更加有效率。上面提到不使用索引的特殊情况(其实所有sql都会被查询优化器优化)即为查询优化器优化的结果。

更多关于查询优化器的描述,可以在这篇博客中进行了解。本篇文章就不赘述了。

结语

面试官还问了不少问题,笔者从这些问题中受益颇多,本篇着重分析了其中两个,后续有空了将会更新其他面试题的分析。

面试当中遇到的问题,很多往往是开发过程当中遇到的实际问题。需要技术累计和经验累积两方面结合才能较好的作答。当遇到不会的问题,应该庆幸,因为以后遇到类似的场景时,可以少走很多弯路。

参考

你真的会用索引么?

MongoDB索引原理

mongo事务模型分析

mongo实时聚合千万文档

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值