springboot mongo查询固定字段_你真的会用索引么?[Mongo]
一次奇怪的查詢經(jīng)歷
如何奇怪了?
- 對同一張表,用同樣的SQL,查詢200萬條數(shù)據(jù)耗時100ms,查詢二十條數(shù)據(jù)卻耗時30s。
- 數(shù)據(jù)量少了10萬倍,完全不是一個數(shù)量級的數(shù)據(jù),耗時卻多了300倍。
- 明明加了索引為什么還是那么慢?
下圖是在本地簡化模擬出來的查詢結(jié)果,雖然沒有那么夸張但是同樣可以復(fù)現(xiàn)問題。
95.6萬條數(shù)據(jù),用時0.08秒106條數(shù)據(jù),用時10秒如上二圖,通過相同查詢條件以及相同的排序條件,進(jìn)行查詢,查詢效率缺卻天差地別,下面讓我們來一起探索一下究竟是為什么?
索引概述
我們常常會看到一些亂七八糟的索引,所以我們用索引的真正目的是什么呢?
終極目的:借助索引快速搜索,有效減少了掃描的行數(shù)
精髓:不止要有索引,索引的過濾性還要好,區(qū)分度要足夠高,這才是好的設(shè)計
索引的類型和屬性
唯一索引
唯一索引是索引具有的一種屬性,讓索引具備唯一性,確保這張表中,該條索引數(shù)據(jù)不會重復(fù)出現(xiàn)。在每一次insert和update操作時,都會進(jìn)行索引的唯一性校驗,保證該索引的字段組合在表中唯一。
db.containers.createIndex({name: 1},{unique:true, background: true}) db.packages.createIndex({ appId: 1, version: 1 },{unique:true, background: true}) 知識點一:創(chuàng)建索引時,1表示按升序存儲,-1表示按降序存儲。知識點二:Mongo提供兩種建索引的方式foreground和background。前臺操作,它會阻塞用戶對數(shù)據(jù)的讀寫操作直到index構(gòu)建完畢;后臺模式,不阻塞數(shù)據(jù)讀寫操作,獨立的后臺線程異步構(gòu)建索引,此時仍然允許對數(shù)據(jù)的讀寫操作。創(chuàng)建索引時一定要寫{background: true}創(chuàng)建索引時一定要寫{background: true}創(chuàng)建索引時一定要寫{background: true}復(fù)合索引
概念:指的是將多個鍵組合到一起創(chuàng)建索引,終極目的是加速匹配多個鍵的查詢。
看例子來理解復(fù)合索引是最直接的方式:
圖中模擬了簡單的航班信息表的數(shù)據(jù)。
對表中指定航班進(jìn)行查詢,查詢后按價格排序。
db.getCollection('flight').find({flight: “CA12345”}).sort({price: 1})
在沒有索引的情況下,那么他其實是會一條一條的掃描全部8條數(shù)據(jù),找到CA12345航班,然后再在內(nèi)存中按價錢進(jìn)行排序。
如果這時我給航班添加一條索引db.flights.createIndex({ flight: 1 },{background: true}),那么索引會類似于下圖一樣,將數(shù)據(jù)按照索引規(guī)則進(jìn)行排序,此時就只需要掃描4條CA12345航班的數(shù)據(jù),然后再在內(nèi)存中進(jìn)行排序。如果數(shù)據(jù)量大了以后,在內(nèi)存中進(jìn)行排序的代價是非常大的。
所以我們可以建立復(fù)合索引 db.flights.createIndex({ flight: 1, price: 1 },{background: true})
讓數(shù)據(jù)按照索引先將所有數(shù)據(jù)以航班號有序排列,再在航班號相同的數(shù)據(jù)集中按價格升序排列,這樣在進(jìn)行查詢的時候,就可以準(zhǔn)確的使用索引掃描4條數(shù)據(jù),并且他們本身就是有序的,無需再進(jìn)行額外的排序工作。以上實現(xiàn)了通過復(fù)合索引,讓查詢變得最優(yōu),這就是復(fù)合索引的作用。
內(nèi)嵌索引
可以在嵌套的文檔上建立索引,方式與建立正常索引完全一致。
個人信息表結(jié)構(gòu)如下,包含了省市區(qū)三級的地址信息,如果想要給城市(city)添加索引,其實就和正常添索引一樣
db.personInfos.createIndex({“address.city”:1})
const personInfo = new Schema({name: { type: String, required: true },address: {province: { type: String, required: true },city: { type: String, required: true }, district: { type: String, required: true },} }, {timestamps: true});知識點三:對嵌套文檔本身“address”建立索引,與對嵌套文檔的某個字段(address.city)建立索引是完全不相同的。
對整個文檔建立索引,只有在使用文檔完整匹配時才會使用到這個索引,例如建立了這樣一個索引db.personInfos.createIndex({“address”:1}),那么只有使用db.personInfos.find({“address”:{“province”:”xxx”,”city”:”xxx”,""district":"xxx"}})這種完整匹配時才會使用到這個索引,使用db.personInfos.find({“address.city”:”xxx”})是不會使用到該索引的。
數(shù)組索引
MongoDB支持對數(shù)組建立索引,這樣就可以高效的搜索數(shù)組中的特定元素。
知識點四:但是!對數(shù)組建立索引的代價是非常高的,他實際上是會對數(shù)組中的每一項都單獨建立索引,就相當(dāng)于假設(shè)數(shù)組中有十項,那么就會在原基礎(chǔ)上,多出十倍的索引大小。如果有一百個一千個呢?
所以在mongo中是禁止對兩個數(shù)組添加復(fù)合索引的,對兩個數(shù)組添加索引那么索引大小將是爆炸增長,所以謹(jǐn)記在心。
過期索引(TTL)
可以針對某個時間字段,指定文檔的過期時間(經(jīng)過指定時間后過期 或 在某個時間點過期)
哈希索引(Hashed Index)
是指按照某個字段的hash值來建立索引,hash索引只能滿足字段完全匹配的查詢,不能滿足范圍查詢等
地理位置索引(Geospatial Index)
能很好的解決一些場景,比如『查找附近的美食』、『查找附近的加油站』等
文本索引(Text Index)
能解決快速文本查找的需求,比如,日志平臺,相對日志關(guān)鍵詞查找,如果通過正則來查找的話效率極低,這時就可以通過文本索引的形式來進(jìn)行查找
索引的優(yōu)點
1.減少數(shù)據(jù)掃描:避免全表掃描代價
2.減少內(nèi)存計算:避免分組排序計算
3.提供數(shù)據(jù)約束:唯一和時間約束性
索引的缺點
1.增加容量消耗:創(chuàng)建時需額外存儲索引數(shù)據(jù)
2.增加修改代價:增刪改都需要維護(hù)索引數(shù)據(jù)
3.索引依賴內(nèi)存:會占用極其寶貴的內(nèi)存資源
索引固然不全是優(yōu)點,如果不能了解到索引可能帶來的危害濫用索引,后果也是非常嚴(yán)重的。
索引雖然也是持久化在磁盤中的,但為了確保索引的速度,實際上需要將索引加載到內(nèi)存中使用,使用過后還會進(jìn)行緩存,內(nèi)存資源相比磁盤空間那是非常的珍貴了。當(dāng)內(nèi)存不足以承載索引的時候,就會出現(xiàn)內(nèi)存——磁盤交換的情況,這時會大大降低索引的性能。
有人說研究索引好累啊?我給我的每個字段都加一個索引不就完事了么?其實每個人都知道這樣不好,但實戰(zhàn)中好多人都是這樣干的。無腦的給每個字段都加上索引就意味著每一次數(shù)據(jù)庫操作,不僅需要更新文檔,還需要有大量索引需要更新。mongo每次查詢只會使用一個索引。想不到吧?不是你想的我先查航班,在用價格排序,會先走航班的索引,再走價格的索引,你做夢去吧,不可能的,他只會選定一條索引,并不會因為你給每個字端都加了索引就解決問題了。
知識點五:為了追求索引的速度,索引是加載在內(nèi)存中使用的,不能合理使用索引后果嚴(yán)重。mongo每次查詢只會使用一次索引!!!只有$or或查詢特殊,他會給每一個或分支使用索引然后再合并何時不應(yīng)該使用索引
也有一些查詢不使用索引會更快。結(jié)果集在原集合中所占的比例越大,查詢效率越慢。因為使用索引需要進(jìn)行兩次查找:一次查找索引條目,一次根據(jù)索引指針去查找相應(yīng)的文檔。而全表掃描只需要進(jìn)行一次查詢。在最壞的情況,使用索引進(jìn)行查找次數(shù)會是全表掃描的兩倍。效率會明顯比全表掃描低。
而相反在提取較小的子數(shù)據(jù)集時,索引就非常有效,這就是我們?yōu)槭裁磿褂梅猪摗?/p>
查詢優(yōu)化器
Mongo自帶了一個查詢優(yōu)化器會為我們選擇最合適的查詢方案。
如果一個索引能夠精確匹配一個查詢,那么查詢優(yōu)化器就會使用這個索引。
如果不能精確匹配呢?可能會有幾個索引都適合你的查詢,那MongoDB是怎樣選擇的呢?
- MongoDB的查詢計劃會將多個索引并行的去執(zhí)行,最先返回第101個結(jié)果的就是勝者,其他查詢計劃都會被終止,執(zhí)行優(yōu)勝的查詢計劃;
- 這個查詢計劃會被緩存,接下來相同的查詢條件都會使用它;
何時查詢計劃緩存才會變呢?
聯(lián)合索引的優(yōu)化
當(dāng)你查詢條件的順序和你索引的順序不一致的話,mongo會自動的調(diào)整查詢順序,保證你可以使用上索引。
例如:你的查詢條件是(a,c,b)但是你的索引是(a,b,c)mongo會自動將你的查詢條件調(diào)整為abc,尋找最優(yōu)解。
聚合管道的優(yōu)化
然而管道中的索引使用情況是極其不佳的,在管道中,只有在管道最開始時的match sort可以使用到索引,一旦發(fā)生過project投射,group分組,lookup表關(guān)聯(lián),unwind打散等操作后,就完全無法使用索引。
希望通過本文能讓你對Mongo的索引有更深的理解
Explain查詢計劃
提到查的慢,二話不說直接看查詢計劃好么?具體每一個字段的含義我就不做贅述了很容易查到,我截取winningPlan的部分和大家一起看一下。WinningPlan就是在查詢計劃中勝出的方案,那肯定就有被淘汰的方案,是在rejectPlan里。
// 查詢計劃中的winningPlan部分 "winningPlan": {"stage": "FETCH","filter": {"createdAt": {"$gte": ISODate("2019-07-22T12:00:44.000Z")}},"inputStage": {"stage": "IXSCAN","keyPattern": {"load": 1},"indexName": "load_1","isMultiKey": false,"multiKeyPaths": {"load": []},"isUnique": false,"isSparse": false,"isPartial": false,"indexVersion": 2,"direction": "backward","indexBounds": {"load": ["[MaxKey, MinKey]"]}} },看不懂?沒關(guān)系,先學(xué)習(xí)了下面兩個知識點,我?guī)阕x一遍。
知識點六:explain 結(jié)果將查詢計劃以階段樹的形式呈現(xiàn)。每個階段將其結(jié)果(文檔或索引鍵)傳遞給父節(jié)點。中間節(jié)點操縱由子節(jié)點產(chǎn)生的文檔或索引鍵。根節(jié)點是MongoDB從中派生結(jié)果集的最后階段。對于新人一定要特別注意:在看查詢結(jié)果的階段樹的時候一定一定是從最里層一層一層往外看的,不是直接順著讀下來的。
知識點七:在查詢計劃中出現(xiàn)了很多stage,下面列舉的經(jīng)常出現(xiàn)的stage以及他的含義:COLLSCAN:全表掃描IXSCAN:索引掃描FETCH:根據(jù)前面掃描到的位置抓取完整文檔SORT:進(jìn)行內(nèi)存排序,最終返回結(jié)果SORT_KEY_GENERATOR:獲取每一個文檔排序所用的鍵值LIMIT:使用limit限制返回數(shù)SKIP:使用skip進(jìn)行跳過IDHACK:針對_id進(jìn)行查詢COUNTSCAN:count不使用用Index進(jìn)行count時的stage返回COUNT_SCAN:count使用了Index進(jìn)行count時的stage返回TEXT:使用全文索引進(jìn)行查詢時候的stage返回Explain解讀:
將解讀寫在了注釋中,按順序閱讀
// 查詢計劃中的winningPlan部分 "winningPlan": {"stage": "FETCH", // 5. 根據(jù)內(nèi)層階段樹查到的索引去抓取完整的文檔"filter": { // 6. 再根據(jù)createdAt參數(shù)進(jìn)行篩選"createdAt": {"$gte": ISODate("2019-07-22T12:00:44.000Z")}},"inputStage": { // 1. 每個階段將自己的查詢結(jié)果傳遞給父階段樹,所以從里往外讀Explain"stage": "IXSCAN", // 2. IXSCAN該階段使用了索引進(jìn)行掃描"keyPattern": {"load": 1 // 3. 使用了 load:1 這條索引},"indexName": "load_1","isMultiKey": false,"multiKeyPaths": {"load": []},"isUnique": false,"isSparse": false,"isPartial": false,"indexVersion": 2,"direction": "backward", "indexBounds": {"load": ["[MaxKey, MinKey]" // 4. 邊界]}} },最后在本文末尾,留下了前面查詢航班按價錢排序的例子,在各種索引下的查詢計劃
最期望看到的查詢組合
- Fetch+IDHACK
- Fetch+ixscan
- Limit+(Fetch+ixscan)
- PROJECTION+ixscan
最不期望看到的查詢組合
- COLLSCAN(全表掃)
- SORT(使用sort但是無index)
- COUNTSCAN (不使用索引進(jìn)行count)
最左前綴原則
假定索引(a,b,c) 它可能滿足的查詢?nèi)缦?#xff1a;
1. a
2. a,b
3. a,b,c
4. a,c [該組合只能用a部分]
5. a, c, b [cb在查詢時會被優(yōu)化換位置]
顯然,最左前綴的核心是查詢條件字段必須含有索引第一個字段
最左值盡可能用最精確過濾性最好的值,不要用那種可能會用于范圍模糊查詢,用于排序的字段
效率極低的操作符
索引設(shè)計和優(yōu)化原則
最后祭出李丹老師的索引設(shè)計和優(yōu)化原則
1.主鍵的設(shè)置
業(yè)務(wù)無關(guān)、顯示指定、遞增屬性
2.數(shù)據(jù)區(qū)分度
原則上區(qū)分度高的字段優(yōu)先做索引字段,如果是組合索引優(yōu)先放前面
3.字段更新頻率
頻繁更新的字段是否做索引字段需要綜合考慮對業(yè)務(wù)的影響及查詢的代價
4.前綴索引問題
需要注意的是因前綴索引只包含部分值因此無法通過前綴索引優(yōu)化排序
5.適當(dāng)冗余設(shè)計
對于存儲較長字符串字段可額外增加字段存儲原字段計算(如hash)后的值
創(chuàng)建索引時只需要對額外字段創(chuàng)建索引即可
6.避免無效索引
通常類似表已經(jīng)含有主鍵ID就無需再創(chuàng)建額外唯一性的ID索引
7.查詢覆蓋率
設(shè)計一個索引我們需要考慮盡量覆蓋更多的查詢場景
8.控制字段數(shù)
如果你設(shè)計的索引例如含有7、8個字段通常需要考慮設(shè)計是否合理
優(yōu)化原則
1.減少網(wǎng)絡(luò)帶寬
按需返回所需字段、盡量避免返回大字段
2.減少內(nèi)存計算
減少無必要中間結(jié)果存儲、減少內(nèi)存計算
3.減少磁盤IO
添加合適的索引、關(guān)注SQL改寫
前文查詢航班按價錢排序的例子,在各種索引下的查詢計劃
表中的四條數(shù)據(jù)查詢語句以及查看explain的語句無索引給timeStamp加索引刪除timeStamp索引給price加索引同時給timeStamp和price加單索引創(chuàng)建timeStamp和price的聯(lián)合索引創(chuàng)建price和timeStamp的聯(lián)合索引總結(jié)
以上是生活随笔為你收集整理的springboot mongo查询固定字段_你真的会用索引么?[Mongo]的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .Net里一个用于驱动摄像头的类
- 下一篇: 黑客主要攻击方式