索引的基本原理以及數據是如何被訪問的

(一)SQLS如何訪問沒有建立索引的數據表
  Heap譯成漢語叫做“堆”,其本義暗含雜亂無章、無序的意思,前面提到數據值被寫進數據頁時,由於每一行記錄之間並沒有特定的排列順序,所以行與行的順序就是隨機無序的,當然表中的數據頁也就是無序的了,而表中所有數據頁就形成了“堆”。可以說,一張沒有索引的數據表,就像一個只有書櫃而沒有索引卡片櫃的圖書館,書庫裏面塞滿了一堆亂七八糟的圖書。當讀者對管理員提交查詢請求後,管理員就一頭鑽進書庫,對照查找內容從頭開始一架一櫃的逐本查找。運氣好的話,在第一個書架的第一本書就  找到了,運氣不好的話,要到最後一個書架的最後一本書才找到。
  SQLS在接到查詢請求時,首先會分析sysindexes表中一個叫做索引標誌符(INDID: Index ID)的字段的值,如果該值爲0,表示這是一張數據表而不是索引表,SQLS就會使用sysindexes表的另一個字段——也就是在前面提到過的FirstIAM值中找到該表的IAM頁鏈,也就是所有數據頁集合。
  這就是對一個沒有建立索引的數據表進行數據查找的方式,是不是很沒效率?對於沒有索引的表,對於一“堆”這樣的記錄,SQLS也只能這樣做,而且更沒勁的是,即使在第一行就找到了被查詢的記錄,SQLS仍然要從頭到尾的將表掃描一次。這種查詢稱爲“遍歷”,又叫“表掃描”。
  可見沒有建立索引的數據表照樣可以運行,不過這種方法對於小規模的表來說沒有什麼太大的問題,但要查詢海量的數據效率就太低了。

(二)SQLS如何訪問建立了非聚集索引的數據表
  如前所述,非聚集索引可以建多個,具有B樹結構,其葉級節點不包含數據頁,只包含索引行。假定一個表中只有非聚集索引,則每個索引行包含了非聚集索引鍵值以及行定位符(ROW ID,RID),他們指向具有該鍵值的數據行,每一個RID由文件ID、頁編號和在頁中行的編號組成。
  當INDID的值在2至250之間時,意味着表中存在非聚集索引頁。此時,SQLS調用ROOT字段的值指向非聚集索引B樹的ROOT,在其中查找與被查詢最相近的值,根據這個值找到在非葉級節點中的頁號,然後順藤摸瓜,在葉級節點相應的頁面中找到該值的RID,最後根據這個RID在Heap中定位所在的頁和行並返回到查詢端。
  例如:假定在Lastname上建立了非聚集索引,則執行Select * From Member Where Lastname=’Ota’時,查詢過程是:
  ①SQLS查詢INDID值爲2;
  ②立即從根出發,在非葉級節點中定位最接近Ota的值“Martin”,並查到其位於葉級頁面的第61頁;
  ③僅在葉級頁面的第61頁的Martin下搜尋Ota的RID,其RID顯示爲N∶706∶4,表示Lastname字段中名  爲Ota的記錄位於堆的第706頁的第4行,N表示文件的ID值,與數據無關;
  ④根據上述信息,SQLS立刻在堆的第706頁第4行將該記錄“揪”出來並顯示於前臺(客戶端)。視表的數據量大小,整個查詢過程費時從百分之幾毫秒到數毫秒不等。
  在談到索引基本概念的時候,我們就提到了這種方式:圖書館的前臺有很多索引卡片櫃,裏面分了若干的類別,諸如按照書名筆畫或拼音順序、作者筆畫或拼音順序等,但有兩點不同之處:
  ① 索引卡片上記錄了每本書擺放的具體位置——位於某櫃某架的第幾本——而不是“特殊編號”;
  ② 書脊上並沒有那個“特殊編號”。管理員在索引櫃中查到所需圖書的具體位置(RID)後,根據RID直接在書庫中的具體位置將書提出來。
  顯然,這種查詢方式效率很高,但資源佔用極大,因爲書庫中書的位置隨時在發生變化,必然要求管理員花費額外的精力和時間隨時做好索引更新。

(三)SQLS如何訪問建立聚集索引的數據表
  在聚集索引中,數據所在的數據頁是葉級,索引數據所在的索引頁是非葉級。
查詢原理和上述對非聚集索引的查詢相似,但由於記錄是按照聚集索引中索引鍵值進行排序,換句話說,聚集索引的索引鍵值也就是具體的數據頁。
  這就好比書庫中的書就是按照書名的拼音在排序,而且也只按照這一種排序方式建立相應的索引卡片,於是查詢起來要比上述只建立非聚集索引的方式要簡單得多。仍以上面的查詢爲例:
  假定在Lastname字段上建立了聚集索引,則執行Select * From Member Where Lastname=’Ota’時,查詢過程是:
  ①SQLS查詢INDID值爲1,這是在系統中只建立了聚集索引的標誌;
  ②立即從根出發,在非葉級節點中定位最接近Ota的值“Martin”,並查到其位於葉級頁面的第120頁;
  ③在位於葉級頁面第120頁的Martin下搜尋到Ota條目,而這一條目已是數據記錄本身;
  ④將該記錄返回客戶端。
  這一次的效率比第二種方法更高,以致於看起來更美,然而它最大的優點也恰好是它最大的缺點——由於同一張表中同時只能按照一種順序排列,所以在任何一種數據表中的聚集索引只能建立一個;並且建立聚集索引需要至少相當於源表120%的附加空間,以存放源表的副本和索引中間頁。
  難道魚和熊掌就不能兼顧了嗎?辦法是有的。

(四)SQLS如何訪問既有聚集索引、又有非聚集索引的數據表
  如果我們在建立非聚集索引之前先建立了聚集索引的話,那麼非聚集索引就可以使用聚集索引的關鍵字進行檢索。就像在圖書館中,前臺卡片櫃中可以有不同類別的圖書索引卡,然而每張卡片上都載明瞭那個特殊編號——並不是書籍存放的具體位置。這樣在最大程度上既照顧了數據檢索的快捷性,又使索引的日常維護變得更加可行,這是最爲科學的檢索方法。
  也就是說,在只建立了非聚集索引的情況下,每個葉級節點指明瞭記錄的行定位符(RID);而在既有聚集索引又有非聚集索引的情況下,每個葉級節點所指向的是該聚集索引的索引鍵值,即數據記錄本身。
假設聚集索引建立在Lastname上,而非聚集索引建立在Firstname上,當執行Select * From Member Where Firstname=’Mike’時,查詢過程是:
  ①SQLS查詢INDID值爲2;
  ②立即從根出發,在Firstname的非聚集索引的非葉級節點中定位最接近Mike的值“Jose”條目;
  ③從Jose條目下的葉級頁面中查到Mike邏輯位置——不是RID而是聚集索引的指針;
  ④根據這一指針所指示位置,直接進入位於Lastname的聚集索引中的葉級頁面中到達Mike數據記錄本身;
  ⑤將該記錄返回客戶端。
  這就完全和我們在“索引的基本概念”中講到的現實場景完全一樣了,當數據發生更新的時候,SQLS只負責對聚集索引的鍵值加以維護,而不必考慮非聚集索引。只要我們在ID類的字段上建立聚集索引,而在其它經常需要查詢的字段上建立非聚集索引,通過這種科學的、有針對性的在一張表上分別建立聚集索引和非聚集索引的方法,我們既享受了索引帶來的靈活與快捷,又相對避免了維護索引所導致的大量的額外資源消耗。

索引的優點和不足
  索引有一些先天不足
  1、系統要佔用大約爲表的1.2倍的硬盤和內存空間來保存索引;
  2、更新數據的時候,系統必須要有額外的時間來同時對索引進行更新,以維持數據和索引的一致性。
  當然建立索引的優點也是顯而易見的,在海量數據的情況下,如果合理的建立了索引,則會大大加強SQLS執行查詢、對結果進行排序、分組的操作效率。
  實踐表明,不恰當的索引不但於事無補,反而會降低系統性能。因爲大量的索引在進行插入、修改和刪除操作時比沒有索引要花費更多的系統時間。
  在如下字段建立索引應該是不恰當的:
  1、很少或從不引用的字段;
  2、邏輯型的字段,如男或女(是或否)等。
  綜上所述,提高查詢效率是以消耗一定的系統資源爲代價的,索引不能盲目的建立,必須要有統籌的規劃,一定要在“加快查詢速度”與“降低修改速度”之間做好平衡。有得必有失,此消則彼長,這是考驗一個DBA是否優秀的很重要的指標

建立索引時一定要在“加快查詢速度”與“降低修改速度”之間做好平衡,有得必有失,此消則彼長。那麼,SQLS維護索引時究竟怎樣消耗資源?應該從哪些方面對索引進行管理與優化?以下從六個方面來回答這些問題。

一.頁分裂

微軟MOC教導我們:當一個數據頁達到了8K容量,如果此時發生插入或更新數據的操作,將導致頁的分裂(又名頁拆分):

1.有聚集索引的情況下:聚集索引將被插入和更新的行指向特定的頁,該頁由聚集索引關鍵字決定;

2.只有堆的情況下:只要有空間就可以插入新的行,但是如果我們對行數據的更新需要更多的空間,以致大於當前頁的可用空間,行就被移到新的頁中,並且在原位置留下一個轉發指針,指向被移動的新行,如果具有轉發指針的行又被移動了,那麼原來的指針將重新指向新的位置;

3.如果堆中有非聚集索引,那麼儘管插入和更新操作在堆中不會發生頁分裂,但是在非聚集索引上仍然產生頁分裂。

無論有無索引,大約一半的數據將保留在老頁面,而另一半將放入新頁面,並且新頁面可能被分配到任何可用的頁。所以,頻繁頁分裂,後果很嚴重,將使物理表產生大量數據碎片,導致直接造成I/O效率的急劇下降,最後,不得不停止SQLS的運行並重建索引。

二.填充因子

然而在“混沌之初”,就可以在一定程度上避免不愉快出現,在創建索引時,可以爲這個索引指定一個填充因子,以便在索引的每個葉級頁面上保留一定百分比的空間,將來數據可以進行擴充和減少頁分裂。填充因子是從0到100的百分比數值,設爲100時表示將數據頁填滿,只有當不會對數據進行更改時(例如只讀表中)才用此設置。值越小則數據頁上的空閒空間越大,這樣可以減少在索引增長過程中進行頁分裂的需要,但這一操作需要佔用更多的硬盤空間。

填充因子只在創建索引時執行,索引創建以後,當表中進行數據的添加、刪除或更新時,是不會保持填充因子的,如果想在數據頁上保持額外的空間,則有悖於使用填充因子的本意,因爲隨着數據的輸入,SQLS必須在每個頁上進行頁拆分,以保持填充因子指定的空閒空間。因此,只有在表中的數據進行了較大的變動,纔可以填充數據頁的空閒空間。這時,可以從容的重建索引,重新指定填充因子,重新分佈數據。

反之,填充因子指定不當,就會降低數據庫的讀取性能,其降低量與填充因子設置值成反比。例如,當填充因子的值爲50時,數據庫的讀取性能會降低兩倍。所以,只有在表中根據現有數據創建新索引,並且可以預見將來會對這些數據進行哪些更改時,設置填充因子纔有意義。

三.兩道數學題

假定數據庫設計沒有問題,那麼是否像上篇分析的那樣,當你建立了衆多的索引,在查詢工作中SQLS就只能按照“最高指示”用索引處理每一個提交的查詢呢?答案是否定的。
實際上,SQLS幾乎完全是“自主”的決定是否使用索引或使用哪一個索引。


這是怎麼回事呢?

讓我們先來算一道題:如果某表的一條記錄在磁盤上佔用1000字節(1K)的話,我們對其中10字節的一個字段建立索引,那麼該記錄對應的索引大小隻有10字節(0.01K)。上篇說過,SQLS的最小空間分配單元是“頁(Page)”,一個頁面在磁盤上佔用8K空間,所以一頁只能存儲8條“記錄”,但可以存儲800條“索引”。現在我們要從一個有8000條記錄的表中檢索符合某個條件的記錄(有Where子句),如果沒有索引的話,我們需要遍歷8000條×1000字節/8K字節=1000個頁面才能夠找到結果。如果在檢索字段上有上述索引的話,那麼我們可以在8000條×10字節/8K字節=10個頁面中就檢索到滿足條件的索引塊,然後根據索引塊上的指針逐一找到結果數據塊,這樣I/O訪問量肯定要少得多。

然而有時用索引比不用索引還快。

同上,如果要無條件檢索全部記錄(不用Where子句),不用索引的話,需要訪問8000條×1000字節/8K字節=1000個頁面;而使用索引的話,首先檢索索引,訪問8000條×10字節/8K字節=10個頁面得到索引檢索結果,再根據索引檢索結果去對應數據頁面,由於是檢索全部數據,所以需要再訪問8000條×1000字節/8K字節=1000個頁面將全部數據讀取出來,一共訪問了1010個頁面,這顯然不如不用索引快。

SQLS內部有一套完整的數據索引優化技術,在上述情況下,SQLS會自動使用表掃描的方式檢索數據而不會使用任何索引。那麼SQLS是怎麼知道什麼時候用索引,什麼時候不用索引的呢?因爲SQLS除了維護數據信息外,還維護着數據統計信息。

四.統計信息

打開企業管理器,單擊“Database”節點,右擊Northwind數據庫→單擊“屬性”→選擇“Options”選項卡,觀察“Settings”下的各項複選項,你發現了什麼?

從Settings中我們可以看到,在數據庫中,SQLS將默認的自動創建和更新統計信息,這些統計信息包括數據密度和分佈信息,正是它們幫助SQLS確定最佳的查詢策略:建立查詢計劃和是否使用索引以及使用什麼樣的索引。

在創建索引時,SQLS會創建分佈數據頁來存放有關索引的兩種統計信息:分佈表和密度表。查詢優化器使用這些統計信息估算使用該索引進行查詢的成本(Cost),並在此基礎上判斷該索引對某個特定查詢是否有用。

隨着表中的數據發生變化,SQLS自動定期更新這些統計信息。採樣是在各個數據頁上隨機進行。從磁盤讀取一個數據頁後,該數據頁上的所有行都被用來更新統計信息。統計信息更新的頻率取決於字段或索引中的數據量以及數據更改量。比如,對於有一萬條記錄的表,當1000個索引鍵值發生改變時,該表的統計信息便可能需要更新,因爲1000 個值在該表中佔了10%,這是一個很大的比例。而對於有1千萬條記錄的表來說,1000個索引值發生更改的意義則可以忽略不計,因此統計信息就不會自動更新。

五.索引的人工維護

上面講到,某些不合適的索引將影響到SQLS的性能,隨着應用系統的運行,數據不斷地發生變化,當數據變化達到某一個程度時將會影響到索引的使用。這時需要用戶自己來維護索引。

隨着數據行的插入、刪除和數據頁的分裂,有些索引頁可能只包含幾頁數據,另外應用在執行大量I/O的時候,重建非聚聚集索引可以維護I/O的效率。重建索引實質上是重新組織B樹。需要重建索引的情況有:

1.數據和使用模式大幅度變化;

2.排序的順序發生改變;

3.要進行大量插入操作或已經完成;

4.使用I/O查詢的磁盤讀次數比預料的要多;

5.由於大量數據修改,使得數據頁和索引頁沒有充分使用而導致空間的使用超出估算;

6.dbcc檢查出索引有問題。

六.索引的使用原則

接近尾聲的時候,讓我們再從另一個角度認識索引的兩個重要屬性----惟一性索引和複合性索引。

惟一性索引保證在索引列中的全部數據是惟一的,不會包含冗餘數據。如果表中已經有一個主鍵約束或者惟一性約束,那麼當創建表或者修改表時,SQLS自動創建一個惟一性索引。但出於必須保證惟一性,那麼應該創建主鍵約束或者惟一性鍵約束,而不是創建一個惟一性索引。

複合索引就是一個索引創建在兩個列或者多個列上。在搜索時,當兩個或者多個列作爲一個關鍵值時,最好在這些列上創建複合索引。當創建複合索引時,應該考慮這些規則:最多可以把16個列合併成一個單獨的複合索引,構成複合索引的列的總長度不能超過900字節;在複合索引中,所有的列必須來自同一個表中,不能跨表建立複合列;在複合索引中,列的排列順序是非常重要的,原則上,應該首先定義最惟一的列,例如在(COL1,COL2)上的索引與在(COL2,COL1)上的索引是不相同的,因爲兩個索引的列的順序不同;爲了使查詢優化器使用複合索引,查詢語句中的WHERE子句必須參考複合索引中第一個列。

綜上所述,我們總結了如下索引使用原則:

1.邏輯主鍵使用惟一的成組索引,對系統鍵(作爲存儲過程)採用惟一的非成組索引,對任何外鍵列採用非成組索引。考慮數據庫的空間有多大,表如何進行訪問,還有這些訪問是否主要用作讀寫;

2.不要索引memo/note 字段,不要索引大型字段(有很多字符),這樣作會讓索引佔用太多的存儲空間;

3.不要索引常用的小型表;

4.一般不要爲小型數據表設置過多的索引,如果經常有插入和刪除操作就更不要設置索引,因爲SQLS對插入和刪除操作提供的索引維護可能比掃描表空間消耗的時間更多。

查詢是一個物理過程,表面上是SQLS在東跑西跑,其實真正大部分壓馬路的工作是由磁盤輸入輸出系統(I/O)完成,全表掃描需要從磁盤上讀表的每一個數據頁,如果有索引指向數據值,則I/O讀幾次磁盤就可以了。但是,在隨時發生的增、刪、改操作中,索引的存在會大大增加工作量,因此,合理的索引設計是建立在對各種查詢的分析和預測上的,只有正確地使索引與程序結合起來,才能產生最佳的優化方案。

SQLS是一個很複雜的系統,讓索引以及查詢背後的東西真相大白,可以幫助我們更爲深刻的瞭解我們的系統。一句話,索引就像鹽,少則無味多則鹹。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章