網(wǎng)站建設 石家莊網(wǎng)站建設哪家公司好
文章目錄
- 一、數(shù)據(jù)庫查詢效率問題引出索引需求
- 二、索引的基本原理及作用
- (一)索引的創(chuàng)建及數(shù)據(jù)組織
- (二)不同類型的索引
- (三)索引的額外屬性
- 三、索引的優(yōu)化與查詢計劃分析
- (一)通過profiling監(jiān)測慢請求
- (二)查詢計劃分析優(yōu)化索引使用
- 四、查詢聚合優(yōu)化
- (一)案例背景
- 問題描述
- 問題分析
- 1. 定位慢查詢
- 2. 分析慢查詢語句
- 第一步:`$match`操作
- 第二步:`$project`操作
- 第三步:`$group`操作
- 查看DB/Server/Collection的狀態(tài)
- 1. DB狀態(tài)
- 2. 查看`orders`這個collection的狀態(tài)
- 性能優(yōu)化
- 1. 性能優(yōu)化 - 索引
- 2. 性能優(yōu)化 - 聚合大量數(shù)據(jù)
- 小結
更多相關內(nèi)容可查看
一、數(shù)據(jù)庫查詢效率問題引出索引需求
當在使用MongoDB等數(shù)據(jù)庫進行集合查詢時,如果遇到查詢效率低下的情況,就可能需要考慮使用索引了。以MongoDB為例,在向集合插入多個文檔后,每個文檔經(jīng)過底層存儲引擎持久化會有一個位置信息(如mmapv1引擎里是『文件id + 文件內(nèi)offset』,wiredtiger存儲引擎里是其生成的一個key),通過這個位置信息能從存儲引擎里讀出該文檔。
mongo-9552:PRIMARY> db.person.find()
{ "_id" : ObjectId("571b5da31b0d530a03b3ce82"), "name" : "jack", "age" : 19 }
{ "_id" : ObjectId("571b5dae1b0d530a03b3ce83"), "name" : "rose", "age" : 20 }
{ "_id" : ObjectId("571b5db81b0d530a03b3ce84"), "name" : "jack", "age" : 18 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce85"), "name" : "tony", "age" : 21 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce86"), "name" : "adam", "age" : 18 }
假設要執(zhí)行一個查詢操作,比如db.person.find( {age: 18} )
,如果沒有索引,就需要遍歷所有的文檔(即進行“全表掃描”),根據(jù)位置信息讀出文檔后,再對比age
字段是否為18。當集合文檔數(shù)量較少時,全表掃描的開銷可能不大,但當文檔數(shù)量達到百萬、千萬甚至上億時,全表掃描的開銷會非常大,一個查詢耗費數(shù)十秒甚至幾分鐘都有可能。
二、索引的基本原理及作用
(一)索引的創(chuàng)建及數(shù)據(jù)組織
比如上面的例子里,person集合里包含插入了5個文檔,假設其存儲后位置信息如下
位置信息 | 文檔 |
---|---|
pos1 | {“name” : “jack”, “age” : 19 } |
pos2 | {“name” : “rose”, “age” : 20 } |
pos3 | {“name” : “jack”, “age” : 18 } |
pos4 | {“name” : “tony”, “age” : 21} |
pos5 | {“name” : “adam”, “age” : 18} |
如果想加速 db.person.find( {age: 18} ),就可以考慮對person表的age字段建立索引。
db.person.createIndex( {age: 1} ) // 按age字段創(chuàng)建升序索引
建立索引后,MongoDB會額外存儲一份按age字段升序排序的索引數(shù)據(jù),索引結構類似如下,索引通常采用類似btree的結構持久化存儲,以保證從索引里快速(O(logN)的時間復雜度)找出某個age值對應的位置信息,然后根據(jù)位置信息就能讀取出對應的文檔。
age | 位置信息 |
---|---|
18 | pos3 |
18 | pos5 |
19 | pos1 |
20 | pos2 |
21 | pos4 |
簡單來說,索引就是將文檔按照某個(或某些)字段順序組織起來,以便能根據(jù)該字段高效地進行查詢。它至少能優(yōu)化以下場景的效率:
- 查詢場景:比如查詢年齡為18的所有人,有了索引就無需全表掃描,可直接通過索引快速定位到符合條件的文檔。
- 更新/刪除場景:在將年齡為18的所有人的信息進行更新或刪除時,因為更新或刪除操作需要先根據(jù)條件查詢出所有符合條件的文檔,所以本質(zhì)上也是在優(yōu)化查詢環(huán)節(jié)。
- 排序場景:將所有人的信息按年齡排序時,如果沒有索引,需要全表掃描文檔,然后再對掃描的結果進行排序;而有了索引,可利用索引的有序性更高效地完成排序。
MongoDB默認會為插入的文檔生成_id
字段(如果應用本身沒有指定該字段),并且為了保證能根據(jù)文檔id
快速查詢文檔,MongoDB默認會為集合創(chuàng)建_id
字段的索引。
mongo-9552:PRIMARY> db.person.getIndexes() // 查詢集合的索引信息
[{"ns" : "test.person", // 集合名"v" : 1, // 索引版本"key" : { // 索引的字段及排序方向"_id" : 1 // 根據(jù)_id字段升序索引},"name" : "_id_" // 索引的名稱}
]
(二)不同類型的索引
MongoDB支持多種類型的索引,每種類型適用于不同的使用場合:
-
單字段索引(Single Field Index):
- 通過
db.person.createIndex( {age: 1} )
語句可針對age
創(chuàng)建單字段索引,能加速對age
字段的各種查詢請求,是最常見的索引形式,MongoDB默認創(chuàng)建的_id
索引也屬于這種類型。 {age: 1}
代表升序索引,也可通過{age: -1}
來指定降序索引,對于單字段索引,升序/降序效果是一樣的。db.person.createIndex( {age: 1, name: 1} )
- 通過
-
復合索引 (Compound Index):
- 它是單字段索引的升級版本,針對多個字段聯(lián)合創(chuàng)建索引,先按第一個字段排序,第一個字段相同的文檔按第二個字段排序,依次類推。例如,通過
db.person.createIndex( {age: 1, name: 1} )
可針對age
、name
這2個字段創(chuàng)建一個復合索引。 - 復合索引能滿足的查詢場景比單字段索引更豐富,不光能滿足多個字段組合起來的查詢(如
db.person.find( {age: 18, name: "jack"} )
),也能滿足匹配復合索引前綴的查詢(如{age: 1}
是{age: 1, name: 1}
的前綴,所以db.person.find( {age: 18} )
的查詢也能通過該索引來加速),但像db.person.find( {name: "jack"} )
這種只涉及部分字段且不符合前綴規(guī)則的查詢則無法使用該復合索引。在創(chuàng)建復合索引時,字段的順序除了受查詢需求影響,還需考慮字段的值分布情況。比如age
字段取值有限,相同age
的文檔較多,而name
字段取值豐富,相同name
的文檔較少,此時先按name
字段查找,再在相同name
的文檔里查找age
字段會更為高效。db.person.createIndex( {name: 1, age: 1} )
- 它是單字段索引的升級版本,針對多個字段聯(lián)合創(chuàng)建索引,先按第一個字段排序,第一個字段相同的文檔按第二個字段排序,依次類推。例如,通過
-
多key索引 (Multikey Index):
- 當索引的字段為數(shù)組時,創(chuàng)建出的索引稱為多key索引。例如,在
person
表加入一個habbit
字段(數(shù)組)用于描述興趣愛好,通過db.person.createIndex( {habbit: 1} )
可自動創(chuàng)建多key索引,用于查詢有相同興趣愛好的人。{"name" : "jack", "age" : 19, habbit: ["football, runnning"]}db.person.createIndex( {habbit: 1} ) // 自動創(chuàng)建多key索引db.person.find( {habbit: "football"} )
- 當索引的字段為數(shù)組時,創(chuàng)建出的索引稱為多key索引。例如,在
-
其他類型索引:
- 哈希索引(Hashed Index):按照某個字段的hash值來建立索引,目前主要用于MongoDB Sharded Cluster的Hash分片,hash索引只能滿足字段完全匹配的查詢,不能滿足范圍查詢等。
- 地理位置索引(Geospatial Index):能很好地解決O2O的應用場景,比如“查找附近的美食”、“查找某個區(qū)域內(nèi)的車站”等。
- 文本索引(Text Index):能解決快速文本查找的需求,比如對于一個博客文章集合,可針對博客的內(nèi)容建立文本索引,以便根據(jù)博客內(nèi)容快速查找。
(三)索引的額外屬性
MongoDB除了支持多種不同類型的索引,還能對索引定制一些特殊的屬性:
- 唯一索引 (unique index):保證索引對應的字段不會出現(xiàn)相同的值,比如
_id
索引就是唯一索引。 - TTL索引:可以針對某個時間字段,指定文檔的過期時間(經(jīng)過指定時間后過期 或 在某個時間點過期)。
- 部分索引 (partial index):只針對符合某個特定條件的文檔建立索引,在3.2版本才支持該特性。
- 稀疏索引(sparse index):只針對存在索引字段的文檔建立索引,可看做是部分索引的一種特殊情況。
三、索引的優(yōu)化與查詢計劃分析
(一)通過profiling監(jiān)測慢請求
MongoDB支持對DB的請求進行profiling,目前支持3種級別的profiling:
- 0級:不開啟profiling。
- 1級:將處理時間超過某個閾值(默認100ms)的請求都記錄到DB下的system.profile集合(類似于mysql、redis的slowlog),生產(chǎn)環(huán)境通常建議使用此級別,并根據(jù)自身需求配置合理的閾值,用于監(jiān)測慢請求的情況,以便及時進行索引優(yōu)化。
- 2級:將所有的請求都記錄到DB下的system.profile集合,生產(chǎn)環(huán)境需慎用。
(二)查詢計劃分析優(yōu)化索引使用
當索引已經(jīng)建立了,但查詢還是很慢時,就需要深入分析索引的使用情況,可通過查看詳細的查詢計劃來決定如何優(yōu)化。通過執(zhí)行計劃可以看出以下問題:
- 根據(jù)某個/些字段查詢,但沒有建立索引。
- 根據(jù)某個/些字段查詢,但建立了多個索引,執(zhí)行查詢時沒有使用預期的索引。
例如,建立索引前,db.person.find( {age: 18} )
必須執(zhí)行COLLSCAN
(全表掃描);
mongo-9552:PRIMARY> db.person.find({age: 18}).explain()
{"queryPlanner" : {"plannerVersion" : 1,"namespace" : "test.person","indexFilterSet" : false,"parsedQuery" : {"age" : {"$eq" : 18}},"winningPlan" : {"stage" : "COLLSCAN","filter" : {"age" : {"$eq" : 18}},"direction" : "forward"},"rejectedPlans" : [ ]},"serverInfo" : {"host" : "localhost","port" : 9552,"version" : "3.2.3","gitVersion" : "b326ba837cf6f49d65c2f85e1b70f6f31ece7937"},"ok" : 1
}
建立索引后,通過查詢計劃可以看出,先進行[IXSCAN](從索引中查找),然后
FETCH`,讀取出滿足條件的文檔。
mongo-9552:PRIMARY> db.person.find({age: 18}).explain()
{"queryPlanner" : {"plannerVersion" : 1,"namespace" : "test.person","indexFilterSet" : false,"parsedQuery" : {"age" : {"$eq" : 18}},"winningPlan" : {"stage" : "FETCH","inputStage" : {"stage" : "IXSCAN","keyPattern" : {"age" : 1},"indexName" : "age_1","isMultiKey" : false,"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 1,"direction" : "forward","indexBounds" : {"age" : ["[18.0, 18.0]"]}}},"rejectedPlans" : [ ]},"serverInfo" : {"host" : "localhost","port" : 9552,"version" : "3.2.3","gitVersion" : "b326ba837cf6f49d65c2f85e1b70f6f31ece7937"},"ok" : 1
}
需要注意的是,索引并不是越多越好,集合的索引太多,會影響寫入、更新的性能,因為每次寫入都需要更新所有索引的數(shù)據(jù)。所以system.profile里的慢請求可能是索引建立得不夠?qū)е?#xff0c;也可能是索引過多導致。
四、查詢聚合優(yōu)化
(一)案例背景
我們有一個電商訂單分析系統(tǒng),使用MongoDB存儲訂單數(shù)據(jù)。當執(zhí)行一個分析接口,獲取特定店鋪在某一周內(nèi)的訂單商品分類統(tǒng)計信息時,發(fā)現(xiàn)查詢速度非常慢,嚴重影響用戶體驗。
問題描述
執(zhí)行訂單分析接口,查詢特定店鋪(假設店鋪ID為“20001”)在某一周(2024 - 05 - 01T00:00:00.000Z到2024 - 05 - 07T23:59:59.999Z)內(nèi)的訂單商品分類統(tǒng)計,需要花費約12秒,這明顯不符合性能要求。
問題分析
1. 定位慢查詢
首先查看當前mongo profile的級別,通過db.getProfilingLevel()
發(fā)現(xiàn)其為0,即默認沒有記錄。設置profile級別為記錄慢查詢模式,設置閾值為1000ms,即db.setProfilingLevel(1, 1000)
。再次執(zhí)行訂單分析查詢接口,查看Profile記錄。
2. 分析慢查詢語句
通過查看Profile記錄,發(fā)現(xiàn)執(zhí)行的查詢是一個聚合管道(pipeline):
第一步:$match
操作
{"$match": {"storeId": "20001","$and": [{"orderTime": {"$gte": ISODate("2024-05-01T00:00:00.000Z"),"$lte": ISODate("2024-05-07T23:59:59.999Z")}}]}
}
用于匹配店鋪ID為“20001”且訂單時間在指定一周內(nèi)的訂單記錄。
第二步:$project
操作
{"$project": {"productCategory": 1,"orderDate": {"$concat": [{"$substr": [{"$year": ["$orderTime"]},0,4]},"-",{"$substr": [{"$month": ["$orderTime"]},0,2]},"-",{"$substr": [{"$dayOfMonth": ["$orderTime"]},0,2]}]}}
}
除了提取productCategory
字段外,還對orderTime
字段進行處理,拼接為“yyyy - MM - dd”格式,并將其命名為orderDate
。
第三步:$group
操作
{"$group": {"_id": {"orderDate": "$orderDate","productCategory": "$productCategory"},"count": {"$sum": 1}}
}
對orderDate
和productCategory
進行分組,統(tǒng)計不同日期和商品分類對應的訂單數(shù)量。
從Profile中可以看到相關指標:
millis
:花費了12010毫秒返回查詢結果。ts
:命令執(zhí)行時間。info
:命令內(nèi)容。query
:代表查詢。ns
:ecommerce.orders
(代表查詢的庫與集合)。nreturned
:返回記錄數(shù)及用時。reslen
:返回的結果集大小(字節(jié)數(shù))。nscanned
:掃描記錄數(shù)量。
發(fā)現(xiàn)nscanned
數(shù)很大,接近記錄總數(shù),可能沒有使用索引查詢。
查看DB/Server/Collection的狀態(tài)
1. DB狀態(tài)
查看數(shù)據(jù)庫整體狀態(tài),包括服務器版本、運行時間、連接數(shù)、各種操作計數(shù)器(如插入、查詢、更新、刪除等操作的次數(shù))、存儲引擎信息等。示例部分信息如下:
{"host": "ECOMMONGODB","version": "6.0.5","process": "mongod","pid": NumberLong(2005),"uptime": 12345678.0,"uptimeMillis": NumberLong(12345678901),"uptimeEstimate": NumberLong(12345678),"localTime": ISODate("2024-05-08T10:20:30.123Z"),"asserts": {"regular": 0,"warning": 0,"msg": 0,"user": 12345,"rollovers": 0},"connections": {"current": 120,"available": 800,"totalCreated": 13000},// 其他更多信息..."ok": 1.0
}
2. 查看orders
這個collection的狀態(tài)
{"ns": "ecommerce.orders","size": 987654321,"count": 3500000,"avgObjSize": 282,"storageSize": 234567890,"capped": false,"wiredTiger": {// wiredTiger存儲引擎相關詳細信息...},"nindexes": 1,"totalIndexSize": 30123456,"indexSizes": {"_id_": 30123456},"ok": 1.0
}
性能優(yōu)化
1. 性能優(yōu)化 - 索引
目前只有_id
索引,接下來對orders
集合創(chuàng)建storeId
、orderTime
和productCategory
字段的索引:
db.orders.ensureIndex({"storeId": 1, "orderTime": 1, "productCategory": 1});
db.orders.ensureIndex({"orderTime": 1});
db.orders.ensureIndex({"productCategory": 1});
db.orders.ensureIndex({"storeId": 1});
創(chuàng)建索引后,查詢特定店鋪一周內(nèi)的訂單商品分類統(tǒng)計信息,時間縮短到了500ms,效果顯著。但當查詢一個月的數(shù)據(jù)時,仍然需要15秒。
通過增加索引小結:添加索引解決了針對索引字段查詢的效率問題,但對于大量數(shù)據(jù)的聚合操作,僅靠索引不能完全解決性能問題。例如,在沒有索引的情況下,從500萬條數(shù)據(jù)中找出特定店鋪的訂單可能需要全表掃描,耗時很長;而有了索引,命中索引查詢(IXSCAN)速度提升明顯。不過,對于聚合操作,隨著數(shù)據(jù)量增大,性能問題依然存在。同時,判斷效率優(yōu)化情況應該看執(zhí)行計劃,而不僅僅是執(zhí)行時間,因為執(zhí)行時間可能受到多種因素影響。
2. 性能優(yōu)化 - 聚合大量數(shù)據(jù)
對于這種查詢聚合大量數(shù)據(jù)的問題,考慮到這是一個類似OLAP的操作,對其性能期望不能過高,因為大量數(shù)據(jù)的I/O操作遠超OLTP操作。但仍有一定的優(yōu)化空間:
- 在訂單插入或更新時,對每個店鋪每天的每個商品分類的訂單數(shù)量進行實時計數(shù),并存儲在一個專門的緩存集合中。例如,以
{storeId: "20001", orderDate: "2024-05-01", productCategory: "Electronics", count: 10}
的形式存儲。 - 每隔一段時間(如每天凌晨)對緩存集合進行一次完整的統(tǒng)計和更新,確保數(shù)據(jù)的準確性。這樣在查詢訂單商品分類統(tǒng)計信息時,可以直接從緩存集合中獲取數(shù)據(jù),大大減少了查詢和聚合的時間。
小結
- 慢查詢定位:通過Profile分析慢查詢。
- 查詢優(yōu)化:通過添加相應索引提升查詢速度。
- 聚合大數(shù)據(jù)方案:對于類似OLAP的聚合操作,要合理降低性能期望。從源頭入手,在數(shù)據(jù)插入或更新時做好部分統(tǒng)計工作,緩存結果,以便在查詢時直接使用,從而提升整體性能。同時,要結合執(zhí)行計劃來評估優(yōu)化效果。