咱們常說的「數據庫」,好比「MySQL」、「Oracle」等,其實嚴格來講是DBMS(Database Management System),數據庫只是一個存儲數據着數據的倉庫,而DBMS作的事是讓咱們可以操做數據庫,好比解析SQL、DML等,都是DBMS在支持着。 在DBMS之下,又有着存儲引擎,爲DBMS提供數據增刪改查的支持,不一樣的存儲引擎提供不一樣的特性,負責組織數據的就是存儲引擎。算法
每個字段都須要定義一個數據類型(DataType),數據類型在數據組織時的意義是肯定數據長度,存儲介質將會爲其分配合適長度的空間。 每一個字段被按照順序組織起來,而且在開頭存儲着這一行的某些頭信息(MetaData),例如記錄總長度、時間戳等,這就組成了一條在硬盤上被存儲的完整記錄: 數據庫
若是是變長記錄,好比某一個字段是varchar(256),則會在頭信息後緊接着存儲這一列的指針,指向這個字段的值存儲地址(通常是在記錄的末尾): 因爲每次從磁盤中讀取數據的單位是頁(Page),爲了保證讀取速度,通常數據庫都會要求一條記錄的長度不超過一頁(頁的長度不固定,能夠被設置,通常是4kb),保證一次讀取就能讀到一條完整記錄,而不須要跨頁讀取,所以對於某些存儲着大數據的字段,好比圖片、視頻,他們每每被獨立存儲到其餘文件,而不與記錄中的其餘小字段存儲在一塊兒。行是一條具備完整意義的記錄,被按照必定的規則,依次存儲在文件中。 記錄在文件中有如下幾種主要的組織方法:緩存
記錄與記錄之間沒有順序關係,每條記錄能夠存放在文件中的任何地方,只要想被存儲的地址有足夠空間。數據結構
也就是遵循某個搜索碼(Search key)的順序,依次存儲每一條記錄。搜索碼是一系列搜索條件的組合,能夠是一個鍵,也能夠是多個鍵組合。以下圖,按照第二列,也就是姓名的順序去決定記錄的存儲順序: 數據結構和算法
這種方式很是利於順序讀取,由於連續的記錄都保存在連續的頁中,可一次性讀出多個頁得到批量的記錄,而無需屢次尋址屢次讀取。 可是這種方式不利於記錄的刪除和插入,由於刪除記錄時須要將後面的記錄依次前移,插入記錄時須要將後面的記錄依次後移,若是受影響的記錄多,效率將會很低。 爲了解決每次插入刪除數據都須要移動後面的記錄的問題,現實中可能採起了指針,也就是每一條記錄中保存着指向下一條記錄的指針,當有記錄被刪除時,僅僅修改指針,避免記錄的移動;當有記錄插入時,先檢查在合適的位置是否有空閒空間,若是沒有,採用開闢 溢出塊的方式,不直接插入至合適位置,而是在其餘空間存儲這條記錄,而後修改上一條記錄的指針指向這條新插入的記錄,去避免大量記錄的移動: 可是若是大量的記錄都存儲在溢出塊中,順序文件自己所帶來的好處就大打折扣,由於不少記錄已經再也不在物理上按順序存儲,那麼順序獲取記錄時,就可能須要屢次尋址屢次讀取,而不能經過一次性讀取連續的頁來獲取連續的記錄。此時,文件就須要被 重組,會將全部記錄從新組織成徹底物理鄰接的文件。前面提到的順序文件是不一樣的表存儲在不一樣的文件中,可是某些具體應用場景下,可能經常涉及多表查詢,好比有一個名爲Singers的表保存着歌手信息,又有一個名爲Albums的表保存着每一個歌手發佈的專輯信息,若是你正在開發一個音樂播放器,那麼涉及的場景通常都是須要找出某個歌手發佈的全部專輯展現給客戶,若是不一樣的表保存在不一樣的文件中,那麼須要進行鏈接(Join) ,複雜度比較高,可是若是將每一個歌手的專輯信息都在物理上存儲在歌手信息以後,也就是兩張表混合存放在同一個文件中: 函數
採用聚簇文件則不須要進行Join操做,找到Singer後,直接順序讀取後面的頁,就能拿到指定歌手的全部專輯,提升了查詢效率。散列文件徹底沒有順序,每條記錄應該存放的位置,是根據搜索碼的Hash值決定的,所以插入刪除都不涉及記錄移動,且因爲搜索碼的Hash值直接決定了存儲位置,因此查找符合特定搜索碼的記錄很是快,可是不支持範圍查找與順序讀取。大數據
DBMS維護着本身的緩存空間,使用一些緩存置換算法儘可能確保那些常常被使用的數據在緩存中,以免磁盤的讀取。與DBMS同樣,磁盤通常也有着本身的緩衝區以保存常常被讀取的數據,減小響應時間。所以,若是要讀取一條記錄,根據優先順序,路徑爲DBMS緩存區 => 磁盤緩存區 => 磁盤。優化
這是成本最低的方式,由於DBMS緩存區就在內存,能夠直接被CPU使用,不涉及磁盤IO,能夠考慮IO時間爲0。操作系統
若是磁盤緩存區有須要的記錄,則只須要直接讀出,傳輸時間考慮爲1ms。設計
因爲SSD比較貴,經常使用的仍是機械硬盤,對於機械硬盤,要讀取指定地址的數據,是須要通過尋道的,機械臂須要先移動到指定位置,所以不管讀取多少數據,準備工做都會耗費一段時間。 整個IO流程包括:排隊等待 => 尋道 => 半圈旋轉 => 傳輸
一次隨機讀取中,有90%的時間都花費在排隊和準備工做,真正的傳輸時間只有1ms,隨機讀取10頁,就須要10*10=100ms,但若是是順序讀,對於傳輸速度爲40MB/s的硬盤,讀取一個4kb的頁僅須要0.1ms,即便順序讀取100頁,也只須要1頁隨機讀99頁順序讀,也就是10ms+9.9ms=19.9ms,速度差距幾十倍,這也是爲什麼咱們想要儘可能保證須要讀取的數據都在物理上排列在一塊兒,由於這樣就能夠順序讀取多個頁,而不須要進行屢次隨機讀取。 所以對於數據讀取速度的優化,主要就是須要下降IO時間,而下降IO時間的關鍵,就在於減小隨機讀次數以及讀取更少的數據。 合適的索引將會很大程度上地幫助咱們實現這個目標。考慮一種狀況:咱們有一張存儲着100萬個註冊用戶的Users表,咱們要搜索用戶名爲AfterShip
的用戶,若是這張表是使用順序文件存儲,而且存儲順序是根據account_id
列,而不是根據username
列,在沒有索引時,查找的方式應該是從第一條記錄起依次讀入記錄,並對比每一條記錄的username
是否爲AfterShip
,直到找到爲止。最好的狀況是第一條記錄即符合要求,最壞的狀況是最後一條記錄才符合要求,在最壞的狀況下,須要讀取100萬條記錄,假設每條記錄1kb,須要讀取976MB的數據!即便以200MB/s的傳輸速度,僅僅是IO時間就須要5s讀取記錄,而且還須要大量的時間給CPU處理100萬條的記錄。 若是是以account_id
做爲搜索條件,最快的方式是從文件的最中間位置讀出最中間記錄,對比account_id
的大小,再判斷往前仍是日後讀,也就是使用2分搜索,最壞的狀況下須要進行logN次,也就是20次左右的隨機讀,耗時200ms。 所以,當咱們的搜索碼被順序地組織起來,咱們就能更少地讀取數據,以更快的方式查詢到符合要求的記錄,可是,文件只能以一種搜索碼組織起來,不能既以account_id
爲順序,又以username
爲順序,所以,咱們須要一種冗餘的數據——索引,來以咱們想要的順序組織某個搜索碼,加速咱們的查詢。
索引是一種被以合適的數據結構組織起來方便搜索的冗餘數據,也存儲在文件中。 好比對於Users表,咱們爲username
創建索引,那麼DBMS會將username
的值複製一份,並排序,保存在一個文件中:
username
爲
AfterShip
的記錄,就可使用二分搜索,或者哪怕是順序掃描整個索引也比以前進行全表掃描快得多,由於一個username的長度若是是50bytes,那麼掃描整個索引也只須要讀取不到50MB的索引文件,體積只是全表掃描的二十分之一。 因而可知,索引能夠有效地加快查詢速度。剛剛講到的是順序索引,在索引的具體實現中還有多種更復雜的數據結構和算法,索引有多種實現方式,每種實現方式都各有優缺點,適應不一樣的應用環境。
索引能夠從多個維度分類,每一個維度的分類互不衝突。
username
創建的索引。查找速度: 對於稠密索引,因爲爲每個搜索碼都創建的相應的索引項,所以空間佔用比較大,可是查找速度較快,由於能夠從索引文件中直接找到對應記錄的位置,而使用稀疏索引須要先找到記錄所在的頁,再讀出整個頁,從頁中找到具體的記錄。 維護成本: 稠密索引爲每一個搜索碼都創建對應的索引項,且索引項中還保存着符合此搜索碼的全部記錄的指針,也就是關聯到了表中的每一條記錄,所以當任何一條記錄被刪除、插入,都須要修改甚至移動、重組索引文件,維護成本較高。 而稀疏索引僅僅爲分組創建索引項,當組中有記錄刪除時不必定會立刻修改索引,有記錄插入到現有的組時,只要不佔用新的頁或者影響到組的第一條記錄,那麼也不會創建新的索引,索引更新相對不那麼頻繁,維護成本較小。
繼續考慮那張存有100萬條用戶數據的Users表,咱們爲username
創建了有序稠密索引,而且咱們假設username
是具有惟一性的,也就是對於100萬個用戶,就有100萬個不一樣的username
,稠密索引將會有100萬條索引項,若是一個4kb的頁能保存100條索引項,那麼就須要1萬頁來保存整個索引文件。若是咱們要查詢username
爲AfterShip
的用戶,使用二分法就須要進行logN次的查詢,也就是14次隨機讀找到其索引項,再經過一次隨機讀讀出記錄,一共150ms,一秒內只能進行6次查詢。若是咱們能減小其隨機讀次數,那麼每少一次隨機讀,就會少10ms的耗時,減小隨機讀有如下兩個思路:
若是咱們基於100萬條稠密索引再去創建稀疏索引,也就是對1萬個頁創建索引,那麼對於一頁能保存100條索引項的狀況下,咱們將會有更上一級的,僅佔用100頁的稀疏索引,整個索引文件爲400kb,足夠小到可以放入內存,所以能夠保存在DBMS緩存區,先經過稀疏索引找到稠密索引所在的頁地址,再進行一次隨機讀,讀出整個頁,找到搜索碼對應的具體索引項,而後再進行第二次隨機讀,讀出表中記錄,一共只有1次內存讀+2次隨機讀,20ms。僅僅多創建一層稀疏索引,也便是使用二級索引結構,就有7倍的效率提高。在現實場景中,每每會屢次進行這種索引結構的創建,也就是多級索引結構。
B+樹是一種多級索引的實現,採用平衡樹結構,有非頁節點、葉節點兩種節點組成,每種節點存放的數據有細微差異:
非葉節點(根節點也是非葉節點): 節點最多包含着n-1個搜索碼值K1…Kn-1,幷包含着n個指針P1…Pn,也就是兩邊是指針,中間是搜索碼值,Pn指針指向小於其Kn搜索碼的下一級索引節點,Pn+1指向大於等於Kn搜索碼的下一級索引節點。
舉個栗子:葉節點 葉節點的P1…Pn-1的指針都指向記錄地址(若是是稠密索引)或者頁地址(若是是稀疏索引),葉節點的最後一個指針Pn與非葉節點不一樣,它指向的是下一個同級葉節點,構成橫向有序的索引結構。
一個完整的三級B+樹以下所示:
B+樹的葉節點中的搜索碼值是能夠重複的,當這個B+樹索引是非聚簇稠密索引且搜索碼對應的記錄不惟一時,就須要將一個搜索碼重複放置在葉節點中,指向不一樣的記錄:B+樹維護成本 考慮一個稠密B+樹索引,在刪除記錄時,因爲B+樹要求每一個葉節點都必須處於半滿狀態,當被刪除索引項所處的節點不知足半滿時,須要向兄弟節點借搜索碼值,而且在須要時調整父節點,是一個局部重組B+樹的過程。 在插入記錄時,可能出現某個索引節點已經沒有多餘空間存儲,此時則須要分裂葉節點,而且上層非葉節點也可能須要分裂,依次往上遞歸,也是一次重組的過程。
B+樹的優勢 從上面可以看出,在某些狀況下,刪除和插入記錄時,B+樹的維護成本比較高,可是爲什麼依舊是最經常使用的索引結構之一呢,由於咱們每每會把每一個節點的空間設置得足夠大,通常是一整頁,若是一個索引項佔用100bytes,則對於4kb的頁可以存儲40個索引項,即便是100萬條記錄的表,B+樹也只須要log(40)1000000=3層,查詢路徑很是短,所以B+樹其實是一種效率很是高的索引結構。
####(2)B樹索引 這是一種與B+樹相似的平衡樹索引,區別在於,B+樹只有葉節點保存着指向記錄的指針,非葉節點僅僅是索引着索引的索引,而B樹整棵樹的全部節點都保存着指向其對應記錄的指針,整棵樹纔是一個完整的索引:
B樹不常被應用,由於在B樹種範圍查詢的效率很是低,B+樹中全部葉節點被連接起來成爲有序鏈表,能夠方便地遍歷所需範圍的數據,而B樹則須要更加複雜的算法去遍歷多個層次的節點才能獲取到必定範圍內的數據。前面講到的索引結構都須要經過對比搜索碼的大小去查找索引項的位置,複雜度是對數級別的,而散列索引將存儲空間分爲多個組,稱爲桶(Bucket),直接經過散列函數計算搜索碼的Hash值,經過Hash值肯定此搜索碼的索引項在哪一個桶中,讀取桶中的索引項,就能夠找到對應索引項,複雜度爲O(1),所以散列索引對於查詢指定搜索碼的效率很是高。 根據桶的數量是否固定,散列索引分爲靜態散列與動態散列兩種:
目前爲止咱們討論的都是搜索碼爲一個字段的狀況,其實搜索碼能夠是多個字段的組合,好比index(username,age,city),索引項中按照索引定義次序依次存儲着三個字段的值,好比(AfterShip,25,ShenZhen),索引項之間的排序先根據第一列索引排序,第一列相同的狀況下再根據第二列排序,以此類推。
覆蓋索引不是一種索引分類,而是一種對索引的使用方式。 繼續考慮上面那張保存着100萬用戶的Users表,咱們要查找username
爲AfterShip
的用戶的email
,若是咱們僅僅爲username
創建索引,那麼咱們須要先經過索引查找到username
爲AfterShip
的帳號的記錄指針,再回表讀取此記錄email
列的值。但若是咱們的索引是爲(username,email)創建的複合索引,那麼咱們在索引項中就能直接獲取到email
值,而不須要回表讀取,減小一次隨機IO操做。 所以,適當地利用覆蓋索引,能夠減小IO,加快查詢。