mongodb原始碼分析(六)查詢3之mongod的cursor的產生
上一篇文章分析了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.
是[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索引優化的文章可以一看:
上面介紹了那麼多的流程情況下面正式進入程式碼分析階段.接上篇文章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的特性:http://www.cnblogs.com/silentcross/archive/2011/07/04/2095424.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的
選取請看下一篇文章.
作者:yhjj0108,楊浩
相關推薦
mongodb原始碼分析(六)查詢3之mongod的cursor的產生
上一篇文章分析了mongod的資料庫載入部分,下面這一篇文章將繼續分析mongod cursor的產生,這裡cursor 的生成應該是mongodb系統中最複雜的部分.下面先介紹幾個關於mongodb的遊標概念. basicCursor: 直接掃描整個co
mongodb原始碼分析(五)查詢2之mongod的資料庫載入
上一篇文章分析到了客戶端查詢請求的傳送,接著分析服務端的處理動作,分析從服務端響應開始到資料庫 正確載入止,主要流程為資料庫的讀入過程與使用者的認證. mongod服務對於客戶端請求的處理在mongo/db/db.cpp MyMessageH
RabbitMQ客戶端原始碼分析(六)之IntAllocator
RabbitMQ-java-client版本 com.rabbitmq:amqp-client:4.3.0 RabbitMQ版本宣告: 3.6.15 IntAllocator 用於分配給定範
Mongodb原始碼分析--Replication之主從模式--Slave
在上文中介紹了主從(master-slave)模式下的一些基本概念及master的執行流程。今天接著介紹一下從(slave)結點是如何發起請求,並通過請求獲取的oplog資訊來構造本地資料的。 不過開始今天的正文前,需要介紹一下mongodb在sla
Linux核心原始碼分析(六)--start_kernel之lockdep_init
這個函式比較短,這裡直接貼出來。 void lockdep_init(void) { int i; /* * Some architectures have their own start_kernel()
【kubernetes/k8s原始碼分析】 controller-manager之replicaset原始碼分析
ReplicaSet簡介 Kubernetes 中建議使用 ReplicaSet來取代 ReplicationController。ReplicaSet 跟 ReplicationController 沒有本質的不同, ReplicaSet 支援集合式的
用MongoDB profiler分析慢查詢
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!  
Android系統原始碼分析--View繪製流程之-setContentView
上一篇分析了四大元件之ContentProvider,這也是四大元件最後一個。因此,從這篇開始我們分析新的篇章--View繪製流程,View繪製流程在Android開發中佔有非常重要的位置,只要有檢視的顯示,都離不開View的繪製,所以瞭解View繪製原理對於應用開發以及系統的學習至關重要。由於View
ndroid系統原始碼分析--View繪製流程之-inflate
上一章我們分析了Activity啟動的時候呼叫setContentView載入佈局的過程,但是分析過程中我們留了兩個懸念,一個是將資原始檔中的layout中xml佈局檔案通過inflate載入到Activity中的過程,另一個是開始測量、佈局和繪製的過程,第二個我們放到measure過程中分析,這一篇先
tornado原始碼分析(二)之iostream
在事件驅動模型中,所有任務都是以某個事件的回撥函式的方式新增至事件迴圈中的,如:HTTPServer要從socket中讀取客戶端傳送的request訊息,就必須將該socket新增至ioloop中,並設定回掉函式,在回掉函式中從socket中讀取資料,並且檢查request訊息是否全部接收到了,如果
VScode原始碼分析:查詢服務可用的埠
基本資訊: 分支: master; commitID: 3c4e9323; 檔案路徑: src/vs/base/node/ports.ts 基本思路: 1、查詢方法返回一個Promise(結果為resolve) 2、與127.0.0.1逐個埠進行Socket連線:
spring原始碼分析六 bean的載入第三步-單例的建立 上篇
在上一篇部落格中有單例建立的如下程式碼: if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { return createBean(bean
mochiweb原始碼分析(六)
1.說下call_body這函式 這裡的Body就是之前說的處理http請求的方法,如下圖 容易看出呼叫的是call_body/2的第三個分支,然後再看看我們寫的簡單的helloweb模組的開啟伺服器部分 這實際上是一個閉包的用法,即呼叫的是helloweb:l
spring4.2.9 java專案環境下ioc原始碼分析 (九)——refresh之postProcessBeanFactory方法
postProcessBeanFactory後處理beanFactory。時機是在所有的beanDenifition載入完成之後,bean例項化之前執行。比如,在beanfactory載入完成所有的bean後,想修改其中某個bean的定義,或者對beanFactory做一些其
Docker原始碼分析(二)之Docker Client
一、建立Docker Client Docker是一個client/server的架構,通過二進位制檔案docker建立Docker客戶端將請求型別與引數傳送給Docker Server,Docker Server具體執行命令呼叫。 Docker Client執行流
深入原始碼分析mybatis查詢原理(四)
由於前面幾篇的查詢都是沒有帶引數的,那麼我現在加一個帶引數查詢的方法,來看一下mybatis如何處理引數並查詢。EmpTexst.java t1()方法Emp emp = empmapper.queryByEmpNoAndName(1, "admin"); System.o
SpringMVC原始碼分析(二)之請求如何轉發到對應的Controller
在前一篇對DispatcherServlet的分析中,初略的過了下請求是如何處理的,本文將重點分析,HandlerMapping與HandlerAdapter是如何工作的 在web容器啟動的過程中,會初初始化一系列SpringMVC所需
ABP原始碼分析六:依賴注入的實現
ABP的依賴注入的實現有一個本質兩個途徑:1.本質上是依賴於Castle這個老牌依賴注入的框架。2.一種實現途徑是通過實現IConventionalDependencyRegistrar的例項定義注入的約定(規則),然後通過IocManager來讀取這個規則完成依賴注入。3另一種實現途徑是直接IocManag
Glusterfs之nfs模組原始碼分析(上)之nfs原理和協議
歡迎大家相互交流,共同提高技術。 一、網路檔案系統概述 Sun Microsystems公司於1984年推出了一個在整個計算機工業中被廣泛接受的遠端檔案存取機制,它被稱為Sun的網路檔案系統(Network File System),或者簡稱為NFS。該機制允許在一
Lighttpd1.4.20原始碼分析 筆記 狀態機之請求處理
lighttpd請求處理的過程: 1.伺服器與客戶端建立連線後,連線進入CON_STATE_REQUEST_START狀態,伺服器做一些標記,如連線開始的時間等。 2.連線進入CON_STATE_READ狀態,伺服器從連線讀取HTTP頭並存放在con->