說實話我從沒有在實際項目中使用過索引,僅知道索引是一個至關重要的技術點,所以我也看了很多文章知道了索引的區別、分類、優缺點以及如何使用索引。但關於索引它最本質的是什麼筆者一直沒明白,本文是筆者帶着這些問題研究msdn的一點小結以及一大堆疑惑。html
1.表結構sql
當開發者在數據庫中建立一個表時,此時默認爲這個表建立了一個分區,注意是一個分區。分區是一種數據組織單元,在這個分區中可存在2種結構,分別是堆結構或B樹結構(索引結構),也就是說一個分區裏要麼是堆結構要麼是B樹結構。爲了在某些方面提升性能以及便於管理, 咱們能夠本身建立分區,將數據以水平方式,也就是以行爲單位進行數據行的分區移動。雖然進行分區將數據行組劃分到不一樣的地方,可是進行查詢或其餘操做時仍將這個表當作是單個的邏輯實體。分區中包含由頁組成的堆結構或B樹結構,可能剛開始你會對堆和B樹結構有點懵,不過不要緊先了解整個表的結構而後再深刻細節。對於堆結構或B樹結構中的數據頁,爲了更高效的存儲它們,又分了3種存儲單元,本質其實就是頁的類型,它們也是一種數據組織單元。這三種頁類型分別是IN_ROW_DATA、LOB_DATA、ROW_OVERLLOW_DATA。頁類型的官方叫法是分配單元,它之因此存在,是由於數據頁中存在佔空間小的數據類型和佔空間比較大的數據類型,對於像varchar(max)這樣的數據列就要分配大型存儲單元,對於普通的數據列固然就要分配小型存儲單元,就比如int類型爲4字節,bit類型爲1字節同樣。到這裏關於一張表的構成也就介紹完了,以下圖所示。數據庫
2.頁類型數據結構
如今你或許很好奇這3種頁類型究竟是怎樣的類型,從上面爲何要分類的介紹中能夠看出它們分類的依據是佔用空間的大小。IN_ROW_DATA是最基本的頁類型,包含除LOB數據之外的全部數據行和索引行,頁的類型爲Data或Index。LOB_DATA頁類型,用來存儲大型數據對象,包括如下數據列text、ntext、varchar(max)、nvarchar(max)、image、varbinary(max)、xml、CLR UDT,頁的類型是Text或Image。ROW_OVERFLOW_DATA頁類型,當IN_ROW_DATA頁類型中的這一行數據超過8060時,將把這個頁上佔用空間最大的列移動到溢出頁中,原始頁上將維護一個指向溢出頁的指針。溢出頁類型和大對象頁類型管理頁的方式相同,都是使用IAM頁鏈來進行管理,注意又出現了一個新名詞IAM頁鏈,用一句話來描述IAM的話就是IAM用來跟蹤表的指定分配單元。如今咱們仍繼續深刻這3種頁類型,更深入的理解仍是須要sql實例。以下我建立了student表。工具
use testDb create table student( studentId int, studentName nvarchar(3600), studentAddress nvarchar(3600), studentDescription nvarchar(max) ) --查看建立的頁信息,最後一個1表示顯示全部分頁的信息,包括IAM分頁,數據分頁,全部存在的LOB分頁和行溢出頁,索引分頁 dbcc ind('testDb','student',1) --此時什麼都沒有 insert into student values(1,'李四','湖北','學生') dbcc ind('testDb','student',1)
在插入一條數據後再執行dbcc ind,它可獲得各個類型的頁面分佈和它們的所在的文件號和頁號,此時再也不什麼都沒有而是出現了下面的結果。性能
PageFID和PagePID分別指頁面Id和頁面編號,IAMFID指管理該頁面的IAM頁所在的文件ID,IAMPID指管理該頁面的IAM頁所在的文件編號。ObjectID指的是student這個表的對象Id,PartitionNumber表示表或索引所在的分區號,PartitionId表示分區id。接下來要介紹的這4個列要格外注意了。學習
iam_chain_type表示的正是頁類型,如今咱們能夠看到全IN_ROW_DATA類型的,此外還能夠是溢出類型和大對象數據類型。測試
PageType也至關重要,它表示分頁類型,1表示數據頁,2是索引頁面,3是Lob_mixed_page,4是Lob_tree_page,10是IAM頁面。大數據
IndexID表示索引ID,0 表明堆, 1 表明彙集索引, 2-250 表明非彙集索引,大於250的是text或image字段。spa
IndexLevel表示索引層級,0 表明葉級別分頁 ,大於0 表明非葉級別層次,NULL 表明IAM分頁。
從結果中咱們看到當插入這條數據後新增了一個數據頁和IAM頁,那IAM頁和數據頁究竟是什麼關係呢?帶着這個問題繼續新增數據。
--新增數據以至溢出,而且插入大數據對象 insert into student values(2,REPLICATE('小王',1700),REPLICATE('湖北',1700),REPLICATE('哈',80000)) dbcc ind('testDb','student',1)
如今又新增了5個頁,數據頁又增長了一個114,溢出頁爲93和其相應的IAM頁,大對象數據增長了109和IAM頁。產生溢出數據的緣由正是我新增的這一行除大對象數據外佔用空間超過了8060字節,大數據對象則是由於插入了80000個字符'哈',另外這些數據全都使用的是堆結構。如今再來理解IAM,前面說了IAM用來跟蹤每個分配單元,這個跟蹤的意思其實就是記錄這些頁的順序以便將這些頁連接起來。若是一個IAM頁面存儲的這個分配單元頁信息超過了它自己的大小,那麼會再建立新的IAM頁,這樣IAM頁和IAM頁一塊兒來記錄分配單元頁的信息,將這些IAM頁稱爲IAM頁鏈。
3.堆結構與B樹結構
有了上面的一個鋪墊,如今要來探索的即是堆和B樹了。堆結構是不含彙集索引的數據結構,每一個堆中的每一個分區至少有一個 IN_ROW_DATA 分配單元。若是堆包含大型對象 (LOB) 列,則該堆的每一個分區還將有一個 LOB_DATA 分配單元。若是堆包含超過 8,060 字節行大小限制的可變長度列,則該堆的每一個分區還將有一個 ROW_OVERFLOW_DATA 分配單元。堆中的數據頁和行沒有任何特定的順序且沒有連接在一塊兒,IAM頁內的信息記錄了數據頁之間惟一的邏輯連接。sys.system_internals_allocation_units系統視圖中的列 first_iam_page指向管理特定分區中堆的分配空間的一系列 IAM 頁的第一頁,具體的結構圖以下圖。
接下來就是本文的重點,彙集索引和非彙集索引的結構。關於彙集索引與非彙集索引區別的經典說法是索引就像字典的目錄,彙集索引比如按拼音查找,非彙集索引比如按偏旁來查找。索引是按B樹結構組織的,因此咱們如今要將這種具體的實例與B樹關聯起來。對於彙集索引,其B樹結構的頂點稱爲根節點,根節點與葉節點之間的任何索引層級都被認爲是中間級,其中最核心的是葉節點包含基礎表的數據頁,其他節點(根節點和中間級節點)包含的是索引頁,索引頁中存在索引行,每個索引行包含一個鍵值對和一個指針。鍵值對的內容是索引順序和該行所連接的下級節點中最小的值。好比2個鍵值對5-100和6-200,第一個表示這個索引行是第5個,那它的下級節點中最小的就是100,範圍是100~200,索引行中的指針則是指向了下一個索引頁或葉節點中的數據行。再看看我從msdn上截的彙集索引結構圖就可以很清楚地理解使用索引加快速度的本質緣由了。
對於非彙集索引,它與彙集索引最本質的兩個區別是"表中的數據行不按非彙集鍵的順序排序和存儲"和"非彙集索引的葉子節點不是數據頁而是索引頁"。也就是說非彙集索引並無真正的對數據在物理上進行排序,只是在數據表外創建了一個索引B樹結構,它根據非彙集索引列進行了排序並在每一個索引節點中有指向數據表中數據行的指針。非彙集索引中的索引行包含非彙集鍵值對和行定位符,其實也就是指針,可是有時候是在存在彙集索引的狀況下去建立非彙集索引,有時候是在堆結構中建立非彙集索引,所以這個行定位符便可能是指向彙集索引中的索引鍵,又多是指向堆中的數據行。這裏還有一個問題要注意,索引的目的是進行排序,而在彙集索引的基礎上建立非彙集索引時,有可能這個彙集索引是非惟一彙集索引,這樣將有可能形成排序錯誤,所以SQL Server 將在內部生成一個惟一鍵以使全部重複鍵惟一,此四字節的值對於用戶是不可見的,僅當須要使彙集鍵惟一以用於非彙集索引中時,才添加該值,SQL Server 經過使用存儲在非彙集索引的葉行內的彙集索引鍵搜索彙集索引來檢索數據行。
4.索引疑惑
之前閱讀索引的文章時知道索引這個工具適用於查詢量很大的列,不適用於須要不少更新的列,那爲何會這樣呢?當在一個列上建立彙集索引時,因爲彙集索引中直接讓索引頁指向了數據頁,所以彙集索引的查詢速度幾乎老是比非彙集索引的查詢速度快。對於更新操做,它下降索引查詢速度的最本質緣由就是索引碎片。索引碎片可分爲外部碎片和內部碎片,博客園上有不少關於這方面的文章。筆者學習的過程能夠用理解-不理解-理解-不理解這樣的感受來形容, 我最初的理解是這樣的。
當建立好彙集索引後,此時是一個B樹結構,假如每一個索引頁(8060字節)中有5條數據。那若是進行一個刪除操做形成不少頁中的索引條數再也不是5條,這樣索引頁中不就有不少剩餘部分了嗎,這就是內部碎片。我查閱的資料裏出現內部碎片大概是2種狀況,一是新增數據致使分頁從而出現剩餘空間,一是刪除原有數據頁中的部分數據致使出現剩餘空間。
若是進行一個新增操做,假若有3頁數據,如今有一個新增一條數據的操做。按照順序應該放在第一頁,但是如今第一頁空間沒法容納。這會致使建立一個新的頁,可是,這個頁並不像邏輯順序那樣在第一頁和第二頁之間,最終結果是在物理上與第一頁不連續,這就是外部碎片。出現外部碎片最關鍵的是要在物理上形成分離而不是連續。
原本我覺得我理解了索引碎片,但是我又發現有些隨筆裏關於索引碎片舉例中,內部碎片是新增數據致使索引分頁,外部碎片則是新增數據致使分頁。以下面這2張圖,左邊是內部碎片示例圖,右邊是外部碎片示例圖。這又讓我感到不理解了,從表面上看內部碎片和外部碎片都是新增數據致使分頁,難道內部碎片與外部碎片之間僅僅只是描述的角度不一樣,實際上本質是同樣的。後來仔細閱讀前輩的文章才知道仍是有區別的,不只分頁且在物理上致使新增頁與原來的頁不連續纔是外部碎片。很快個人理解又變成不理解了,或許是我有點鑽牛角尖,可是這個地方好多隨筆都很模糊。我不明白到底這個新增的頁,是如何分配物理空間的。何時會與原來的頁連續形成內部碎片,何時會與原來的頁不連續形成外部碎片?
再來思考出現碎片的這種狀況下,爲何會變慢。在內部碎片中,索引頁裏一個頁有不少剩餘空間,這是讓我認爲變慢的現象。我在網上查閱資料來解釋爲何變慢發現大概都是這樣的說法:明明只有10頁的數據結果卻要在20頁中查詢,這增長了I/O並且還致使頁命中率降低,存儲上也必須消耗更多的空間。假設如今是這樣一種狀況,我有10頁數據,進行刪除操做後每頁中的數據減小了,這樣形成了內部碎片。按照通常的想法如今速度變慢了,的確這些數據可能已經不須要10頁來存儲,可能5頁就足夠。但是我原來是有10頁數據,如今依舊是10頁數據,爲何相對於之前完整的10頁數據來講如今變慢了?我愈來愈想知道到底這種內部的操做的是什麼樣子的,若是有大牛閱讀到這裏還請您指點一下。我也不知道我爲何會出現這種奇怪的疑問,如今我能夠作的就是在進行刪除操做致使頁數據減小,從而產生內部碎片的狀況下查詢速度是否真的變慢了呢?sql以下所示,在測試過程當中又出現了3個疑惑。
use testDb create table student( studentId int, studentName char(500), studentAddress char(500) ) declare @i int set @i=0 while(@i<3200) begin insert into student values(@i,'wangwu','hubei') set @i=@i+1 end select index_type_desc, --索引類型說明:HEAP、CLUSTERED INDEX、NONCLUSTERED INDEX、XML INDEX、PRIMARY XML INDEX、SPATIAL INDEX index_depth, --索引級別數:堆 或 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元 page_count, --索引或數據頁的總數,對於索引表示IN_ROW_DATA中B樹的當前級別中索引頁總數,對於堆表示IN_ROW_DATA分配單元中的數據頁總數 record_count, --總記錄數 fragment_count, --IN_ROW_DATA 分配單元的葉級別中的碎片數。 avg_record_size_in_bytes, --平均記錄大小(字節) avg_fragment_size_in_pages, --IN_ROW_DATA 分配單元的葉級別中的一個碎片的平均頁數。 avg_fragmentation_in_percent, --索引的邏輯碎片,或 IN_ROW_DATA 分配單元中堆的區碎片。 avg_page_space_used_in_percent --全部頁中使用的可用數據存儲空間的平均百分比。 from sys.dm_db_index_physical_stats(DB_ID('testDb'),OBJECT_ID('student'),NULL,NULL,'sampled')
create clustered index index_studentId on student(studentId) select index_type_desc, --索引類型說明:HEAP、CLUSTERED INDEX、NONCLUSTERED INDEX、XML INDEX、 PRIMARY XML INDEX、SPATIAL INDEX index_depth, --索引級別數:1是堆 或 LOB_DATA 或 ROW_OVERFLOW_DATA 分配單元 page_count, --索引或數據頁的總數,對於索引表示IN_ROW_DATA中B樹的當前級別中索引頁總數,對於堆表示IN_ROW_DATA分配單元中的數據頁總數 record_count, --總記錄數 fragment_count, --IN_ROW_DATA 分配單元的葉級別中的碎片數。 avg_record_size_in_bytes, --平均記錄大小(字節) avg_fragment_size_in_pages, --IN_ROW_DATA 分配單元的葉級別中的一個碎片的平均頁數。 avg_fragmentation_in_percent, --索引的邏輯碎片,或 IN_ROW_DATA 分配單元中堆的區碎片。 avg_page_space_used_in_percent --全部頁中使用的可用數據存儲空間的平均百分比。 from sys.dm_db_index_physical_stats(DB_ID('testDb'),OBJECT_ID('student'),NULL,NULL,'sampled')
(1)按理說一行數據爲1004個字節,一頁能夠存放8條數據,3200條應該是400行,但結果倒是464頁,這是第一個讓我很疑惑的地方。
(2)在建立索引後頁數page_count爲458,頁數相比原來的464反而還變小了,這是第二個讓我很疑惑的地方。
declare @i int set @i=6 while(@i<3200) begin delete from student where studentId=@i set @i=@i+8 end go
(3)結果中avg_fragmentation_in_persent爲0,這說明根本就沒有產生碎片,我不知道是我誤解這篇隨筆的意思仍是這篇隨筆關於內部碎片的地方寫錯了。
本人數據庫菜鳥一枚,如您有想法歡迎交流。