查詢優(yōu)化器
MongoDB的查詢計劃會將多個索引并行去執(zhí)行,最先返回101的結(jié)果就是勝者,其他查詢計劃就會被終止,執(zhí)行優(yōu)勝的查詢計劃
這個查詢計劃將會被緩存较鼓,接下來相同的語句查詢條件都會使用它
- 何時查詢計劃才會變
- 建立索引時
- 每執(zhí)行1000次查詢之后,查詢優(yōu)化器就會重新評估查詢計劃
- 較大的數(shù)據(jù)波動
explain 使用
db.getCollection('db_name').explain('executionStats').aggregate([....])
// 得到的結(jié)果
{
"stages" : [
{
"$cursor" : {
"query" : {
.....
},
"fields" : {
......
},
"queryPlanner" : {
......
},
"winningPlan" : { // 優(yōu)勝的方案
"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顆粒度這么大的索引不應(yīng)該建立违柏,難怪要被拒絕
},
"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, // 返回的結(jié)果數(shù)量
"executionTimeMillis" : 1, // 運(yùn)行的時間
"totalKeysExamined" : 1, // 掃描的索引數(shù)量
"totalDocsExamined" : 1, // 掃描的文檔數(shù)量
"executionStages" : {
"stage" : "FETCH", // step2: 用is_deleted字段從上一階段的結(jié)果中過濾出相應(yīng)結(jié)果
"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字段索引搜索出相應(yīng)結(jié)果
"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)
}
}
}
- 查看方式:嵌套最內(nèi)層往外的順序看,不是從上到下香椎。
原因:
explain 結(jié)果將查詢計劃以階段樹的形式呈現(xiàn)漱竖。
每個階段將其結(jié)果傳遞給父節(jié)點(diǎn),中間節(jié)點(diǎn)操作由子節(jié)點(diǎn)產(chǎn)生的文檔或索引
- 索引使用情況解讀
stage 主要分為以下幾種:
COLLSACN: 全盤掃描
IXSACN: 索引掃描
FETCH: 根據(jù)前面節(jié)點(diǎn)掃描出的文檔畜伐,進(jìn)一步過濾抓取
SORT: 內(nèi)存進(jìn)行排序
SORT_KEY_GENERATOR: 獲取每一個文檔排序所用的鍵值
LIMIT: 使用limit限制返回數(shù)
SKIP: 使用skip進(jìn)行跳過
IDHACK: 針對_id進(jìn)行查詢
COUNTSCAN: count不使用index進(jìn)行count
COUNT_SCACN: count使用index進(jìn)行count
TEXT: 使用全文索引進(jìn)行查詢
SUBPLA:未使用到索引的$or查詢
PROJECTION:限定返回字段
- 所以不希望看到explain分析出現(xiàn)如下的stage:
COLLSCAN
SORT
COUNTSCAN
SUBPLA
- 最好是如下的組合:
FETCH + IXSCAN
FETCH + IDHACK
LIMIT + ( FETCH + IXSCAN)
PROJECTION + IXSCAN
COUNT_SCAN
效率極低的操作符
exists:這兩個操作符馍惹,完全不能使用索引。
not: 通常來說取反和不等于,可以使用索引玛界,但是效率極低万矾,不是很有效,往往也會退化成掃描全表慎框。
- $nin: 不包含良狈,這個操作符也總是會全表掃描
- 對于管道中的索引,也很容易出現(xiàn)意外笨枯,只有在管道最開始時的match sort可以使用到索引薪丁,一旦發(fā)生過project投射,group分組馅精,lookup表關(guān)聯(lián)严嗜,unwind打散等操作后,就完全無法使用索引洲敢。
aggregate優(yōu)化
- 我認(rèn)為的準(zhǔn)則是盡可能先縮小文檔大新(例如:
sort,
skip)压彭,最后進(jìn)行其他復(fù)雜操作(
project,
unwind),因為這些操作打散后睦优,完全無法使用索引.
最佳順序:sort + $limit + ...
- 千萬別忘了$lookup連表的字段,兩張表一定要建立索引
- 優(yōu)化案例1:limit提前縮小文檔大小哮塞,減少內(nèi)存計算
// 使用時間: 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' },
], {})
- 優(yōu)化案例2: 轉(zhuǎn)換搜索的主表刨秆,使索引生效
// 使用時間: 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' },
], {})
索引設(shè)計原則
-
索引字段顆粒度越小越好
顆粒度為結(jié)果集在原集合中所占的比例
顆粒度小的,例如身份證號等唯一性質(zhì)的忆畅,索引掃描能夠很快定位出位置
相反字段顆粒度大的衡未,例如枚舉尸执,例如布爾值,索引定位出的位置不夠精準(zhǔn)缓醋,到頭來還得大部分掃描如失,因為多了索引掃描,最后速度可能還不如全盤掃描送粱。
-
字段更新頻率小
索引的缺點(diǎn)之一就是修改時還需要維護(hù)索引褪贵,所以最好選擇字段更新比較小的字段
-
適當(dāng)冗余設(shè)計
aggregate連表查詢。如果查詢字段在副表中抗俄,就無法使用到索引脆丁,如果這種連表查詢頻率較高,可考慮冗余設(shè)計动雹。如上訴案例2槽卫,可通過冗余phone的字段提高查詢效率,以及增加代碼通用性
-
索引數(shù)量控制
一個索引的字段超過7胰蝠,8歼培,需考慮合理性
查詢優(yōu)化原則
-
減少帶寬
按需取字段,避免返回大字段
-
減少內(nèi)存計算
減少中間存儲茸塞,內(nèi)存計算
-
減少磁盤IO
增加索引躲庄,避免全盤掃描,優(yōu)化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://www.reibang.com/p/f92919ae6c90