數據庫索引的實現原理

索引問題就是一個查找問題。。。

數據庫查詢是數據庫的最主要功能之一。我們都希望查詢數據的速度能儘可能的快,因此數據庫系統的設計者會從查詢算法的角度進行優化。最基本的查詢算法當然是順序查找(linear search),這種複雜度爲O(n)的算法在數據量很大時顯然是糟糕的,好在計算機科學的發展提供了很多更優秀的查找算法,例如二分查找(binary search)、二叉樹查找(binary tree search)等。如果稍微分析一下會發現,每種查找算法都只能應用於特定的數據結構之上,例如二分查找要求被檢索數據有序,而二叉樹查找只能應用於二叉查找樹上,但是數據本身的組織結構不可能完全滿足各種數據結構(例如,理論上不可能同時將兩列都按順序進行組織),所以,在數據之外,數據庫系統還維護着滿足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就可以在這些數據結構上實現高級查找算法。這種數據結構,就是索引

數據庫索引,是數據庫管理系統中一個排序的數據結構,以協助快速查詢、更新數據庫表中數據。索引的實現通常使用B樹及其變種B+樹

在數據之外,數據庫系統還維護着滿足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就可以在這些數據結構上實現高級查找算法。這種數據結構,就是索引。

爲表設置索引要付出代價的:一是增加了數據庫的存儲空間二是在插入和修改數據時要花費較多的時間(因爲索引也要隨之變動)


上圖展示了一種可能的索引方式。左邊是數據表,一共有兩列七條記錄,最左邊的是數據記錄的物理地址(注意邏輯上相鄰的記錄在磁盤上也並不是一定物理相鄰的)。爲了加快Col2的查找,可以維護一個右邊所示的二叉查找樹,每個節點分別包含索引鍵值和一個指向對應數據記錄物理地址的指針,這樣就可以運用二叉查找在O(log2n)的複雜度內獲取到相應數據。


創建索引可以大大提高系統的性能。

第一,通過創建唯一性索引,可以保證數據庫表中每一行數據的唯一性。

第二,可以大大加快數據的檢索速度,這也是創建索引的最主要的原因。

第三,可以加速表和表之間的連接,特別是在實現數據的參考完整性方面特別有意義。

第四,在使用分組和排序子句進行數據檢索時,同樣可以顯著減少查詢中分組和排序的時間。

第五,通過使用索引,可以在查詢的過程中,使用優化隱藏器,提高系統的性能。 


也許會有人要問:增加索引有如此多的優點,爲什麼不對錶中的每一個列創建一個索引呢?因爲,增加索引也有許多不利的方面。

第一,創建索引和維護索引要耗費時間,這種時間隨着數據量的增加而增加。

第二,索引需要佔物理空間,除了數據表佔數據空間之外,每一個索引還要佔一定的物理空間,如果要建立聚簇索引,那麼需要的空間就會更大。

第三,當對錶中的數據進行增加、刪除和修改的時候,索引也要動態的維護,這樣就降低了數據的維護速度。


索引是建立在數據庫表中的某些列的上面。在創建索引的時候,應該考慮在哪些列上可以創建索引,在哪些列上不能創建索引。一般來說,應該在這些列上創建索引:在經常需要搜索的列上,可以加快搜索的速度;在作爲主鍵的列上,強制該列的唯一性和組織表中數據的排列結構;在經常用在連接的列上,這些列主要是一些外鍵,可以加快連接的速度;在經常需要根據範圍進行搜索的列上創建索引,因爲索引已經排序,其指定的範圍是連續的;在經常需要排序的列上創建索引,因爲索引已經排序,這樣查詢可以利用索引的排序,加快排序查詢時間;在經常使用在WHERE子句中的列上面創建索引,加快條件的判斷速度。


同樣,對於有些列不應該創建索引。一般來說,不應該創建索引的的這些列具有下列特點:

第一,對於那些在查詢中很少使用或者參考的列不應該創建索引。這是因爲,既然這些列很少使用到,因此有索引或者無索引,並不能提高查詢速度。相反,由於增加了索引,反而降低了系統的維護速度和增大了空間需求。

第二,對於那些只有很少數據值的列也不應該增加索引。這是因爲,由於這些列的取值很少,例如人事表的性別列,在查詢的結果中,結果集的數據行佔了表中數據行的很大比例,即需要在表中搜索的數據行的比例很大。增加索引,並不能明顯加快檢索速度。

第三,對於那些定義爲text, image和bit數據類型的列不應該增加索引。這是因爲,這些列的數據量要麼相當大,要麼取值很少。

第四,當修改性能遠遠大於檢索性能時,不應該創建索引。這是因爲,修改性能和檢索性能是互相矛盾的。當增加索引時,會提高檢索性能,但是會降低修改性能。當減少索引時,會提高修改性能,降低檢索性能。因此,當修改性能遠遠大於檢索性能時,不應該創建索引。


根據數據庫的功能,可以在數據庫設計器中創建三種索引:唯一索引、主鍵索引和聚集索引

唯一索引 

唯一索引是不允許其中任何兩行具有相同索引值的索引。

當現有數據中存在重複的鍵值時,大多數數據庫不允許將新創建的唯一索引與表一起保存。數據庫還可能防止添加將在表中創建重複鍵值的新數據。例如,如果在employee表中職員的姓(lname)上創建了唯一索引,則任何兩個員工都不能同姓。
主鍵索引
數據庫表經常有一列或列組合,其值唯一標識表中的每一行。該列稱爲表的主鍵。
在數據庫關係圖中爲表定義主鍵將自動創建主鍵索引,主鍵索引是唯一索引的特定類型。該索引要求主鍵中的每個值都唯一。當在查詢中使用主鍵索引時,它還允許對數據的快速訪問。
聚集索引
在聚集索引中,表中行的物理順序與鍵值的邏輯(索引)順序相同。一個表只能包含一個聚集索引。

如果某索引不是聚集索引,則表中行的物理順序與鍵值的邏輯順序不匹配。與非聚集索引相比,聚集索引通常提供更快的數據訪問速度。



局部性原理與磁盤預讀

由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此爲了提高效率,要儘量減少磁盤I/O。爲了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向後讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中著名的局部性原理當一個數據被用到時,其附近的數據也通常會馬上被使用。程序運行期間所需要的數據通常比較集中。

由於磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高I/O效率。

預讀的長度一般爲頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割爲連續的大小相等的塊,每個存儲塊稱爲一頁(在許多操作系統中,頁得大小通常爲4k),主存和磁盤以頁爲單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,然後異常返回,程序繼續運行。

B-/+Tree索引的性能分析

到這裏終於可以分析B-/+Tree索引的性能了。

上文說過一般使用磁盤I/O次數評價索引結構的優劣。先從B-Tree分析,根據B-Tree的定義,可知檢索一次最多需要訪問h個節點。數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。爲了達到這個目的,在實際實現B-Tree還需要使用如下技巧:

每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁裏,加之計算機存儲分配都是按頁對齊的,就實現了一個node只需一次I/O。

B-Tree中一次檢索最多需要h-1次I/O(根節點常駐內存),漸進複雜度爲O(h)=O(logdN)。一般實際應用中,出度d是非常大的數字,通常超過100,因此h非常小(通常不超過3)。

而紅黑樹這種結構,h明顯要深的多。由於邏輯上很近的節點(父子)物理上可能很遠,無法利用局部性,所以紅黑樹的I/O漸進複雜度也爲O(h),效率明顯比B-Tree差很多。


綜上所述,用B-Tree作爲索引結構效率是非常高的。



應該花時間學習B-樹和B+樹數據結構

=============================================================================================================

1)B樹

B樹中每個節點包含了鍵值和鍵值對於的數據對象存放地址指針,所以成功搜索一個對象可以不用到達樹的葉節點。

成功搜索包括節點內搜索和沿某一路徑的搜索,成功搜索時間取決於關鍵碼所在的層次以及節點內關鍵碼的數量。

在B樹中查找給定關鍵字的方法是:首先把根結點取來,在根結點所包含的關鍵字K1,…,kj查找給定的關鍵字(可用順序查找或二分查找法),若找到等於給定值的關鍵字,則查找成功;否則,一定可以確定要查的關鍵字在某個Ki或Ki+1之間,於是取Pi所指的下一層索引節點塊繼續查找,直到找到,或指針Pi爲空時查找失敗。

2)B+樹

B+樹非葉節點中存放的關鍵碼並不指示數據對象的地址指針,非也節點只是索引部分。所有的葉節點在同一層上,包含了全部關鍵碼和相應數據對象的存放地址指針,且葉節點按關鍵碼從小到大順序鏈接。如果實際數據對象按加入的順序存儲而不是按關鍵碼次數存儲的話,葉節點的索引必須是稠密索引,若實際數據存儲按關鍵碼次序存放的話,葉節點索引時稀疏索引。

B+樹有2個頭指針,一個是樹的根節點,一個是最小關鍵碼的葉節點。

所以 B+樹有兩種搜索方法:

一種是按葉節點自己拉起的鏈表順序搜索。

一種是從根節點開始搜索,和B樹類似,不過如果非葉節點的關鍵碼等於給定值,搜索並不停止,而是繼續沿右指針,一直查到葉節點上的關鍵碼。所以無論搜索是否成功,都將走完樹的所有層。

B+ 樹中,數據對象的插入和刪除僅在葉節點上進行。

這兩種處理索引的數據結構的不同之處:
a,B樹中同一鍵值不會出現多次,並且它有可能出現在葉結點,也有可能出現在非葉結點中。而B+樹的鍵一定會出現在葉結點中,並且有可能在非葉結點中也有可能重複出現,以維持B+樹的平衡。
b,因爲B樹鍵位置不定,且在整個樹結構中只出現一次,雖然可以節省存儲空間,但使得在插入、刪除操作複雜度明顯增加。B+樹相比來說是一種較好的折中。
c,B樹的查詢效率與鍵在樹中的位置有關,最大時間複雜度與B+樹相同(在葉結點的時候),最小時間複雜度爲1(在根結點的時候)。而B+樹的時候複雜度對某建成的樹是固定的。


爲什麼需要索引

數據在磁盤上是以塊的形式存儲的。爲確保對磁盤操作的原子性,訪問數據的時候會一併訪問所有數據塊。磁盤上的這些數據塊與鏈表類似,即它們都包含一個數據段和一個指針,指針指向下一個節點(數據塊)的內存地址,而且它們都不需要連續存儲(即邏輯上相鄰的數據塊在物理上可以相隔很遠)。

鑑於很多記錄只能做到按一個字段排序,所以要查詢某個未經排序的字段,就需要使用線性查找,即要訪問N/2個數據塊,其中N指的是一個表所涵蓋的所有數據塊。如果該字段是非鍵字段(也就是說,不包含唯一值),那麼就要搜索整個表空間,即要訪問全部N個數據塊。

然而,對於經過排序的字段,可以使用二分查找,因此只要訪問log2 N個數據塊。同樣,對於已經排過序的非鍵字段,只要找到更大的值,也就不用再搜索表中的其他數據塊了。這樣一來,性能就會有實質性的提升。

什麼是索引

索引是對記錄按照多個字段進行排序的一種方式。對錶中的某個字段建立索引會創建另一種數據結構,其中保存着字段的值,每個值又指向與它相關的記錄。這種索引的數據結構是經過排序的,因而可以對其執行二分查找。

索引的缺點是佔用額外的磁盤空間。因爲索引保存在MyISAM數據庫中,所以如果爲同一個表中的很多字段都建立索引,那這個文件可能會很快膨脹到文件系統規定的上限。

索引的原理

首先,來看一個示例數據庫表的模式:

<code style="padding: 0px; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; color: inherit; border: 0px; background-color: transparent;"><span class="pun" style="color: rgb(0, 0, 0);">字段名</span><span class="pln" style="color: rgb(0, 0, 0);">              </span><span class="pun" style="color: rgb(0, 0, 0);">數據類型</span><span class="pln" style="color: rgb(0, 0, 0);">         </span><span class="pun" style="color: rgb(0, 0, 0);">在磁盤上的大小</span><span class="pln" style="color: rgb(0, 0, 0);">
id </span><span class="pun" style="color: rgb(0, 0, 0);">(</span><span class="typ" style="color: rgb(43, 145, 175);">Primary</span><span class="pln" style="color: rgb(0, 0, 0);"> key</span><span class="pun" style="color: rgb(0, 0, 0);">)</span><span class="pln" style="color: rgb(0, 0, 0);">   </span><span class="typ" style="color: rgb(43, 145, 175);">Unsigned</span><span class="pln" style="color: rgb(0, 0, 0);"> INT     </span><span class="lit" style="color: rgb(128, 0, 0);">4</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="pun" style="color: rgb(0, 0, 0);">字節</span><span class="pln" style="color: rgb(0, 0, 0);">
firstName          </span><span class="typ" style="color: rgb(43, 145, 175);">Char</span><span class="pun" style="color: rgb(0, 0, 0);">(</span><span class="lit" style="color: rgb(128, 0, 0);">50</span><span class="pun" style="color: rgb(0, 0, 0);">)</span><span class="pln" style="color: rgb(0, 0, 0);">         </span><span class="lit" style="color: rgb(128, 0, 0);">50</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="pun" style="color: rgb(0, 0, 0);">字節</span><span class="pln" style="color: rgb(0, 0, 0);">
lastName           </span><span class="typ" style="color: rgb(43, 145, 175);">Char</span><span class="pun" style="color: rgb(0, 0, 0);">(</span><span class="lit" style="color: rgb(128, 0, 0);">50</span><span class="pun" style="color: rgb(0, 0, 0);">)</span><span class="pln" style="color: rgb(0, 0, 0);">         </span><span class="lit" style="color: rgb(128, 0, 0);">50</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="pun" style="color: rgb(0, 0, 0);">字節</span><span class="pln" style="color: rgb(0, 0, 0);">
emailAddress       </span><span class="typ" style="color: rgb(43, 145, 175);">Char</span><span class="pun" style="color: rgb(0, 0, 0);">(</span><span class="lit" style="color: rgb(128, 0, 0);">100</span><span class="pun" style="color: rgb(0, 0, 0);">)</span><span class="pln" style="color: rgb(0, 0, 0);">        </span><span class="lit" style="color: rgb(128, 0, 0);">100</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="pun" style="color: rgb(0, 0, 0);">字節</span></code>

注意:這裏用char而不用varchar是爲了精確地描述數據佔用磁盤的大小。這個示例數據庫中包含500萬行記錄,而且沒有建立索引。接下來我們就分析針對這個表的兩個查詢:一個查詢使用id(經過排序的鍵字段),另一個查詢使用firstName(未經排序的非鍵字段)。

示例分析一

對於這個擁有r = 5 000 000條記錄的示例數據庫,在磁盤上要爲每條記錄分配 R = 204字節的固定存儲空間。這個表保存在MyISAM數據庫中,而這個數據庫默認的數據庫塊大小爲 B = 1024字節。於是,我們可計算出這個表的分塊因數爲 bfr = (B/R) = 1024/204 = 5,即磁盤上每個數據塊保存5條記錄。那麼,保存整個表所需的數據塊數就是 N = (r/bfr) = 5000000/5 = 1 000 000。

使用線性查找搜索id字段——這個字段是鍵字段(每個字段的值唯一),需要訪問 N/2 = 500 000個數據塊才能找到目標值。不過,因爲這個字段是經過排序的,所以可以使用二分查找法,而這樣平均只需要訪問log2 1000000 = 19.93 = 20 個塊。顯然,這會給性能帶來極大的提升。

再來看看firstName字段,這個字段是未經排序的,因此不可能使用二分查找,況且這個字段的值也不是唯一的,所以要從表的開頭查找末尾,即要訪問 N = 1 000 000個數據塊。這種情況通過建立索引就能得到改善。

如果一條索引記錄只包含索引字段和一個指向原始記錄的指針,那麼這條記錄肯定要比它所指向的包含更多字段的記錄更小。也就是說,索引本身佔用的磁盤空間比原來的表更少,因此需要遍歷的數據塊數也比搜索原來的表更少。以下是firstName字段索引的模式:

<code style="padding: 0px; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; color: inherit; border: 0px; background-color: transparent;"><span class="pun" style="color: rgb(0, 0, 0);">字段名</span><span class="pln" style="color: rgb(0, 0, 0);">         </span><span class="pun" style="color: rgb(0, 0, 0);">數據類型</span><span class="pln" style="color: rgb(0, 0, 0);">        </span><span class="pun" style="color: rgb(0, 0, 0);">在磁盤上的大小</span><span class="pln" style="color: rgb(0, 0, 0);">
firstName     </span><span class="typ" style="color: rgb(43, 145, 175);">Char</span><span class="pun" style="color: rgb(0, 0, 0);">(</span><span class="lit" style="color: rgb(128, 0, 0);">50</span><span class="pun" style="color: rgb(0, 0, 0);">)</span><span class="pln" style="color: rgb(0, 0, 0);">        </span><span class="lit" style="color: rgb(128, 0, 0);">50</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="pun" style="color: rgb(0, 0, 0);">字節</span><span class="pln" style="color: rgb(0, 0, 0);">
</span><span class="pun" style="color: rgb(0, 0, 0);">(記錄指針)</span><span class="pln" style="color: rgb(0, 0, 0);">    </span><span class="typ" style="color: rgb(43, 145, 175);">Special</span><span class="pln" style="color: rgb(0, 0, 0);">         </span><span class="lit" style="color: rgb(128, 0, 0);">4</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="pun" style="color: rgb(0, 0, 0);">字節</span></code>

注意:在MySQL中,根據表的大小,指針的大小可能是2、3、4或5字節。

示例分析二

對於這個擁有r = 5 000 000條記錄的示例數據庫,每條索引記錄要佔用 R = 54字節磁盤空間,而且同樣使用默認的數據塊大小 B = 1024字節。那麼索引的分塊因數就是 bfr = (B/R) = 1024/54 = 18。最終這個表的索引需要佔用 N = (r/bfr) = 5000000/18 = 277 778個數據塊。

現在,再搜索firstName字段就可以使用索引來提高性能了。對索引使用二分查找,需要訪問 log2 277778 = 18.09 = 19個數據塊。再加上爲找到實際記錄的地址還要訪問一個數據塊,總共要訪問 19 + 1 = 20個數據塊,這與搜索未索引的表需要訪問277 778個數據塊相比,不啻於天壤之別。

什麼時候用索引

創建索引要額外佔用磁盤空間(比如,上面例子中要額外佔用277 778個數據塊),建立的索引太多可能導致磁盤空間不足。因此,在建立索引時,一定要慎重選擇正確的字段。

由於索引只能提高搜索記錄中某個匹配字段的速度,因此在執行插入和刪除操作的情況下,僅爲輸出結果而爲字段建立索引,就純粹是浪費磁盤空間和處理時間了;這種情況下不用建立索引。另外,由於二分查找的原因,數據的基數性(cardinality)或唯一性也非常重要。對基數性爲2的字段建立索引,會將數據一分爲二,而對基數性爲1000的字段,則同樣會返回大約1000條記錄。在這麼低的基數性下,索引的效率將減低至線性查找的水平,而查詢優化器會在基數性大於記錄數的30%時放棄索引,實際上等於索引純粹只會浪費空間。

查詢優化器的原理

查詢優化中最核心的問題就是精確估算不同查詢計劃的成本。優化器在估算查詢計劃的成本時,會使用一個數學模型,該模型又依賴於對每個查詢計劃中涉及的最大數據量的基數性(或者叫重數)的估算。而對基數性的估算又依賴於對查詢中謂詞選擇因數(selection factor of predicates)的估算。過去,數據庫系統在估算選擇性時,要使用每個字段中值的分佈情況的詳盡統計信息,比如直方圖。這種技術對於估算孤立謂詞的選擇符效果很好。然而,很多查詢的謂詞是相互關聯的,例如 select count(*) from R where R.make='Honda' and R.model='Accord'。查詢謂詞經常會高度關聯(比如,model='Accord'的前提條件是make='Honda'),而估計這種關聯的選擇性非常困難。查詢優化器之所以會選擇低劣的查詢計劃,一方面是因爲對基數性估算不準,另一方面就是因爲遺漏了很多關聯性。而這也是爲什麼數據庫管理員應該經常更新數據庫統計信息(特別是在重要的數據加載和卸載之後)的原因。


發佈了27 篇原創文章 · 獲贊 5 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章