mongodb源码分析(六)查询3之mongod的cursor的产生
2012-12-05 16:22
393 查看
上一篇文章分析了mongod的数据库加载部分,下面这一篇文章将继续分析mongod cursor的产生,这里cursor
的生成应该是mongodb系统中最复杂的部分.下面先介绍几个关于mongodb的游标概念.
basicCursor: 直接扫描整个collection的游标,可设置初始的扫描位置,扫描为顺序扫描.
ReverseCursor: 反向扫描游标,相对于顺序扫描,这里是反向扫描.
ReverseCappedCursor: cap集合的反向扫描游标.
ForwardCappedCursor: cap集合的顺序扫描游标.
GeoCursorBase: 空间地理索引游标的基类,我并未阅读相关代码,感兴趣的自己研究吧.
BtreeCursor: mongodb的一般数据索引扫描游标,这个游标完成对于索引的扫描.
MultiCursor: 有待研究.
QueryOptimizerCursor: 经过优化的扫描游标,多plan扫描时或者对于查询中有$or的语句且$or语句其作用时由于
优化查询的游标. 这里将简单描述其流程.
1. 如果是类似这种db.coll.find()的查询则将直接返回一个BasicCursor的游标扫描全表.
2. 如果是简单的id查询如db.coll.find(_id:xxx),且允许_id查询plan的情况下直接查询_id索引,返回一个_id索引
的BtreeCursor.
3.根据查询整理出查询值的范围,作为优化查询范围的依据,如:db.coll.find({x:{$lt:100,$gt:20}}),那么这里其范围就
是[20.100],这个范围只有在对应的变量是索引的情况下起作用,如x为其中的一个索引,那么这里的范围将帮助其
游标BtreeCursor首先直接将查询范围定位到[20,100]的位置,这个工作对于由Btree组织的索引来说很简单.简单
来说就是优化查询范围.但是若x不是索引那么这里得到的查询范围将是无用的,这里将返回一个BasicCursor的
游标执行全表扫描.
4.根据得到的所有的查询域的范围比如说x:[10,20],y:[4,6]这种选取查询计划(QueryPlan).查询计划的选取这里举个
例子,有x,y两个查询域.index有{x:1},{y:1},{x:1,y:1}这几个索引,那么选取计划时发现只有索引{x:1,y:1}完全满足查
询计划,其是最优的,那么确定选取这个索引为查询索引.返回唯一的QueryPlan,最后生成一个确切的
BtreeCursor.但是如果没有{x:1,y:1}这个索引怎么办呢?那么剩下两个索引{x:1},{y:1}都部分包含了查询域,他们
都是有作用的,于是乎生成了两个QueryPlan,一个对应于索引{x:1},一个对应于索引{y:1},于是乎使用
QueryOptimizerCursor这个cursor管理两个BtreeCursor,每次交替执行两个BtreeCursor的查询,直到一个
BtreeCursor查询完毕,那么这个plan是所有plan中最优的,将其缓存起来,下一次同样查询时直接选择这个plan作
为查询的plan.因为两个plan中首先完成扫描的plan查询的次数最少.那么两个plan都查询到的同一条满足查询要
求的数据怎么办,查询结尾会有一个对于满足要求的document地址的记录,如果一条满足要求的document的地址
已经在记录中了,就不再记录这个document.
5.$or查询的优化,对于一个$or举例来说明:{$or:[{x:1},{y:2},{z:3,a:4}]}这样的查询请求,这样要当$or中的每一个查
询域,中至少一个域是可用的索引比如说有索引x,y,a那么这个$or才是有意义的.如果这个$or有意义,那么这里将
使用QueryOptimizerCursor来处理每一个$or中的查询域,比如说{x:1},然后这里退化到4,plan的选取,$or中的查
询一个一个的执行.回过头来分析,如果没有索引y,那么对于这个$or的查询因为满足y:2的文档将会被返回,那么
只能扫描全表,这时即使有索引x,z或者a这种也不能避免全表的扫描,那么这里的$or就变得没有优化的意义了.
另外如果查询指定了sort(xxx:1)按照某些域排序或者设定了最大值最小值$or也是无意义的.
6. 查询结束后的排序,当指定了如db.coll.find({x:1,y:2}).sort(z:1),这种需要按照z升序排列的查询时,这种情况就要
考虑当没有索引z时,那么排序是肯定避免不了的,查询的结果会放到一个map中,map按照z的升序来排序,当排序
的文档总大小超过了默认热32M最大值时会返回错误,提醒你应该为z域建立索引了.下面来看有索引时的状况.
(1),索引为{x:1},{z:1},如果这里两个索引查询的文档数目一样多,那么优先选择{x:1},因为建立索引时其比较靠前,然
后还是得排序.
(2)索引{x:1,z:1},{z:1,x:1},由于第一个索引查出来的顺序是按照x的顺序来排列的,那么还是得排序,第二个索引不需
要排序,但是考虑最优QueryPlan的选取是找到最先执行完plan的索引,这里仍然不会选取{z:1,x:1}这个plan,而
是会选取{x:1,z:1}这个plan.考虑到两个索引还不直观,这里加入一个{x:1},{x:1,z:1},{z:1,x:1},那么这里将会选择第
一个索引{x:1}.要让mongod选择{z:1,x:1}这plan只能使用db.coll.find({x:{$lt:5,$gt:0}).sort({z:1}).hint({z:1,x:1}),
总觉得这是一个bug,mongod应该能够修正这种情况才对,应该能自己选择最优的索引{z:1,x:1}才对.这里有一篇
10gen的工程师谈mongodb索引优化的文章可以一看:
http://www.csdn.net/article/2012-11-09/2811690-optimizing-mongodb-compound
上面介绍了那么多的流程情况下面正式进入代码分析阶段.接上篇文章runQuery->queryWithQueryOptimizer
继续来看游标的产生:
NamespaceDetailsTransient::getCursor->CursorGenerator::generate
索引如:db.coll.ensureIndex({x:1})时,当不存在x:[xx,xxx,xxxx]这种数据时那么这个索引x就是单值索引,当插入一条数据中
包括了x:[xx,xxx,xxx]这种array结构的x时,x变为多值索引.多值索引简单来说就是就是对于array中的每一个之建立一个索引.
继续前进到_singleKey对象的构造函数:
回到MultiPlanScanner::init继续前进看看:QueryPlanSet::make函数.
在继续之前这里需要说明的是mongodb的plan分5种:
时会产生一个空的range,进而产生完全无法匹配的状况.
Optimal: FieldRangeSetPair中每一个域都在索引中,这是一个最优的索引,根据这个索引产生的plan
将是最优的,不需要再考虑其它plan了.
Helpful: 选择的索引能够覆盖FieldRangeSetPair中的部分域,这个索引是有用的,虽然可能会多搜索
一些不会匹配其它域的document.在没有Optimal索引的情况下会根据Helpful索引建立plan
有多个Helpful的索引将建立多plan.
Unhelpful:无用的索引,不会考虑,似乎和Impossible差不多.
Disallowed: 如果使用这个索引查询数据可能会出错,这里有一个sparse的概念.mongodb的普通索引
是会索引无关数据的,举例来说有索引{x:1},插入一条数据{y:10},那么索引也会把这条数据
索引了,但是建立sparse索引db.xxx.ensureIndex({x:1},{sparse:true})那么这里的索引将
不再索引{y:10}这条数据了.对于sparse索引并且存在类似{z:{$exist:false}}这种情况,那么
使用该索引结果可能是不正确的不考虑该索引.
下面继续看代码:
继续来看看newPlan函数,这个函数包括了一个plan的构造.其同样是new一个QueryPlan然后调用其init函数:
条件就能够达到要求找到最优的plan{y:1,x:1},为什么不选择这个plan呢,难道说考虑到要插入这种
{y:40}这种数据吗,虽然{x:1}这种索引没有y域但是其还是会对这个{y:40}数据加入索引啊,数目并不会
比{y:1,x:1}这个索引的数目多啊,而且{y:1,x:1},但是后来我发现我忽略了一个问题,索引{y:1,x:1}无法直接定
位到x的范围,那么查询的无关的document数目可能比{x:1}这个索引查询的数目多,对于mongodb优化考
虑的是如何得到最少的document扫描数目,所以{y:1,x:1}也只能是一个可考虑的索引而无法成为最优的
索引,所以要想让查询使用这个索引只能使用hint了.
这篇文件就暂时写到这里,后面还有很多内容,一篇文章写下来太多,还是分成两篇文章吧,关于plan的
选取请看下一篇文章.
本文链接:/article/8409343.html
作者:yhjj0108,杨浩
的生成应该是mongodb系统中最复杂的部分.下面先介绍几个关于mongodb的游标概念.
basicCursor: 直接扫描整个collection的游标,可设置初始的扫描位置,扫描为顺序扫描.
ReverseCursor: 反向扫描游标,相对于顺序扫描,这里是反向扫描.
ReverseCappedCursor: cap集合的反向扫描游标.
ForwardCappedCursor: cap集合的顺序扫描游标.
GeoCursorBase: 空间地理索引游标的基类,我并未阅读相关代码,感兴趣的自己研究吧.
BtreeCursor: mongodb的一般数据索引扫描游标,这个游标完成对于索引的扫描.
MultiCursor: 有待研究.
QueryOptimizerCursor: 经过优化的扫描游标,多plan扫描时或者对于查询中有$or的语句且$or语句其作用时由于
优化查询的游标. 这里将简单描述其流程.
1. 如果是类似这种db.coll.find()的查询则将直接返回一个BasicCursor的游标扫描全表.
2. 如果是简单的id查询如db.coll.find(_id:xxx),且允许_id查询plan的情况下直接查询_id索引,返回一个_id索引
的BtreeCursor.
3.根据查询整理出查询值的范围,作为优化查询范围的依据,如:db.coll.find({x:{$lt:100,$gt:20}}),那么这里其范围就
是[20.100],这个范围只有在对应的变量是索引的情况下起作用,如x为其中的一个索引,那么这里的范围将帮助其
游标BtreeCursor首先直接将查询范围定位到[20,100]的位置,这个工作对于由Btree组织的索引来说很简单.简单
来说就是优化查询范围.但是若x不是索引那么这里得到的查询范围将是无用的,这里将返回一个BasicCursor的
游标执行全表扫描.
4.根据得到的所有的查询域的范围比如说x:[10,20],y:[4,6]这种选取查询计划(QueryPlan).查询计划的选取这里举个
例子,有x,y两个查询域.index有{x:1},{y:1},{x:1,y:1}这几个索引,那么选取计划时发现只有索引{x:1,y:1}完全满足查
询计划,其是最优的,那么确定选取这个索引为查询索引.返回唯一的QueryPlan,最后生成一个确切的
BtreeCursor.但是如果没有{x:1,y:1}这个索引怎么办呢?那么剩下两个索引{x:1},{y:1}都部分包含了查询域,他们
都是有作用的,于是乎生成了两个QueryPlan,一个对应于索引{x:1},一个对应于索引{y:1},于是乎使用
QueryOptimizerCursor这个cursor管理两个BtreeCursor,每次交替执行两个BtreeCursor的查询,直到一个
BtreeCursor查询完毕,那么这个plan是所有plan中最优的,将其缓存起来,下一次同样查询时直接选择这个plan作
为查询的plan.因为两个plan中首先完成扫描的plan查询的次数最少.那么两个plan都查询到的同一条满足查询要
求的数据怎么办,查询结尾会有一个对于满足要求的document地址的记录,如果一条满足要求的document的地址
已经在记录中了,就不再记录这个document.
5.$or查询的优化,对于一个$or举例来说明:{$or:[{x:1},{y:2},{z:3,a:4}]}这样的查询请求,这样要当$or中的每一个查
询域,中至少一个域是可用的索引比如说有索引x,y,a那么这个$or才是有意义的.如果这个$or有意义,那么这里将
使用QueryOptimizerCursor来处理每一个$or中的查询域,比如说{x:1},然后这里退化到4,plan的选取,$or中的查
询一个一个的执行.回过头来分析,如果没有索引y,那么对于这个$or的查询因为满足y:2的文档将会被返回,那么
只能扫描全表,这时即使有索引x,z或者a这种也不能避免全表的扫描,那么这里的$or就变得没有优化的意义了.
另外如果查询指定了sort(xxx:1)按照某些域排序或者设定了最大值最小值$or也是无意义的.
6. 查询结束后的排序,当指定了如db.coll.find({x:1,y:2}).sort(z:1),这种需要按照z升序排列的查询时,这种情况就要
考虑当没有索引z时,那么排序是肯定避免不了的,查询的结果会放到一个map中,map按照z的升序来排序,当排序
的文档总大小超过了默认热32M最大值时会返回错误,提醒你应该为z域建立索引了.下面来看有索引时的状况.
(1),索引为{x:1},{z:1},如果这里两个索引查询的文档数目一样多,那么优先选择{x:1},因为建立索引时其比较靠前,然
后还是得排序.
(2)索引{x:1,z:1},{z:1,x:1},由于第一个索引查出来的顺序是按照x的顺序来排列的,那么还是得排序,第二个索引不需
要排序,但是考虑最优QueryPlan的选取是找到最先执行完plan的索引,这里仍然不会选取{z:1,x:1}这个plan,而
是会选取{x:1,z:1}这个plan.考虑到两个索引还不直观,这里加入一个{x:1},{x:1,z:1},{z:1,x:1},那么这里将会选择第
一个索引{x:1}.要让mongod选择{z:1,x:1}这plan只能使用db.coll.find({x:{$lt:5,$gt:0}).sort({z:1}).hint({z:1,x:1}),
总觉得这是一个bug,mongod应该能够修正这种情况才对,应该能自己选择最优的索引{z:1,x:1}才对.这里有一篇
10gen的工程师谈mongodb索引优化的文章可以一看:
http://www.csdn.net/article/2012-11-09/2811690-optimizing-mongodb-compound
上面介绍了那么多的流程情况下面正式进入代码分析阶段.接上篇文章runQuery->queryWithQueryOptimizer
string queryWithQueryOptimizer( int queryOptions, const string& ns, const BSONObj &jsobj, CurOp& curop, const BSONObj &query, const BSONObj &order, const shared_ptr<ParsedQuery> &pq_shared, const BSONObj &oldPlan, const ConfigVersion &shardingVersionAtStart, scoped_ptr<PageFaultRetryableSection>& parentPageFaultSection, scoped_ptr<NoPageFaultsAllowed>& noPageFault, Message &result ) { const ParsedQuery &pq( *pq_shared ); shared_ptr<Cursor> cursor; QueryPlanSummary queryPlan; if ( pq.hasOption( QueryOption_OplogReplay ) ) {//用于oplog的回放的游标. cursor = FindingStartCursor::getCursor( ns.c_str(), query, order ); } else { cursor =//本文的主要分析的部分,游标的获取 NamespaceDetailsTransient::getCursor( ns.c_str(), query, order, QueryPlanSelectionPolicy::any(), 0, pq_shared, false, &queryPlan ); } scoped_ptr<QueryResponseBuilder> queryResponseBuilder ( QueryResponseBuilder::make( pq, cursor, queryPlan, oldPlan ) ); bool saveClientCursor = false; OpTime slaveReadTill; ClientCursor::Holder ccPointer( new ClientCursor( QueryOption_NoCursorTimeout, cursor, ns ) ); for( ; cursor->ok(); cursor->advance() ) { bool yielded = false;//这里的查询机制,当查询时间超过了一个给定的值,这里为10ms或者在一段时间内调用该函数超过了128次,又或者cursor指向的文档不在内存中,那么这里将睡眠一会儿,睡眠的时间由当前系统中读取读者数目r和写者数目w,由10*r+w决定,单位为ms,最大值不超过1000000. if ( !ccPointer->yieldSometimes( ClientCursor::MaybeCovered, &yielded ) ||//睡眠前会通知游标保存当前位置 !cursor->ok() ) {//这里是睡眠完成后发现当前游标失效了 cursor.reset(); queryResponseBuilder->noteYield(); // !!! TODO The queryResponseBuilder still holds cursor. Currently it will not do // anything unsafe with the cursor in handoff(), but this is very fragile. // // We don't fail the query since we're fine with returning partial data if the // collection was dropped. // NOTE see SERVER-2454. // TODO This is wrong. The cursor could be gone if the closeAllDatabases command // just ran. break; } if ( yielded ) {//发生过yield,这是由两种情况构成,要么关心的数据不在内存,要么 queryResponseBuilder->noteYield();//clientCursor超过了ccPointer->_yieldSometimesTracker规定的yield时间 } if ( pq.getMaxScan() && cursor->nscanned() > pq.getMaxScan() ) {//超过了用户查询时设置的最大的扫描扫描数目 break; } if ( !queryResponseBuilder->addMatch() ) {//具体查询的文档的匹配过程,下一篇文章将介绍 continue; } // Note slave's position in the oplog. if ( pq.hasOption( QueryOption_OplogReplay ) ) { BSONObj current = cursor->current(); BSONElement e = current["ts"]; if ( e.type() == Date || e.type() == Timestamp ) { slaveReadTill = e._opTime(); } } if ( !cursor->supportGetMore() || pq.isExplain() ) { if ( queryResponseBuilder->enoughTotalResults() ) { break; } } else if ( queryResponseBuilder->enoughForFirstBatch() ) { // if only 1 requested, no cursor saved for efficiency...we assume it is findOne() if ( pq.wantMore() && pq.getNumToReturn() != 1 ) { queryResponseBuilder->finishedFirstBatch(); if ( cursor->advance() ) { saveClientCursor = true; } } break; } } if ( cursor ) { if ( pq.hasOption( QueryOption_CursorTailable ) && pq.getNumToReturn() != 1 ) { cursor->setTailable(); } // If the tailing request succeeded. if ( cursor->tailable() ) { saveClientCursor = true; } } int nReturned = queryResponseBuilder->handoff( result ); ccPointer.reset(); long long cursorid = 0; if ( saveClientCursor ) {//保存cursor下一次客户端请求调用dbGetmore时直接从这里读出游标 // Create a new ClientCursor, with a default timeout. ccPointer.reset( new ClientCursor( queryOptions, cursor, ns, jsobj.getOwned() ) ); cursorid = ccPointer->cursorid(); DEV tlog(2) << "query has more, cursorid: " << cursorid << endl; if ( cursor->supportYields() ) { ClientCursor::YieldData data; ccPointer->prepareToYield( data ); } else { ccPointer->c()->noteLocation(); } // Save slave's position in the oplog. if ( pq.hasOption( QueryOption_OplogReplay ) && !slaveReadTill.isNull() ) { ccPointer->slaveReadTill( slaveReadTill ); } if ( !ccPointer->ok() && ccPointer->c()->tailable() ) { DEV tlog() << "query has no more but tailable, cursorid: " << cursorid << endl; } if( queryOptions & QueryOption_Exhaust ) { curop.debug().exhaust = true; } // Set attributes for getMore. ccPointer->setChunkManager( queryResponseBuilder->chunkManager() ); ccPointer->setPos( nReturned ); ccPointer->pq = pq_shared; ccPointer->fields = pq.getFieldPtr(); ccPointer.release(); }//返回结果集 QueryResult *qr = (QueryResult *) result.header(); qr->cursorId = cursorid; curop.debug().cursorid = ( cursorid == 0 ? -1 : qr->cursorId ); qr->setResultFlagsToOk(); // qr->len is updated automatically by appendData() curop.debug().responseLength = qr->len; qr->setOperation(opReply); qr->startingFrom = 0; qr->nReturned = nReturned; int duration = curop.elapsedMillis(); bool dbprofile = curop.shouldDBProfile( duration );//记录查询命令 if ( dbprofile || duration >= cmdLine.slowMS ) { curop.debug().nscanned = ( cursor ? cursor->nscanned() : 0LL ); curop.debug().ntoskip = pq.getSkip(); } curop.debug().nreturned = nReturned; return curop.debug().exhaust ? ns : ""; }
继续来看游标的产生:
NamespaceDetailsTransient::getCursor->CursorGenerator::generate
shared_ptr<Cursor> CursorGenerator::generate() { setArgumentsHint();//设置查询使用的索引,来自于db.coll.find({x:1}).hint({xx:1})的hint函数.mongodb提供一个snapshot的选项,当设置了snapshot时强制使用索引_id.这里有一篇文章介绍snapshot的特性:/article/5515845.html shared_ptr<Cursor> cursor = shortcutCursor();//这里要么查询是空的且排序是空的,则返回一个集合扫描的Cursor,要么是按照简单的_id查询 if ( cursor ) { //返回一个_id索引的BtreeCursor. return cursor; } setMultiPlanScanner();//创建查询范围,生成plan. cursor = singlePlanCursor()//如果只有单个plan生成则根据plan生成对应的cursor if ( cursor ) { return cursor; }//多plan优化 return newQueryOptimizerCursor( _mps, _planPolicy, isOrderRequired(), explain() ); }继续前进看看setMultiPlanScanner:
void CursorGenerator::setMultiPlanScanner() {//基本这里所有的make都干两件事,1是分配对应的对象,二是调用其初始化函数init初始化 _mps.reset( MultiPlanScanner::make( _ns, _query, _order, _parsedQuery, hint(), explain() ? QueryPlanGenerator::Ignore : QueryPlanGenerator::Use, min(), max() ) ); }这里跳过分配对象的new操作直接进入init函数:
void MultiPlanScanner::init( const BSONObj &order, const BSONObj &min, const BSONObj &max ) { if ( !order.isEmpty() || !min.isEmpty() || !max.isEmpty() ) { _or = false;//_or是根据传入的query中是否有$or标志设置的 } if ( _or ) {//这里的OrRangeGenerator和下面的FieldRangeSetPair其实都是用来确定查询域的范围的,通过这个范围来直接定位Btree的位置,跳过不必要的btree数据对象扫描,当没有相应的索引时,这里建立的查询域范围将是无用的. // Only construct an OrRangeGenerator if we may handle $or clauses. _org.reset( new OrRangeGenerator( _ns.c_str(), _query ) ); if ( !_org->getSpecial().empty() ) { _or = false; } else if ( haveUselessOr() ) {//对于如{$or:[{x:1},{y:1}]},这里要存在index:x,y时_or才不会被设置_or=false,前面描述过程时说到过 _or = false; } } // if _or == false, don't use or clauses for index selection if ( !_or ) { ++_i;//若query为{$or:[{a:1},{:1}]}这种以or开头的语句,那么frsp是无用的,具体见FieldRangeSet::handleMatchField auto_ptr<FieldRangeSetPair> frsp( new FieldRangeSetPair( _ns.c_str(), _query, true ) ); updateCurrentQps( QueryPlanSet::make( _ns.c_str(), frsp, auto_ptr<FieldRangeSetPair>(), _query, order, _parsedQuery, _hint, _recordedPlanPolicy, min, max, true ) ); } else { BSONElement e = _query.getField( "$or" ); massert( 13268, "invalid $or spec", e.type() == Array && e.embeddedObject().nFields() > 0 ); handleBeginningOfClause(); } }
OrRangeGenerator::OrRangeGenerator( const char *ns, const BSONObj &query , bool optimize ) : _baseSet( ns, query, optimize ), _orFound() { BSONObjIterator i( _baseSet.originalQuery() ); //取出所有的$or开始的对象,分别创建一个FieldRangeSetPair while( i.more() ) {//just like{$or:[{a:1},{b:1}], $or[{c:1},{d:1}]....} BSONElement e = i.next(); if ( strcmp( e.fieldName(), "$or" ) == 0 ) { uassert( 13262, "$or requires nonempty array", e.type() == Array && e.embeddedObject().nFields() > 0 ); BSONObjIterator j( e.embeddedObject() ); while( j.more() ) { BSONElement f = j.next(); uassert( 13263, "$or array must contain objects", f.type() == Object ); _orSets.push_back( FieldRangeSetPair( ns, f.embeddedObject(), optimize ) ); uassert( 13291, "$or may not contain 'special' query", _orSets.back().getSpecial().empty() ); _originalOrSets.push_back( _orSets.back() ); } _orFound = true; continue; } } }继续看FieldRangeSetPair:
FieldRangeSetPair( const char *ns, const BSONObj &query, bool optimize=true ) :_singleKey( ns, query, true, optimize ), _multiKey( ns, query, false, optimize ) {}_singleKey对应于单索引,_multikey对应于多值索引.mongodb提供一个multiKey的索引,简单来说就是当创建一个
索引如:db.coll.ensureIndex({x:1})时,当不存在x:[xx,xxx,xxxx]这种数据时那么这个索引x就是单值索引,当插入一条数据中
包括了x:[xx,xxx,xxx]这种array结构的x时,x变为多值索引.多值索引简单来说就是就是对于array中的每一个之建立一个索引.
继续前进到_singleKey对象的构造函数:
FieldRangeSet::FieldRangeSet( const char *ns, const BSONObj &query, bool singleKey, bool optimize ) : _ns( ns ), _queries( 1, query.getOwned() ), _singleKey( singleKey ), _exactMatchRepresentation( true ), _boundElemMatch( true ) { init( optimize ); }
void FieldRangeSet::init( bool optimize ) { BSONObjIterator i( _queries[ 0 ] ); while( i.more() ) { handleMatchField( i.next(), optimize ); } }
void FieldRangeSet::handleMatchField( const BSONElement& matchElement, bool optimize ) { const char* matchFieldName = matchElement.fieldName(); if ( matchFieldName[ 0 ] == '$' ) { if ( str::equals( matchFieldName, "$and" ) ) {//$and对象对于$and中的每一个查询调用handleMatchField递归处理 uassert( 14816, "$and expression must be a nonempty array", matchElement.type() == Array && matchElement.embeddedObject().nFields() > 0 ); handleConjunctionClauses( matchElement.embeddedObject(), optimize ); return; } adjustMatchField(); if ( str::equals( matchFieldName, "$or" ) ) {//这里出现了这种形式:$or:[{xxx:1}],只有一个分支,也是调用 // Check for a singleton $or expression. //handleMatchField递归处理 if ( matchElement.type() == Array && matchElement.embeddedObject().nFields() == 1 ) { // Compute field bounds for a singleton $or expression as if it is a $and // expression. With only one clause, the matching semantics are the same. // SERVER-6416 handleConjunctionClauses( matchElement.embeddedObject(), optimize ); } return; } if ( str::equals( matchFieldName, "$nor" ) ) { return; } if ( str::equals( matchFieldName, "$where" ) ) { return; } } //just like {x: 1} or {x : {y : 1, z : 2}} bool equality = // Check for a parsable '$' operator within a match element, indicating the object // should not be matched as is but parsed. // NOTE This only checks for a '$' prefix in the first embedded field whereas Matcher // checks all embedded fields. ( getGtLtOp( matchElement ) == BSONObj::Equality ) && // Similarly check for the $not meta operator. !( matchElement.type() == Object &&//这里对于intersectMatchField以及其内部的内容就不再做分析了,写出来太多 str::equals( matchElement.embeddedObject().firstElementFieldName(), "$not" ) ); if ( equality ) {//这里将建立一个matchFieldName的FieldRange结构,然后和之前可能存在的这个域的FieldRange结构做运算 intersectMatchField( matchFieldName, matchElement, false, optimize );//得到新的matchFieldName的范围 return; } bool untypedRegex = ( matchElement.type() == Object ) &&//like {x: {$regex: /acme.*corp/i, $nin: ['acmeblahcorp']}} matchElement.embeddedObject().hasField( "$regex" );//like {x:{$regex: 'acme.*corp', $options:'i'}} if ( untypedRegex ) { // $regex/$options pairs must be handled together and so are passed via the // element encapsulating them. intersectMatchField( matchFieldName, matchElement, false, optimize ); // Other elements may remain to be handled, below. }//这里是处理类似{x:{$elemMatch:{y:1,z:2}}},{x:{$all:[1,2,3]}}这种查询语句 BSONObjIterator matchExpressionIterator( matchElement.embeddedObject() ); while( matchExpressionIterator.more() ) { BSONElement opElement = matchExpressionIterator.next(); if ( str::equals( opElement.fieldName(), "$not" ) ) { handleNotOp( matchFieldName, opElement, optimize ); } else { handleOp( matchFieldName, opElement, false, optimize ); } } }
void FieldRangeSet::handleOp( const char* matchFieldName, const BSONElement& op, bool isNot, bool optimize ) { int opType = op.getGtLtOp(); // If the first $all element's first op is an $elemMatch, generate bounds for it // and ignore the remaining $all elements. SERVER-664 if ( opType == BSONObj::opALL ) {//类似这种{x:{$all:[{$elemMatch:{k:1,f:1}},{x:1},{z:1}]}},则这里只处理其中的第一个element uassert( 13050, "$all requires array", op.type() == Array ); BSONElement firstAllClause = op.embeddedObject().firstElement(); if ( firstAllClause.type() == Object ) { BSONElement firstAllClauseOp = firstAllClause.embeddedObject().firstElement(); if ( firstAllClauseOp.getGtLtOp() == BSONObj::opELEM_MATCH ) { handleElemMatch( matchFieldName, firstAllClauseOp, isNot, optimize ); return; } } }//不再深入到HandleElemMatch函数内部,简单说一下,对于{$elemMatch:{k:1,y:2}}这种语句就是再建立一个FieldRangeSet并对其内部 if ( opType == BSONObj::opELEM_MATCH ) {//的{k:1,y:2}做处理,得到的FieldRangeSet与当前的FieldRangeSet做与运算,得到的结果 handleElemMatch( matchFieldName, op, isNot, optimize );//保存到当前FieldRangeSet中 } else { intersectMatchField( matchFieldName, op, isNot, optimize ); } }
回到MultiPlanScanner::init继续前进看看:QueryPlanSet::make函数.
auto_ptr<FieldRangeSetPair> frsp( new FieldRangeSetPair( _ns.c_str(), _query, true ) ); updateCurrentQps( QueryPlanSet::make( _ns.c_str(), frsp, auto_ptr<FieldRangeSetPair>(), _query, order, _parsedQuery, _hint, _recordedPlanPolicy, min, max, true ) );签名说过make函数是new一个QueryPlanSet并且调用其init函数继续看init函数:
void QueryPlanSet::init() { DEBUGQO( "QueryPlanSet::init " << ns << "\t" << _originalQuery ); _plans.clear();//清空plans,这里将是plan的选取 _usingCachedPlan = false; _generator.addInitialPlans(); }
void QueryPlanGenerator::addInitialPlans() { const char *ns = _qps.frsp().ns(); NamespaceDetails *d = nsdetails( ns ); if ( addShortCircuitPlan( d ) ) {//这里直接选择单个plan,下面看看这里添加的plan都是什么状况 return; } addStandardPlans( d );//根据索引实际添加的plan warnOnCappedIdTableScan(); }QueryPlanGenerator::addInitialPlans->QueryPlanGenerator::addShortCircuitPlan:
bool QueryPlanGenerator::addShortCircuitPlan( NamespaceDetails *d ) { return//1 集合不存在,2 不可能有match的索引,3 hint指定选择索引的plan, 4使用特殊索引的plan如: // The collection is missing.//空间地理索引,5无法指定范围并且排序为空的plan,6指定排序 setUnindexedPlanIf( !d, d ) ||//不为空为$natural(这个是按照插入顺序排序的要求)的plan // No match is possible.//这几种情况下选择的plan都是一定的,不存在多plan的情况 setUnindexedPlanIf( !_qps.frsp().matchPossible(), d ) || // The hint, min, or max parameters are specified. addHintPlan( d ) || // A special index operation is requested. yhjj0108 add -- maybe for special index 2d and so on addSpecialPlan( d ) || // No indexable ranges or ordering are specified. setUnindexedPlanIf( _qps.frsp().noNonUniversalRanges() && _qps.order().isEmpty(), d ) || // $natural sort is requested. setUnindexedPlanIf( !_qps.order().isEmpty() && str::equals( _qps.order().firstElementFieldName(), "$natural" ), d ); }继续QueryPlanGenerator::addInitialPlans->QueryPlanGenerator::addStandardPlans:
void QueryPlanGenerator::addStandardPlans( NamespaceDetails *d ) { if ( !addCachedPlan( d ) ) {//plan已经被缓存了,表示执行过一次该查询以上,上一次已经找出了最优的 addFallbackPlans();//plan,这一次直接取出最优的plan就行了. } }QueryPlanGenerator::addInitialPlans->QueryPlanGenerator::addStandardPlans->QueryPlanGenerator::addFallbackPlans
在继续之前这里需要说明的是mongodb的plan分5种:
enum Utility { Impossible, // Cannot produce any matches, so the query must have an empty result set. // No other plans need to be considered. Optimal, // Should run as the only candidate plan in the absence of an Impossible // plan. Helpful, // Should be considered. Unhelpful, // Should not be considered. Disallowed // Must not be considered unless explicitly hinted. May produce a // semantically incorrect result set. };Impossible:完全无法匹配的,如查询是db.coll.find({x:{$lt:5,$gt:10}})这种产生的FieldRangeSetPair中的域
时会产生一个空的range,进而产生完全无法匹配的状况.
Optimal: FieldRangeSetPair中每一个域都在索引中,这是一个最优的索引,根据这个索引产生的plan
将是最优的,不需要再考虑其它plan了.
Helpful: 选择的索引能够覆盖FieldRangeSetPair中的部分域,这个索引是有用的,虽然可能会多搜索
一些不会匹配其它域的document.在没有Optimal索引的情况下会根据Helpful索引建立plan
有多个Helpful的索引将建立多plan.
Unhelpful:无用的索引,不会考虑,似乎和Impossible差不多.
Disallowed: 如果使用这个索引查询数据可能会出错,这里有一个sparse的概念.mongodb的普通索引
是会索引无关数据的,举例来说有索引{x:1},插入一条数据{y:10},那么索引也会把这条数据
索引了,但是建立sparse索引db.xxx.ensureIndex({x:1},{sparse:true})那么这里的索引将
不再索引{y:10}这条数据了.对于sparse索引并且存在类似{z:{$exist:false}}这种情况,那么
使用该索引结果可能是不正确的不考虑该索引.
下面继续看代码:
void QueryPlanGenerator::addFallbackPlans() { const char *ns = _qps.frsp().ns(); NamespaceDetails *d = nsdetails( ns ); vector<shared_ptr<QueryPlan> > plans; shared_ptr<QueryPlan> optimalPlan; shared_ptr<QueryPlan> specialPlan; for( int i = 0; i < d->nIndexes; ++i ) {//遍历所有索引,找出有用的索引,indexUseful指只要索引中 if ( !QueryUtilIndexed::indexUseful( _qps.frsp(), d, i, _qps.order() ) ) {//有一个域覆盖了 continue;//查询条件或者排序条件那么这个索引就是有用的 }//根据索引建立一个plan,通过建立的plan得出其是否是有用的 shared_ptr<QueryPlan> p = newPlan( d, i ); switch( p->utility() ) { case QueryPlan::Impossible://后面将会看到对于这个plan若只存在其 _qps.setSinglePlan( p );//那么将建立一个有0个文档的cursor return; case QueryPlan::Optimal://最优的plan,有则肯定选择它 if ( !optimalPlan ) { optimalPlan = p; } break; case QueryPlan::Helpful://这个plan是有帮助的记录其 if ( p->special().empty() ) { // Not a 'special' plan. plans.push_back( p ); } else if ( _allowSpecial ) {//类似空间地理索引这种索引插件产生的索引plan specialPlan = p; } break; default: break; } } if ( optimalPlan ) {//最优的plan,有人肯呢个会问如果存在impossible的plan后那么这里的 _qps.setSinglePlan( optimalPlan );//setSinglePlan会插入不进去,其实不用担心,impossible表示完全无法匹配如:y>10 and y<3这种情况,那么任意的plan都无法匹配,自然无法产生optimalPlan了. // Record an optimal plan in the query cache immediately, with a small nscanned value // that will be ignored. optimalPlan->registerSelf//将其注册为最优的plan,以后可以直接使用这个plan而不用比对哪个plan最优了 ( 0, CandidatePlanCharacter( !optimalPlan->scanAndOrderRequired(), optimalPlan->scanAndOrderRequired() ) ); return; } // Only add a special plan if no standard btree plans have been added. SERVER-4531 if ( plans.empty() && specialPlan ) { _qps.setSinglePlan( specialPlan ); return; } //对于这种db.coll.find({x:1,y:1}),存在着索引{key:{x:1}},{key:{y:1}},两者都不是最优的 //所以这里产生了两个QueryPlan,分别是{key:{x:1}}和{key:{y:1}} for( vector<shared_ptr<QueryPlan> >::const_iterator i = plans.begin(); i != plans.end(); ++i ) {//将所有的planplan键入到候选plan中. _qps.addCandidatePlan( *i ); }//最后加入一个不使用索引的plan. _qps.addCandidatePlan( newPlan( d, -1 ) ); }
继续来看看newPlan函数,这个函数包括了一个plan的构造.其同样是new一个QueryPlan然后调用其init函数:
QueryPlan::QueryPlan( NamespaceDetails *d, int idxNo, const FieldRangeSetPair &frsp, const BSONObj &originalQuery, const BSONObj &order, const shared_ptr<const ParsedQuery> &parsedQuery, string special ) : _d(d), _idxNo(idxNo), _frs( frsp.frsForIndex( _d, _idxNo ) ), _frsMulti( frsp.frsForIndex( _d, -1 ) ), _originalQuery( originalQuery ), _order( order ), _parsedQuery( parsedQuery ), _index( 0 ), _scanAndOrderRequired( true ),//默认是需要排序的 _exactKeyMatch( false ), _direction( 0 ), _endKeyInclusive(), _utility( Helpful ),//默认索引是有用的 _special( special ), _type(0), _startOrEndSpec() { } void QueryPlan::init( const FieldRangeSetPair *originalFrsp, const BSONObj &startKey, const BSONObj &endKey ) { _endKeyInclusive = endKey.isEmpty(); _startOrEndSpec = !startKey.isEmpty() || !endKey.isEmpty(); BSONObj idxKey = _idxNo < 0 ? BSONObj() : _d->idx( _idxNo ).keyPattern(); if ( !_frs.matchPossibleForIndex( idxKey ) ) {//Impossible的状况,这个plan是无用的 _utility = Impossible; _scanAndOrderRequired = false; return; } if ( willScanTable() ) {//索引编号为-1(newplan(xxx,-1))且plan不为Impossible,那么只能扫描全表了 if ( _order.isEmpty() || !strcmp( _order.firstElementFieldName(), "$natural" ) ) _scanAndOrderRequired = false;//要么order为空,要么order指定为$natural(自然序列,那么都不需要排序了) return; } _index = &_d->idx(_idxNo);//得到索引 // If the parsing or index indicates this is a special query, don't continue the processing if ( _special.size() ||//这部分的代码和索引插件有关,就是类似空间地理索引的处理流程 ( _index->getSpec().getType() &&//跳过 _index->getSpec().getType()->suitability( _originalQuery, _order ) != USELESS ) ) { _type = _index->getSpec().getType(); if( !_special.size() ) _special = _index->getSpec().getType()->getPlugin()->getName(); massert( 13040 , (string)"no type for special: " + _special , _type ); // hopefully safe to use original query in these contexts; // don't think we can mix special with $or clause separation yet _scanAndOrderRequired = _type->scanAndOrderRequired( _originalQuery , _order ); return; } const IndexSpec &idxSpec = _index->getSpec(); BSONObjIterator o( _order ); BSONObjIterator k( idxKey ); if ( !o.moreWithEOO() )//索引与排序要求匹配,排序要求先结束那么扫描完了后 _scanAndOrderRequired = false;//不需要再排序 while( o.moreWithEOO() ) { BSONElement oe = o.next(); if ( oe.eoo() ) { _scanAndOrderRequired = false; break; } if ( !k.moreWithEOO() ) break; BSONElement ke; while( 1 ) { ke = k.next(); if ( ke.eoo() ) goto doneCheckOrder; if ( strcmp( oe.fieldName(), ke.fieldName() ) == 0 ) break; if ( !_frs.range( ke.fieldName() ).equality() ) goto doneCheckOrder; }//索引的顺序与排序要求相反,则使用反序 int d = elementDirection( oe ) == elementDirection( ke ) ? 1 : -1; if ( _direction == 0 ) _direction = d; else if ( _direction != d ) break; } doneCheckOrder: if ( _scanAndOrderRequired ) _direction = 0; BSONObjIterator i( idxKey ); int exactIndexedQueryCount = 0; int optimalIndexedQueryCount = 0; bool awaitingLastOptimalField = true; set<string> orderFieldsUnindexed; _order.getFieldNames( orderFieldsUnindexed ); while( i.moreWithEOO() ) { BSONElement e = i.next(); if ( e.eoo() ) break; const FieldRange &fr = _frs.range( e.fieldName() ); if ( awaitingLastOptimalField ) {//这个索引有用,则OptimalIndexedQueryCount++,这里回到之前讨论的问题,查询为db.coll.find({x:{$lt:10,$gt:4}}).sort{y:1},当存在索引时{x:1},{x:1,y:1},{y:1,x:1},这里本来{y:1,x:1}应该是最优的索引,当使用其时前面会将_scanAndOrderRequired设置为false,这里遍历y时第一个进入这里时因为y不在查询内容中,所以fr.universal()为false,为universal()范围最大值为maxkey,最小为minkey,mongodb中maxkey大于其它一切数据,minkey小于其它一切数据,所以fr.equality()为false,那么awaitingLastOptmalField=false,第二次x遍历时走x路线,optimalIndexedQueryCount=-1, if ( !fr.universal() ) ++optimalIndexedQueryCount; if ( !fr.equality() ) awaitingLastOptimalField = false; } else { if ( !fr.universal() ) optimalIndexedQueryCount = -1; } if ( fr.equality() ) { BSONElement e = fr.max(); if ( !e.isNumber() && !e.mayEncapsulate() && e.type() != RegEx ) ++exactIndexedQueryCount; } orderFieldsUnindexed.erase( e.fieldName() ); } if ( !_scanAndOrderRequired &&//不需要排序并且索引有效的个数和之前得到的查询域的有效范围相等,那么这是最优的一个plan了. ( optimalIndexedQueryCount == _frs.numNonUniversalRanges() ) ) _utility = Optimal; if ( exactIndexedQueryCount == _frs.numNonUniversalRanges() && orderFieldsUnindexed.size() == 0 && exactIndexedQueryCount == idxKey.nFields() && exactKeyMatchSimpleQuery( _originalQuery, exactIndexedQueryCount ) ) { _exactKeyMatch = true; } _frv.reset( new FieldRangeVector( _frs, idxSpec, _direction ) ); if ( originalFrsp ) { _originalFrv.reset( new FieldRangeVector( originalFrsp->frsForIndex( _d, _idxNo ), idxSpec, _direction ) ); } else { _originalFrv = _frv; } if ( _startOrEndSpec ) { BSONObj newStart, newEnd; if ( !startKey.isEmpty() ) _startKey = startKey; else _startKey = _frv->startKey(); if ( !endKey.isEmpty() ) _endKey = endKey; else _endKey = _frv->endKey(); } if ( ( _scanAndOrderRequired || _order.isEmpty() ) && _frs.range( idxKey.firstElementFieldName() ).universal() ) { // NOTE SERVER-2140 _utility = Unhelpful; } if ( idxSpec.isSparse() && hasPossibleExistsFalsePredicate() ) {// _utility = Disallowed; } if ( _parsedQuery && _parsedQuery->getFields() && !_d->isMultikey( _idxNo ) ) { // Does not check modifiedKeys() _keyFieldsOnly.reset( _parsedQuery->getFields()->checkKey( _index->keyPattern() ) ); } }这里探讨一个问题,我之前一直觉得上面代码中应该是有办法判断{x:1}和{y:1,x:1}的优劣的,加入相应
条件就能够达到要求找到最优的plan{y:1,x:1},为什么不选择这个plan呢,难道说考虑到要插入这种
{y:40}这种数据吗,虽然{x:1}这种索引没有y域但是其还是会对这个{y:40}数据加入索引啊,数目并不会
比{y:1,x:1}这个索引的数目多啊,而且{y:1,x:1},但是后来我发现我忽略了一个问题,索引{y:1,x:1}无法直接定
位到x的范围,那么查询的无关的document数目可能比{x:1}这个索引查询的数目多,对于mongodb优化考
虑的是如何得到最少的document扫描数目,所以{y:1,x:1}也只能是一个可考虑的索引而无法成为最优的
索引,所以要想让查询使用这个索引只能使用hint了.
这篇文件就暂时写到这里,后面还有很多内容,一篇文章写下来太多,还是分成两篇文章吧,关于plan的
选取请看下一篇文章.
本文链接:/article/8409343.html
作者:yhjj0108,杨浩
相关文章推荐
- mongodb源码分析(七)查询3之mongod的cursor的产生(续)
- mongodb源码分析(八)查询4之mongod文档的匹配
- Mongodb源码分析--查询结果集封装
- Mongodb源码分析--游标Cursor
- mongodb源码分析--查询
- Mongodb源码分析--查询结果集封装
- Mongodb源码分析--查询结果集封装
- mongodb源码分析(四)查询1之mongo的查询请求
- mongodb源码分析--查询
- Mongodb源码分析--游标Cursor
- Mongodb源码分析--游标Cursor
- 数据库中间件 MyCAT 源码分析 —— 【单库单表】查询
- android播放器(music player)源码分析2(BaseExpandableListAdapter, SimpleCursorTreeAdapter
- MongoDB 操作手册CRUD 查询性能分析
- Mongodb源码分析--消息(message)
- Mongodb源码分析--删除记录
- hsqldb源码分析系列5 查询引擎之查询操作
- jqGrid使用multipleSearch产生的查询条件filters分析
- MySQL连接查询流程源码分析
- 结合源码分析Solr&Lucene查询打分的工作流程