SQL-Server索引漫談


一. SQL-Server數據存儲基本單位

    [文章排版比較亂,所以還請讀者體諒一下,后續如果有時間會重新整理一下]這篇文章討論的主題是索引,但在正式進入索引的內容前,想簡單介紹一下關於SQL-Server數據存儲的一些簡單認識,這將幫助我們更好地理解索引的結構。在SQL-Server中,數據存儲的基本單位是頁,一頁的大小是8KB(共8192字節)。

  

  1. 頁首

    頁首固定占用每個數據頁的96字節,保存了頁面系統信息。下表列出了部分具體信息:

  pageID: 數據庫中該頁的文件編號和頁編號

  nextPage:如果該頁位於一個頁鏈中,則該字段表示下一頁的文件編號和頁編號

  pervPage:如果該頁位於一個頁鏈中,則該字段表示上一頁的文件編號和頁編號

  Metadata:ObjectId:該頁所在對象的ID

  indexId:該頁的索引ID(0為數據頁)

  2. 行內數據的數據行

    數據行是真實數據的存儲區域。每一行的大小是不固定的,以Slot為單位,0開始編號,Slot0,Slot1依次類推。

  3. 行偏移數組

    用於記錄該數據頁中每個Slot的相對位置,便於快速定位Slot的位置。例如:行偏移數組槽0可以指向偏移0X60(96字節)的Slot0。行偏移數組的每個記錄占兩個字節。行偏移數組表示的是數據頁上面的邏輯順序。例如Slot0可以指向0X80,而Slot1可以指向0X60,實際存儲的物理位置不一定按照(聚集索引)排序的。

二. 什么是索引

  數據庫索引是對表的一列或多列的值進行排序的一種結構,索引與表數據的關系類似於目錄與書籍內容的關系。在SQL-Server中存在兩種比較重要的索引,分別為聚集索引與非聚集索引,它們是以B+樹組織保存的。

  建立索引也要付出額外代價的: 索引需要占據額外的內存空間 (當創建一張新表table_name后,並插入數據,使用sp_spaceused(table_name)就可以查看當前索引使用了多少內存,但必須注意sp_spaceused是每個數據庫都有的一個系統存儲過程,在使用的時候必須指明要查詢的數據庫) ; 插入和修改數據需要涉及索引的改動,將花費更多的時間。

三. 為什么要使用索引

   眾所周知,數據查詢是數據庫一項使用非常頻繁的操作,查詢的快慢已成為了衡量系統好壞的一個重要標准,而合理地使用索引可以提高數據檢索效率,改善數據庫性能,加快數據訪問速度。

四. 堆結構

  1. 什么是堆結構

     堆的本義是雜亂無章,無序的意思。對於未建立聚集索引的表,數據是沒有遵循特定的某種規則排序的,表中的所有數據頁就形成了堆結構。

  2. 堆結構實例

--接下來的數據庫語句操作都是一些簡單的命令
--
數據庫為Test USE Test GO --創建一張table CREATE TABLE t1 ( t1_id INT NOT NULL ) GO --查看t1的使用情況 sp_spaceused t1

 

USE Test
GO

--sys.indexes是系統視圖,當創建一張新表的時候,就會在sys.indexes中增加一條記錄
--可以在http://msdn.microsoft.com/zhcn/library/ms173760(v=sql.100).aspx 中找到相關信息
SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('t1')
GO

--sys.sysindexes同樣是系統視圖
--可以在http://technet.microsoft.com/zh-cn/library/ms190283(v=sql.100).aspx 中找到相關信息
SELECT * FROM sys.sysindexes WHERE id = OBJECT_ID('t1')
GO

  sys.indexes中的記錄type_desc明確指出了索引的類型是堆(HEAP)。

  sys.sysindexes中的記錄與數據頁組織有莫大的關系,后面會繼續討論。indid=0表示這是堆。堆只需考慮FirstIAM的變化情況,可以看到這里FirstIAM的指針地址為0。

USE Test

--接下來,插入一條記錄
INSERT INTO t1(t1_id) VALUES (1)
GO

--重新查看內存使用情況
sp_spaceused t1
GO

  這里可以明顯地知道,數據插入的基本單位是數據頁,即使插入的是一行數據,也占據一張數據頁。后續插入數據的時候,如果當前的數據頁還可以繼續容納的話,就插入到當前數據頁中,不然就插入到一張新數據頁中。但這里還是有一個疑問,為什么會有index_size呢?這里不是只插入了一條記錄而已嗎?這就要從堆的內部結構說起了。

USE Test
GO

--我們來繼續查看sys.sysindexes,看下這條記錄發生了什么變化
SELECT * FROM sys.sysindexes WHERE id = OBJECT_ID('t1')
GO

  這里的FirstIAM的指針地址發生了變化。

--查看頁的基本信息
--前提條件:表中必須插入了數據
--所需參數:(數據庫名,表名,-1表示顯示全部IAM頁,數據頁, 索引頁) DBCC IND (Test2,t1,-1)

  PageType=1表示這是數據頁,PageType=10表示這是IAM頁。

  什么是IAM頁? IAM=Index Allocation Map,索引分配映射。IAM頁的結構與數據頁的結構基本相同。堆結構中,IAM是SQL-Server查找屬於該表單所有范圍的唯一方法。

  那么FirstIAM的地址指針0X4F0000000100又是如何跟IAM頁關聯的呢? 首先必須拆分這個地址指針,從右往左讀,兩數字為一組,得到0X 00 01 00 00 00 4F。前兩組表示文件編號(PageFID),即0X0001(1),后四組表示頁編號(PagePID),即0X0000004F(79)。這樣FirstIAM就與IAM頁關聯起來了。

USE Test
GO

DECLARE @i INT;
SET @i = 10000;

--插入10000條記錄
WHILE @i>0
BEGIN
    INSERT INTO t1 (t1_id) VALUES (@i)
    SET @i = @i - 1
END
GO

--再次查看頁的基本信息
DBCC IND(Test,t1,-1)
GO

 

  插入了10000條記錄后,可以看到數據頁增加了17頁,而IAM頁還是只有1頁,這是因為IAM頁最多可定位4GB的數據量。在堆結構中,PrevPagePID和NextPagePID都是0,數據頁之間不存在鏈表關系,數據頁的關系僅靠IAMPID維持着。

  堆結構的查詢示意圖如下:

 

  3. 堆結構全表掃描

  SQL-Server在接到查詢請求后,便會首先分析sys.sysindexes的索引標識符indid,堆結構的indid為0,這時就會查找另一個字段FirstIAM,找到IAM頁鏈,便開始所有數據頁的依次遍歷查找過程。堆結構的表就像一個存放着亂七八糟的書而且沒有排序好的書庫,當要查詢某一類型的書或某個范圍內的書的時候,就只能從第一個書架開始找起,每一本書都要看,如果匹配就拿出來,直到最后一個書架都找完了。當書庫的書成千上萬的時候,這樣的查找方式確實效率低下。

五. 索引的分類

  1.聚集索引(CLUSTERED INDEX)

  1) 什么是聚集索引

  聚集索引定義了數據在表中存儲的物理順序。如果不止在一個列上定義了聚集索引,數據將按照在這些列上所指定的順序而存儲,先按第一列指定的順序,再按第二列指定的順序,以此類推。由於數據只能有一種實際存儲方式,所以對於一張表來說聚集索引只能有一個。以經典的新華字典例子說起,新華字典中的字是按照拼音字母的先后順序排序存儲的,當我們要查‘安’字的時候,我們首先會在字典的拼音索引中找到‘an’讀音所在的頁碼,並開始按序查找,如果找不到就表示新華字典中沒有‘安’字。這就是聚集索引的工作原理。

  2) 聚集索引結構

 

  接下來將對這張結構圖進行全面的分析。

  SQL-Server在接到查詢請求后,便會首先分析sys.sysindexes的索引標識符indid,可以看到聚集索引結構的indid=1,這時就會查找root的字段,而root指向的是聚集索引根級索引頁。這里跟堆結構是有區別的,堆結構使用的字段是FirstIAM。 

  那什么是索引頁呢?

  索引頁與文章開頭討論的數據頁的結構幾乎完全相同,也是8KB固定大小,使用96字節的頁首,結尾處使用偏移數組。只是索引頁存儲的是索引記錄,而數據頁存儲的是數據記錄。

USE Test
GO

--創建一張表
CREATE TABLE t2
(
    t2_id INT IDENTITY(1,1) NOT NULL,
    t2_c1 VARCHAR(10) NOT NULL 
)


--創建一個在列t2_id上的聚集索引
CREATE CLUSTERED INDEX ix_t2_id 
ON t2 (t2_id ASC)



--插入4000行數據
DECLARE @i INT
SET @i = 4000

WHILE @i>0
BEGIN
    INSERT INTO t2 (t2_c1) VALUES ('a')
    SET @i = @i - 1
END 

--查看sys.sysyindexes的使用情況
SELECT * FROM sys.sysindexes WHERE id = OBJECT_ID('t2')

--使用DBCC IND查看頁的使用情況
DBCC IND(Test,t2,-1)
聚集索引

  indid=1表示這是一個聚集索引,與堆結構的FirstIAM轉換規則一樣,0X730000000100可以轉化為(1:115),則指向的索引頁的文件編號是1,頁編號是115。

  PagePID=115的行的PageType=2,表示這是一個索引頁。IndexLevel表示索引的等級,數值越大表示離根節點越近。這里因為只插入了4000條數據,數據量較少,所以只需一個索引頁。同時觀察一下PageType=1的數據頁,NextPagePID和PrePagePID將數據頁串聯成一個數據鏈表,和堆結構的數據頁是有明顯的區別的。這是B+樹的一個特點所在,在此對B+樹的結構就不多加討論了。

  可以看到建立了聚集索引的表也有一個IAM頁,個人推測這個IAM頁的作用是當刪除聚集索引后,表變成了堆結構,這時就按堆結構的工作方式查詢數據。

USE Test
GO

--這里可以使用PAGE命令查看頁的具體情況
--參數(數據庫名,FileID,PageID,3表示查看索引頁信息)
DBCC PAGE(Test,1,115,3)

  可以看到,索引頁共有10行,分別指向了10個頁面,t2_id是索引鍵,ChlidPageId是指向數據頁的頁編號。

  t2表的B+樹結構如圖:

  這里因為插入的每一行數據都占據相同的大小,所以數據頁呈規律遞增,但實際應用情況下,插入的數據基本是不同的,就不會像這里一樣出現規律性的遞增了。當要查詢t2_id=800的記錄的時候,就在索引頁中查找,由於405<800<809,所以就在索引鍵405指向的數據頁中順序查找,如果找得到就返回t2_id=800的記錄,這樣就避免了堆結構的全表掃描,提高了查詢的效率。

  2.非聚集索引(NONCLUSTERED INDEX)

  1) 什么是非聚集索引

  非聚集索引與表中的邏輯組織順序無關,是以分離的結構存在的,所以在一個表中可以同時存在多個非聚集索引。同樣以新華字典為例,當我們要查詢一個不知道讀音的字,比如查詢‘張’字,這時可以通過查詢偏旁部首找到它,並查到‘張’字所在的頁面。具有相同偏旁部首的字是按照筆畫的多少排序的,運用了鍵值對的方法,每個字的右邊就跟上具體的頁碼。非聚集索引也是使用索引鍵和指向表數據的指針。

  2) 非聚集索引結構

     

  與聚集索引結構對比,非聚集索引結構同樣是以B+樹存儲的。不同的是,indid字段>1,表示這是一個非聚集索引,非聚集索引中只有索引頁,葉節點也是索引頁,而聚集索引的葉節點是數據頁;非聚集索引的每個索引行都指向具體的數據行。

  3) 堆上的非聚集索引

USE Test
GO

--創建堆表
CREATE TABLE t3
(
    t3_id INT IDENTITY(1,1) NOT NULL,
    t3_c1 VARCHAR(10) NOT NULL,
)

--建立非聚集索引
CREATE NONCLUSTERED INDEX ix_t3_c1_nonclus
ON t3 (t3_c1)

--插入1000行數據
DECLARE @i INT
DECLARE @num INT
DECLARE @str VARCHAR(10)

SET @i = 1000

WHILE @i > 0
BEGIN
    --設置插入字符個數
    SET @num = CAST(RAND()*1000 AS INT)% 10 + 1
    --字符串置空
    SET @str = ''
    --插入隨機字符
    WHILE @num > 0
    BEGIN
        SET @str += CHAR(CAST(RAND()*1000 AS INT)% 26 + 97) 
        SET @num -= 1
    END
    --插入t3表中
    INSERT INTO t3 (t3_c1) VALUES (@str)
    SET @i -= 1
END

--查看頁的使用情況
DBCC IND(Test,t3,-1)
推上的聚集索引

  PageType=2表示這是非聚集索引。IndexLevel=1表示這是根節點頁。

--查看根節點頁的信息
DBCC PAGE(Test,1,356,3)

  這里可以看到ChildPageId指向的是葉節點索引頁,t3_c1是非聚集索引鍵,HEAP RID表示的是指針,指向真實的堆數據行。

--查看葉節點頁的信息
DBCC PAGE(Test,1,343,3)

  這里可以看到非聚集索引頁是按照t3_c1的先后順序排序的。HEAP RID的指針是有規律可循的,例如0X6301000001003700,前12位表示的是數據頁編號和文件編號,后4位表示的是行編號,按照從右往左的順序,以兩位為一組,可以得到(1:355:55),表示文件編號為1,數據頁編號為355,數據行編號為55。

  堆上的聚集索引示意圖:

  4)聚集表中的非聚集索引 

USE Test
GO

--創建表t4 表結構與t3類似
CREATE TABLE t4
(
    t4_id INT IDENTITY(1,1) NOT NULL,
    t4_c1 VARCHAR(10) NOT NULL
)

--建立聚集索引
CREATE CLUSTERED INDEX ix_t4_clus
ON t4 (t4_id)
GO

--建立非聚集索引
CREATE NONCLUSTERED INDEX ix_t4_c1_nonclus
ON t4 (t4_c1)
GO

USE Test
GO

--插入1000行數據
DECLARE @i INT
DECLARE @num INT
DECLARE @str VARCHAR(10)

SET @i = 1000

WHILE @i > 0
BEGIN
    --設置插入字符個數
    SET @num = CAST(RAND()*1000 AS INT)% 10 + 1
    --字符串置空
    SET @str = ''
    --插入隨機字符
    WHILE @num > 0
    BEGIN
        SET @str += CHAR(CAST(RAND()*1000 AS INT)% 26 + 97) 
        SET @num -= 1
    END
    --插入t3表中
    INSERT INTO t4 (t4_c1) VALUES (@str)
    SET @i -= 1
END

--查看頁的使用情況
DBCC IND(Test,t4,-1)
聚集表上的非聚集索引

  我們可以看到IndexID=1,PageType=2,IndexLevel=1的索引頁就是根聚集索引頁,而IndexID=2,PageType=2,IndexLevel的索引頁就是根非聚集索引頁。

--查看非聚集索引根節點頁的信息
DBCC PAGE(Test,1,367,3)

  非聚集索引有聚集索引的鍵值,這就將非聚集索引與聚集索引聯系起來了。這里的UNIQUIFIER(key),是做為一個唯一標識,因為聚集索引可以設定為不唯一,這個標識就能區分出相同的聚集索引鍵。

--查看非聚集索引葉節點頁的信息
DBCC PAGE(Test,1,363,3)

  當需要定位到指定的t4_id的時候,數據庫就是用聚集索引查找的方法搜索聚集索引B+樹,找到最終的數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM