天天看點

SQL Server索引原了解析

此文是我之前的筆記整理而來,以索引為入口進行探讨相關資料庫知識(又做了修改以讓人更好消化)。SQL Server接觸不久的朋友可以隻看以下藍色字型字,簡單有用節省時間;如果是資料庫基礎不錯的朋友,可以全看,歡迎探讨。

全文章節:

1.聚集索引和非聚集索引

2.索引的結構

3.索引包含列和書簽查找

索引分為聚集索引和非聚集索引

1.1 聚集索引

表的資料是存儲在資料頁中(資料頁的PageType标記為1),SqlServer一頁是8k,存滿一頁就開辟下一頁存儲。如果表有聚集索引,那麼一筆一筆實體資料就是按聚集索引字段的大小升/降排序存儲在頁中。當對聚集索引字段更新或中間插入/删除資料時,都會導緻表資料移動(造成性能一定影響),因為它要保持升/降排序。

注意,主鍵隻是預設是聚集索引,它也可以設定為非聚集索引,也可以在非主鍵字段上設定為聚集索引,全表隻能有一個聚集索引。

一個優秀的聚集索引字段一般包含以下4個特性:

(A).自增長

總是在末尾增加記錄,減少分頁和索引碎片。

(B).不被更改

減少資料移動。

(C).唯一性

唯一性是任何索引最理想的特性,可以明确索引鍵值在排序中的位置。

更重要的是,索引鍵指唯一的話,它在每條記錄裡才可以正确指向源資料行RID。如果聚集索引鍵值不唯一,SqlServer就需要内部生成uniquifier 列組合當作聚集鍵保證“鍵值”唯一性;如果非聚集索引鍵值不唯一,就會增加RID列(聚集索引鍵或者堆表中的行指針)保證“鍵值”唯一性。

思考(可略過):索引“鍵值”在非葉子節點也有保證唯一性,原因應該是為了明确索引記錄在非葉子節點中的位置。比如有個非聚集索引字段Name2,表中有很多Name2='a'的記錄,導緻Name2='a'在非葉子節點上有多條索引記錄(節點),這時候再insert一筆Name2=‘a'的記錄時,就可以根據非葉子節點的RID和新增記錄的RID很快确定要insert到哪個索引記錄(節點)上,如果沒有非葉子節點的RID,那得周遊到所有Name2='a'的葉子節點才能确定位置。另外,當我們select * from Table1 where Name2<='a'時,傳回的資料是按非聚集索引Name2和RID排序的,很好了解傳回的資料就是按這邊索引存儲的順序排序的。這是這條sql查詢時有用到Name2索引的結果,如果資料庫查詢計劃因“臨界點”問題選擇直接表資料掃描,那傳回的資料預設就是按表資料的順序排序的。

為了“鍵值”唯一性,對于聚集索引,uniquifier 列隻在索引值重複時增加。對于非聚集索引,如果建立索引時沒定義唯一,RID會在所有記錄增加,就算索引值是唯一的;如果建立索引時定義唯一,RID隻在葉子層增加,用于查找源資料行,即書簽查找操作。

(D).字段長度小

聚集索引鍵長度越小,一頁索引頁就可以容納更多索引記錄,進而減少索引B樹結構的深度。例如,一個百萬記錄的表有一個int聚集索引,可能隻需要3層的B樹結構。如果把聚集索引定義在更寬的列(比如uniqueidentifier列需要16 位元組),那麼索引的深度會增加到4層。任何聚集索引查找需要4個I/O操作(确切的說是4個邏輯讀),原先隻要3個I/O操作。

同樣,非聚集索引裡會包含聚集索引鍵值,聚集索引鍵長度越小非聚集索引記錄也就越小,一頁索引頁就可以容納更多索引記錄。

1.2 非聚集索引

也是存儲在頁中(PageType标記為2的頁,叫索引頁)。比如表T建立了一個非聚集索引Index_A,那麼表T有100條資料的話,那麼索引Index_A也就有100條資料(準确的說是100條葉子節點資料,索引是B樹結構,如果樹的高度大于0,那麼就有根節點頁或中間節點頁資料,這時索引資料就超過100條),如果表T還有非聚集索引Index_B,那麼Index_B也是至少100條資料,是以索引建越多開銷越大。

更新索引字段、插入一條資料、删除一條資料都會造成索引的維護進而造成性能的一定影響。在不同情況下,性能影響是不同的。比如當你有一個聚集索引,插入的資料又都是在末尾,這樣幾乎是不會造成資料移動,影響較小;如果插入的資料在中間位置,一般會導緻資料移動,而且可能産生分頁和頁碎片,影響就會稍大一點(如果插入到的中間頁有足夠的剩餘空間容納插入的資料,而且位置是在頁末,也是不會造成資料移動)

都說SqlServer的索引是B樹結構(這邊假定你對B樹結構有一定了解),那它到底長什麼個模樣呢,可以用Sql語句來檢視它的邏輯呈現。

建立查詢執行文法: DBCC IND(Test,OrderBo,-1) --其中Test庫的OrderBo表有1萬筆資料,有聚集索引Id主鍵字段

(不妨自己動手建個表,有聚集索引字段,插入1萬表資料,然後執行這個文法看看,會收獲很多,百聞不如一見)

執行結果: 

如上圖,看到一個IndexLevel=2的索引頁2112(這邊它就是B樹的根節點,IndexLevel最大的就是根節點,往下就是子級、子子級...隻有一個根頁作為B樹結構的通路入口點),說明一定還有IndexLevel=1的索引頁和IndexLevel=0的葉子頁。由于這邊是聚集索引,是以當IndexLevel=0的葉子頁就是資料頁,存儲的是一筆一筆的實體資料。如上圖也可以看到,IndexLevel=0的行的PageType等于1,就是代表資料頁,上面1.1章節講到聚集索引時,也有提到PageType=1;而如果是非聚集索引,IndexLevel=0的葉子頁,PageType是等于 2,仍然是索引頁。

同樣,我們用Sql指令DBCC PAGE看一看

-- DBCC TRACEON(3604,-1) 
DBCC PAGE(Test,1,2112,3) 
 --根節點2112,可以查出它的兩個子節點2280和2448,然後對這兩個子節點再作DBCC PAGE查詢
DBCC PAGE(Test,1,2280,3) 
DBCC PAGE(Test,1,2448,3)       

如上圖,IndexLevel=2的2112頁有兩個IndexLevel=1的子節點2280和2448,子節點下又有子節點,每個節點負責不同的索引鍵值的區間(即上圖的“Id(key)”欄位,第一行值是Null,表示最小值或倒序時的最大值)。這樣的層級關系是不是就是一棵B樹結構,其中IndexLevel其實就是B樹結構中的高度Height。

SqlServer在索引中查找某一筆記錄時,是從根節點往下找到葉子節點,因為所有資料位址都有存在葉子節點,這其實是B+樹的特點之一(B樹特點是如果查找的值在非葉子節點就找到,則就能直接傳回,顯然SqlServer不是這麼做,要驗證這一點你可以set statistics io on把統計開起來,然後select看下邏輯讀的次數)。

既然一定會找到葉子節點,那麼索引包含列隻要在葉子節點記錄就可以了,即非葉子節點沒有記錄包含列,“索引包含列”見下文第3章節。

B+樹這個特點(所有資料位址都有存在葉子節點)也利于between value1 and value2 區間查詢,隻要找到value1和value2(在葉子節點),然後把中間串起來就是要的結果了。

SqlServer索引結構更像是B+樹,最終是B樹和B+樹的混合版,資料結構都是人定的,不一定就是純粹的B樹或者單純的B+樹。

談到索引,這邊再講一個SqlServer2005開始增加的“索引包含列”功能,很實用。

比如,在大報表查詢資料時,where條件用到索引字段Name2,但是要select的字段是Name1,這時候可以使用“索引包含列”把Name1包含在索引字段Name2中,大大提高查詢性能。

文法: Create [UNIQUE]  Nonclustered/Clustered Index IndexName On dbo.Table1(Name2) Include(Name1);

接下來分析為什麼索引包含列可以大大提高性能。仍然使用DBCC PAGE指令,檢視一個非聚集索引并有包含列的索引資料情況:

SQL Server索引原了解析

由上圖可知,包含列Name1也存儲在索引資料中。是以,當資料庫用索引字段Name2定位到要查找的某一行時,就可以直接把Name1的值傳回了,而不用再根據RID(上圖是【HEAP RID(Key)】列)定位到資料頁中去取值,即減少了書簽查找。當查詢隻傳回一條資料,隻有一次書簽查找時當然沒什麼,如果查詢傳回的資料很大,每一筆都要去資料頁找資料取出來,1000筆就是1000次書簽查找,可想而知性能消耗很大,這時候“索引包含列”價值就大大展現出來了。

關于一次書簽查找,表有聚集索引(比如Id)時就是類似執行了一次 select Name1 from Table1 where Id=1 ,利用聚集索引鍵Id查找(查找方式就是索引Id的B樹結構查找),而如果表沒有聚集索引,則是根據資料行指針(由“檔案号2byte:頁号4byte:槽号2byte”組成)查找。聚集索引鍵和行指針一般統稱為RID(Row ID)指針。從這裡我們可以想到,如果你的表沒有很好的聚集索引字段,建議自增長的Id字段做聚集索引主鍵(備援出Id字段也行),它符合自增長、不被更改、唯一性、長度小的特性,是聚集索引的很好選擇。

自增長Id絕大部分情況下是适用的,特殊的情況看具體需求而定吧。還有自增長Id要考慮一個缺陷,當對表大資料量的并發insert記錄時,可以想象每個線程都是要insert到末尾那個頁,就會發生競争和等待。解決這種情況你可以用uniqueidentifier類型字段(16位元組,我是不建議使用)或者哈希分區(就是一個表分成多個表,大資料進行中分庫分表是正常的)等。但是我建議先優化你的insert效率(insert性能本身是很快的),測試每秒并發insert數是否滿足生産環境,以保留簡單穩定高效的自增長Id作法。

自增長Id不一定就是用資料庫提供的自增長,你也可以自己寫算法生成一個并發情況下也能唯一的Id(這時候一般長度是bitint,8位元組整形),這種情況适合場景是分布式資料庫中主從複制時Id欄位是要求一定不能出錯的情況(主從複制的一般模式下,主庫的Id是按主庫增長,從庫Id也是按從庫自己的增長,如果遇到死鎖等原因導緻主從複制不同步時,那從庫的Id就和主庫的Id自增長就對不上号了)。如果自增長Id是備援出的主鍵,那主從庫Id對不上号也就無影響。

另外,上圖最後一列【Row Size】還告訴我們,索引列或索引包含列的size不要太長,否則一頁容不了幾筆記錄,這樣大大增加了索引頁數量,而且索引資料所占的空間也大大增加了。

分享、互相交流學習