有很多人寫了聚集索引和非聚集索引的文章,但我覺得在很多文章中表達的概念並不清楚,因此自己也寫一篇,能夠讓自己想清楚。我的最初目的是要寫到NO SQL,因此這系列的文章主要是關注在 1.數據庫索引結構、2.表聯接、3.遞歸查詢這幾個點上。
一、基本概念
1.數據的讀取
頁(page)是SQL SERVER可以讀寫的最小I/O單位。即使只需訪問一行,也要把整個頁加載到緩存之中,再從緩存中讀取數據。物理讀取是從磁盤上讀取,邏輯讀取是從緩存中讀取。物理讀取一頁的開銷要比邏輯讀取一頁的要大得多。
2.表的組織方式
表有兩種組織方式,B樹(Balance Tree)或者堆(Heap)。當在表上創建了一個聚集索引的時候,整個表數據就以B數的結構排列。否則就是按照堆的結構排列。無論表是怎么組織的,都可以在表上面創建多個非聚集索引。非聚集索引都是以B樹的結構排列。
2.1 堆(Heap)
之所以這個結構稱為堆,是因為它不以任何人為指定的邏輯順序進行排列。而是按照分區組隊數據進行組織。也就是說,是按照磁盤的物理順序。只要需要讀取的數據文件沒有文件系統碎片(注意和下面提到的索引的碎片區分),這個讀取過程在磁盤中就可以連續的進行,沒有多余的磁盤臂移動。而磁盤臂移動是I/O操作中開銷最大的操作。
堆使用一個bitmap結構來管理數據的分配。也就是它會告訴你兩個結果,這個區是分配了,還是沒有分配。每一個區中的物理順序如下圖。
對於新插入的數據,堆只管在最后一條數據的后面的一個空閑位置保存新插入的數據,不保持任何的邏輯順序。比如拿order表舉例,如果先插入orderid 4,5,6, 假設在位置1:176、 1:177、1:178這三個位置。這時再插入1,這時保存的數據就變味4,5,6,1, 1保存在 1:179的位置。
2.2聚集索引(Clustered Index)
聚集索引以B樹的方式保存數據。由於在另一篇文章中已經詳細的分析了B樹,這里就不再詳細說明。
繼續拿Order表舉例,Order表中的全部數據都保存在B樹中的葉層(leaf level)中,其他層只是起到一個索引的作用,並不包含任何數據。葉層是一個雙向鏈表結構,並按照聚集索引的主鍵的邏輯順序排列。因此邏輯順序是用指針來維護。
我們在圖中頁層所見到是邏輯順序,和上圖堆中所展示的物理順序要區分開來。
為什么我一再強調邏輯順序和物理順序?因為理解這很重要。
如圖所示,聚集索引中除了B樹之外,仍然維護了一個IAM結構,而這個結構就能保證在需要的時候,我們能按照物理順序而不是邏輯順序去在葉層中讀取數據。
那么什么時候才需要呢?先看什么是索引碎片。
2.2.1 索引碎片
數據庫中之所以會出現碎片,是因為B樹的頁拆分造成的。具體頁拆分請參考數據結構,這里要說的是由於拆分所產生的新頁不保證一定就會在被拆分的頁的后面,而是可能出於文件的任何位置。這就是“無序頁”。換句話說,也就是在列表中處於后面位置的元素,在物理文件中卻排在前面。如果你明白指針的定義的話,這句話並不難理解。因為葉層的雙向列表就是以指針來維護邏輯順序。
因此在按邏輯順序讀取的時候,由於無序頁的存在,可能造成磁臂頻繁的擺動。別忘記,磁盤擺動是I/O中開銷最大的操作。而I/O往往是一個系統的瓶頸所在。
如果按照物理順序來讀取,也就是unordered讀取,就會避免上面所產生的問題。再次強調,unordered是指不按邏輯順序讀取,所以叫unordered。
2.2.2 索引的層數
索引的層數,也就是B樹的高度,直接表明了一次查找操作在頁面讀取方面的開銷。一些執行計划如Nested loop聯接會多次調用查找操作。因此理解這個概念很重要。
樹的高度主要和以下幾個因素相關
- 表的總行數。
- 平均一行保存數據的大小。
- 頁的平均密度。因為不是每一頁都應該填充滿數據,這樣可以減少頁拆分的次數。
- 一頁所能容納的行數。
具體公式也很簡單,3級索引大概能容納4百萬行,4級索引大概能容納4億行數據。因此通常一張表的索引層數通常為3到4級。
2.3非聚集索引(NonClustered Index)
非聚集索引也是以B樹組織的。和聚集索引的區別就在於它的葉層並不包含所有的數據。在默認情況下它只包含了鍵列的數據,並包含了一個行定位符(row locator)。這個行定位符的具體內容取決於它建立在以堆形式的表還是以B樹組織的表,換句話說也就是這張表是否建立了聚集索引會影響到非聚集索引的行定位符。如果是建立了聚集索引,那么這個行定位符就是一個聚集鍵,我們通過這個聚集鍵再次查找聚集索引上的數據。
聚集索引上的非聚集索引
如果表是堆組織結構的,那么它就是一個直接指向數據所在行的物理指針。
下圖是建立在堆上的非聚集索引
2.3.1 如果非聚集索引包含了我們需要查找的所有數據
這種情況我們通常叫做索引覆蓋。
正因為非聚集索引有着和索引一樣的結構,並且由於非聚集索引所包含的列少,因此數據量就小,使得葉層的一頁能包含更多的行,因此進行一次I/O頁讀取的動作的時候,就能讀取進更多的行。因此查找效率是最高的。
舉個不恰當的例子,美女征婚,應征人員的個人信息表有 “姓名、 德、 智、 體 、美、 勞、 高、 富、 帥”這幾列,按姓名排序。美女只關注“高、 富、 帥”這三列的內容,為了更快的篩選,我們幫美女按照個人信息表的內容重新制作了一張表,這張表忽略了其他信息,只保留了高、富、帥和姓名,篩選效率當然就比原來關注更多內容時要高。
2.3.2 如果非聚集索引不包含我們需要查找的所有數據
通俗的說這時我們就需要從非聚集索引中所包含的線索去包含所有數據的表中去找。
按照我們之前的定義換句話來說,就是通過非聚集索引中的行定位符去聚集索引或者堆中去查找所需的數據。
二、通過實例來說明上述概念
我們創建一張Order表,表上建立了幾個索引
1.為orderdate列創建了聚集索引
2.為orderid列創建了非聚集索引
1.1.1 只為獲取整張表的數據,對數據順序不關心
SELECT [orderid]
,[custid]
,[empid]
,[shipperid]
,[orderdate]
,[filler]
FROM [Performance].[dbo].[Orders]
分析:由於我們需要獲取整張表的數據,因此不需要任何篩選也不需要任何排序。因此我們按照磁盤物理順序讀取出所有數據無疑是最快的選擇。 所以已排序為False. 再次說明這里的順序是聚集鍵的邏輯順序,和物理順序不同。
通過IAM在聚集索引的葉層掃描。在這種情況下無論表是以堆或者B樹的形式組織情況都類似。
(1000000 行受影響)
表'Orders'。掃描計數1,邏輯讀取25081 次,物理讀取5 次,預讀23545 次,lob 邏輯讀取0 次,lob 物理讀取0 次,lob 預讀0 次。
1.1.2 按聚集鍵順序獲取整張表的數據
對於Orders表,以orderdate為聚集鍵,因此如果我們使用順序查詢,就可以直接獲取所需要的數據。
這是我們就不再通過IAM來對葉層進行掃描,而是通過葉節點的指針來進行掃描。
1.1.3 如果不按照聚集鍵,而是按照其他列的順序來獲取整張表
我們並沒有把orderid設置成聚集索引的鍵,而是把它設成了非聚集索引的鍵。因此在返回整張表的內容時:
1.非聚集索引鍵列orderid對我們沒有意義,因為我們期望返回的是整張表的內容,而非聚集索引只包含鍵列的內容。
2.聚集鍵列orderdate的順序在這里對我們是沒有什么用的。
由上面的推論可以知道,這時我們所創建的索引對我們都沒有任何幫助。因此,與其按照邏輯順序返回,不如按照最快速的無序返回,再把返回的結果集排序。而計划證明了我們的猜想。
1.1.4 如果我們要查詢的內容,正好在非聚集索引里面就已經包含了
和上面查詢基本類似,區別在於我們在查詢結果中把非聚集索引中不包含的列全部刪除了,這時非聚集索引就形成了覆蓋。我們就可以利用非聚集索引進行查詢。
一些索引建議:
1.對於長字符串,比如VARCHAR(80)
這種類型的索引要比更為緊湊數據類型的索引大很多。同樣地,你也不太可能對長字符串
列進行全匹配查找。
本來想一次寫完,但是因為整體太長了,因此分開幾段來寫。
下一篇中我們將分析一些帶有篩選條件WHERE的查詢。
當然,如果覺得有幫助,請點一下推薦。