文章目录
前言
前一段时间接到了字节跳动的面试邀请,怀着略有些紧张的心情参与了面试。整体体验下来,面试难度是比较高的。面试官提出的问题需要对你所使用的工具、语言及框架有比较深入的理解才能够从容应答。
本篇着重解析笔者面试过程
中被提问到的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
的索引进行查询,该索引包含两个字段orgID
、userName
,我们使用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
操作来做map
和reduce
操作,因此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 !=...
,或<
、>
操作符,会使得引擎放弃使用索引而进行全表扫描。in
、not 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都会被查询优化器优化)即为查询优化器优化的结果。
更多关于查询优化器的描述,可以在这篇博客中进行了解。本篇文章就不赘述了。
结语
面试官还问了不少问题,笔者从这些问题中受益颇多,本篇着重分析了其中两个,后续有空了将会更新其他面试题的分析。
面试当中遇到的问题,很多往往是开发过程当中遇到的实际问题。需要技术累计和经验累积两方面结合才能较好的作答。当遇到不会的问题,应该庆幸,因为以后遇到类似的场景时,可以少走很多弯路。