索引修改的大體規則:
- 對錶的任何修改操做(UDI),總會對錶上的非彙集索引執行等價的操做。某些更新操做除外。
- 對錶的任何修改操做,都會先修改堆或者彙集索引,而後再修改非彙集索引。
- 若是修改的數據行,正是過濾索引過濾掉的行(過濾索引的葉級頁不包含的行),則不會對過濾索引產生任何操做。
插入數據行
對於彙集和非彙集索引的插入,新行(不論是數據行仍是索引行)所包含的索引
鍵列值就決定了它將被插入的位置。插入操做的可能來源有:
- 直接的INSERT命令
- UPDATE致使的行移動(原來的地方已經容不下被更新後的行),內部使用先DELETE,再INSERT的UPDATE策略。
- UPDATE致使的索引鍵列變動。索引行是有序的,行的索引鍵值變動會致使行在索引中的位置變動,從而須要移動到新位置。一樣是先DELETE,再INSERT。
若是當前
索引的葉級(葉級在彙集索引中是數據頁,非彙集索引中是索引頁)沒有空間存放插入的新行,則索引會發生頁拆分(Page Split)。行在索引中的位置是有序的,因此當新行將要被插入的
某個特定頁沒有可用空間時,就須要分配新頁給索引。會先從已經分配的區中找是未使用的頁,若是沒有,則會分配一個新的統一區給索引,而後再使用新區中的頁。
頁拆分
獲得新頁以後,SQL Server會盡可能按照」對半分「原則,拆分原來頁上的一半數據行到新頁。第一次拆分是基於頁上偏移陣列(Offset Array)來計算的。每次索引頁拆分,還要向B+樹中的父級頁添加一行。有時須要屢次頁拆分才能將新行保存下來。頁拆分發生的越多,新頁也載多,須要向你級頁添加的行數載多,頗有可能同時致使父級頁也發生頁拆分。
索引樹的查找方式是從根節點向葉節點進行的,因此Insert致使的頁拆分也是從根節點向下發生的。這樣在Insert致使的拆分未完成前,索引樹須要使用閂鎖(Latch)對索引進行保護,以防止索引被其它的操做修改。當從磁盤上讀/寫頁時或者對數據頁進行操做時(如頁拆分),爲了保護頁中的數據的物理完整性,須要對頁加上閂鎖進行保護。當子節點的拆分完成而且再也不須要對父節點進行更新時,索引樹中父節點的閂鎖纔會被釋放。
在父節點的閂鎖釋放前,SQL Server會檢測父節點頁中是否還能容納兩行新數據。若是不能,則拆分它。這種狀況只會當查找索引,而且須要向索引頁中添加新行時纔會發生。這樣作的目的是當因爲子級頁發生頁拆分而須要向父級頁插入新行時,父級頁老是有空間存放這些新行。
頁拆分的類型由發生拆分的頁的類型決定
根頁拆分
當根頁發生拆分時,會分配兩個新頁給索引。原來根頁的數據會被插入到這兩個新頁中。原來的根頁仍然是索引的根頁,它上面只有兩行數據,分別指向兩個新頁。原來的根頁被保留,能夠避免修改系統目錄中指向根頁的指針值。根頁拆分會致使索引增長新的一級索引層次(深度增長一級)。這種拆分不多發生。
中間級頁拆分
中間索引頁發生拆分時,會增長一個新頁,而後根據索引鍵的中間點(Midpoint)將一半的行拆分到新頁,再往父級頁中插入一行指向新頁。這種拆分也不多發生。
葉級頁拆分
這是最多見,也是最須要關注的拆分類型。彙集索引數據頁和非彙集索引葉級頁的拆分機制是同樣的。雖然數據頁拆分只會發生在對彙集索引表執行Insert操做時,可是也多是Update操做致使的內部Insert操做。前文提過了,當Update不是原地更新時,會執行先Delete再Insert的操做。
葉級頁的拆分與中間級頁的方式相似。可是須要索引管理器決定兩頁中的誰來接收後續的新行,還要處理兩個頁面誰也存不下的大型行(Large Row)。數據頁拆分不會改變彙集索引鍵,因此相關的非彙集索引不會受到影響。
下面經過例子觀察一下葉級頁拆分。建立一個表,定義並插入大型行,使得一個頁只能存放5行數據,而後插入第6行數據後,觀察頁拆分的狀況。注意第6行的彙集鍵小於第5行。
USE test
go
CREATE TABLE bigrows
(
a int primary key,
b varchar(1600)
);
GO
/* Insert five rows into the table */
INSERT INTO bigrows
VALUES (5, REPLICATE('a', 1600));
INSERT INTO bigrows
VALUES (10, replicate('b', 1600));
INSERT INTO bigrows
VALUES (15, replicate('c', 1600));
INSERT INTO bigrows
VALUES (20, replicate('d', 1600));
INSERT INTO bigrows
VALUES (25, replicate('e', 1600));
GO
--get the data page id
select allocated_page_file_id as PageFID,allocated_page_page_id as PagePID,page_type_desc
from sys.dm_db_database_page_allocations(db_id('test'),object_id('bigrows'),null,null,'Detailed')
go
dbcc traceon(3604)
dbcc page(test,1,168,1)
go
--OFFSET TABLE:
--Row - Offset
--4 (0x4) - 6556 (0x199c)
--3 (0x3) - 4941 (0x134d)
--2 (0x2) - 3326 (0xcfe)
--1 (0x1) - 1711 (0x6af)
--0 (0x0) - 96 (0x60)
INSERT INTO bigrows
VALUES (21, replicate('f', 1600));
GO
select allocated_page_file_id as PageFID,allocated_page_page_id as PagePID,page_type_desc
from sys.dm_db_database_page_allocations(db_id('test'),object_id('bigrows'),null,null,'Detailed')
go
dbcc traceon(3604)
dbcc page(test,1,168,1)
dbcc page(test,1,172,1)
go
--Page 168
--OFFSET TABLE:
--Row - Offset
--2 (0x2) - 3326 (0xcfe)
--1 (0x1) - 1711 (0x6af)
--0 (0x0) - 96 (0x60)
--Page 172
--OFFSET TABLE:
--Row - Offset
--2 (0x2) - 1711 (0x6af)
--1 (0x1) - 3326 (0xcfe)
--0 (0x0) - 96 (0x60)
View Code
經過偏移陣列,能夠看出頁拆分後原來的頁和新頁各有三行。經過觀察DBCC PAGE的輸出的行內容,能夠看到:併發
- 原來的頁(PID:168)保存着彙集索引鍵值爲5,10和15的行,新頁(PID:172)保存着20,21和25的行。
- 在新頁上,21這行存儲在Slot1的位置上。根據Offset的值,21實際的物理位置倒是在25以後。
21的邏輯位置在25以前,物理位置在25以後。能夠看出:
行的彙集索引鍵順序是由Slot編號表示的,而不是行的物理位置。也就是
彙集索引表中,某行的Slot編號小於另外一行,則它的彙集索引鍵值也小於它。這實際上是一種優化的設計結果:當頁上發生數據修改時,只須要修改頁上偏移陣列的值來保證行的順序,而不須要物理性的移動數據行位置來保證順序。極大地減小數據修改的開銷。因此「索引中行的物理存儲順序老是與它的索引鍵值的順序是同樣的。」這種說法是不正確的。實際上,只要偏移陣列提供了正確的邏輯順序,行能夠存儲頁的任意位置。
頁拆分的代價是很大的。頁拆分過程當中對舊頁、新頁和父頁的修改操做,都須要完整寫入事務日誌。最小化業務高峯期發生頁拆分的辦法,一般有:
- 選擇一個更合理的彙集索引鍵。好比,讓新行插入到表的末尾,而不是像GUID那樣隨機插入。
- 對於更新變長列引發的頁拆分,能夠經過減小索引的填充因子(Fill Factor),在頁上保留多一些可用空間給變長列更新使用。
刪除數據行
刪除數據行時,須要同時考慮數據頁和索引頁的變化。彙集索引表中刪除行與非彙集索引葉級中刪除行是同樣的方式。
葉級中刪除行
當索引葉級的行被刪除時會被標記爲幻影行(Ghost Record)。行被刪除後,行頭的一個位(Bit)被修改,行就標記爲幻影,可是行仍是保留於頁上。頁頭的元數據m_ghostRecCnt表示當前頁的幻影行的數量。幻影行的用途有:
- 快速回滾。當行沒有被物理刪除時,回滾Delete操做只須要修改行頭的表示幻影行的位便可。
- 鍵值範圍鎖定(key-Range Locking)和其它鎖定的併發優化
- 用於行版本控制
幻影行何時被清除,由系統負載決定。SQL Server有一個叫作ghost-cleanup的後臺線程,用於清理那些再也不須要被活動事務和其它功能使用的幻影行。幻影行可能很快被ghost-cleanup線程清除掉。因此爲了觀察幻影行,能夠將Delete包裹在未被提交或者回滾的用戶事務中,或者使用末公開的(Undocumented)跟蹤標記661來禁用幻影行清理功能。可使用存儲過程sp_clean_db_free_space清除整個庫的幻影行,也能夠用sp_clean_db_fie_free_space清除庫中指定數據文件中的所有幻影行。
下面的例子,刪除彙集索引表中的一行,觀察幻影行。
USE test
GO
IF object_id('dbo.smallrows') IS NOT NULL
DROP TABLE dbo.smallrows;
GO
CREATE TABLE dbo.smallrows
(
a int IDENTITY PRIMARY KEY,
b char(10)
);
GO
INSERT INTO dbo.smallrows
VALUES ('row 1');
INSERT INTO dbo.smallrows
VALUES ('row 2');
INSERT INTO dbo.smallrows
VALUES ('row 3');
INSERT INTO dbo.smallrows
VALUES ('row 4');
INSERT INTO dbo.smallrows
VALUES ('row 5');
GO
--get data page id
select allocated_page_file_id as PageFID,allocated_page_page_id as PagePID,page_type_desc
from sys.dm_db_database_page_allocations(db_id('test'),object_id('smallrows'),null,null,'Detailed')
go
DELETE FROM dbo.smallrows
WHERE a = 3;
GO
dbcc traceon(3604)
dbcc page(test,1,174,1)
go
View Code
截取DBCC PAGE輸出中與幻影行相關的內容。
- 頁頭中(Page Header)中的 m_ghostRecCnt = 1,表示當前頁中幻影行數量是1。
- Slot2中的Record Type = GHOST_DATA_RECORD,表示當前行是幻影行。
- 偏移陣列中,Slot2的偏移量沒有變,其它Slot的偏移理也沒有變化。表示沒有行發生移動。
OFFSET TABLE:
Row - Offset
4 (0x4) - 180 (0xb4)
3 (0x3) - 159 (0x9f)
2 (0x2) - 138 (0x8a)
1 (0x1) - 117 (0x75)
0 (0x0) - 96 (0x60)
能夠經過sys.dm_db_index_physical_stats查看錶中的幻影行總數。
中間級中刪除行
中間級中刪除行與從堆表中刪除行相似。中間級頁中被刪除行,不會被標記爲幻影行,所佔用的頁空間也不會馬上釋放,當有新索引行須要頁面上的空間時才被釋放和重用。
頁回收
數據頁上全部行被刪除後,這個頁會被ghost-cleanup線程回收(Dealocated)。堆表是個例外。若是表
只有一個數據頁,此頁也不會被回收。當數據頁被刪除,指向這些數據頁的索引行會被刪除。中間級頁上索引行全被刪除,不會立刻回收,而是會在頁上保留一行,這一行稍後會被移動到鄰近的有空閒空間的頁上,而後回收原來的空頁。
更新行
SQL Server會自動選擇最優的數據更新策略。基於受影響行數,訪問數據的方式和是否須要修改索引鍵來選擇最優的策略。更新實現方式包括:直接將舊值原地修改成新值;刪除舊行再插入新行。
行移動
發生行移動的兩種狀況:
當行中的變長列被更新後,原來位置沒法再存儲它時
由於行的邏輯順序由索引鍵決定,因此當彙集索引或者非彙集索引的鍵列發生修改後,行的邏輯順序發生改變時。例如當在lastname列上創建彙集索引,lastname爲Abel的行存儲在接近表開始的位置,若是將Abel修改成Zek,則將會被移動到接近表結束的位置。
非彙集索引的葉級中包含指向表中每一行的行定位器。彙集索引表中,行定位器就是彙集索引鍵。因此僅當彙集索引鍵被修改時,非彙集索引纔會被修改。所以選擇彙集索引鍵列時,儘可能選擇非易失性的列(數據修改率極低,如Identity)。彙集索引表中,就算表的物理位置發生改變,也不會致使索引鍵改變,所心非彙集索引也不會被修改。
堆表中,行定位器是行的物理地址。行移動不會致使非彙集索引修改,由於它在行的原地址放置一個轉發指針指向新地址,非彙集索引仍然引用原來的行地址,經過轉發指針作重定向而已。可是堆表物理位置改變,會致使全部非彙集索引被修改。
原地更新
原地更新行是SQL Server的更新規則。每個原地更新操做都會在向事務日誌寫入一行,除非表上有更新觸發器或者被標記爲複製時。
若是原地更新須要修改索引鍵,則每一個操做會向事務日誌先寫入一條DELETE記錄,而後再寫入同樣INSERT記錄。
原地更新的場景:
- 更新堆,被更新的頁有足夠的空間存放更新後的行。
- 更新彙集索引表,且彙集索引列沒有被更新。
- 更新彙集索引表的彙集索引列,可是更新後的行不會發生移動。
非原地更新
非原地更新發生在更新彙集索引的索引鍵時。更新會變成先刪除再插入
兩個操做。更新索引鍵也有多是混合更新,即有些行是原地更新,其它行是非原地更新。更新彙集索引鍵時,SQL Server會生成一個包含刪除和插入操做涉及到的全部行的列表。這個列表較小就存在內存,較大就存在tempdb。而後根據鍵值和操做符(刪除或者插入)對列表排序。接下來分種狀況:
- 若是索引鍵值非惟一,則先刪除再插入。
- 若是索引鍵值惟一,則會將刪除和插入這兩個操做合併成一個更新操做。這樣更高效。
表級修改vs.索引級修改
在多索引的表上修改多行數據時,SQL Server提供兩種索引維護策略:表級修改和索引級修改。表級修改也叫作"一次一行"(row-at-a-time),索引級修改也叫作」一次一索引「(index-at-a-time)。
在表級修改中,每一行數據被修改時,全部的索引都須要被維護一次。若是更新流是無序的,則SQL Server每更新一行就須要訪問一次索引,這樣就增長不少的隨機訪問。若是更新流是有序的,由於只能按一種條件排序,因此最多有一個索引不須要隨機訪問。
在索引級修改中,SQL Server將全部將要被修改的行彙總起來,並針對索引進行排序(有幾個索引,就會有幾回排序)。而後將全部的修改彙總再應用到每一個索引上。能夠看出,這個過程當中每一個索引頁最多被訪問一次。
修改大表和索引上中的不多部分數據,SQL Server通常採用表級修改。若是修改的量很是大,則通常會選擇索引級修改。經過執行計劃能夠看出採用的哪一種修改方式:每一個受影響的索引前都有一個UPDATE操做符,則是索引級。若是隻有UPDATE操做符,則是表級。