堆的修改需要使用到PFS頁(PageFreeSpace)。PFS記錄着數據頁的空間使用情況。PFS頁上使用1個字節(Byte)表示一個頁的使用情況。一個PFS頁可以表示8088個數據頁,於是每8088個數據頁就會有一個PFS頁。一個數據文件的第二個頁就是PFS頁。PFS頁上1個字節的結構:
Bit 1:是否被分配並使用。比如,分配給對象的統一區,並不是區內所有的頁都被使用。此位就用標示已分配區中頁是否被真正使用。
Bit2:表示頁是否來自混合區
Bit3:表示頁是否是一個IAM頁(Index Allocation Map)
Bit4:表示頁中是否有幻影行(Ghost Record)。後臺的幻影行清理進程就需要用到這個位了。只有刪除索引中的數據時纔會產生幻影行。
Bit5-7:表示頁的空間被使用的情況。取值如上圖所示。
插入數據行
堆表插入新的數據行時,新行會被分配到任何有可用空間的地方。也就是說插入位置可能是表的任何位置。如果沒有頁有可用空間,則會從已經分配給表的統一分區中尋找未被使用的頁來存儲數據。如果所有區的所有頁都沒有空閒空間,則會分配新的統一分區給表來存儲數據。
刪除數據行
直接刪除之,刪除方式跟刪除索引中的非葉級頁一樣但跟。
從堆表刪除數據行後,被刪除行所在的數據頁並不會馬上重組數據頁以釋放空間,只會標示這些空間可用。當插入新行需要連續的可用空間時,纔會被回收利用。下面的示例從頁中間刪除一行:
CREATE TABLE smallrows ( a int identity, b char(10) ); GO INSERT INTO smallrows VALUES ('row 1'); INSERT INTO smallrows VALUES ('row 2'); INSERT INTO smallrows VALUES ('row 3'); INSERT INTO smallrows VALUES ('row 4'); INSERT INTO smallrows VALUES ('row 5'); go --get the data page id for dbcc page SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc FROM sys.dm_db_database_page_allocations (db_id('test'), object_id('smallrows'), NULL, NULL, 'DETAILED'); go dbcc traceon(3604) dbcc page(test,1,146,1) go --delete the row a=3 DELETE FROM smallrows WHERE a = 3; GO dbcc traceon(3604) dbcc page(test,1,146,1) go
觀察兩次dbcc page輸出的OFFSET TABLE:
Row - Offset
4 (0x4) - 180 (0xb4)
3 (0x3) - 159 (0x9f)
2 (0x2) - 138 (0x8a)
1 (0x1) - 117 (0x75)
0 (0x0) - 96 (0x60)
Row - Offset
4 (0x4) - 180 (0xb4)
3 (0x3) - 159 (0x9f)
2 (0x2) - 0 (0x0)
1 (0x1) - 117 (0x75)
0 (0x0) - 96 (0x60)
可以看出:
刪除前後其它行的偏移量沒有變化,也就是說沒有行被移動。
刪除後,被刪除行(slot2)偏移量變成了0,表示該slot未被使用。
其實使用DBCC PAGE(TEST,1,146,2)仍然可以看到row3這一行。
清空堆表數據頁上的數據,它也不會自動的釋放這些頁。可以通過sys.dm_db_partition_statst和sys.dm_db_
database_page_allocations觀察到頁的使用和分配信息是不會有變化的。要想清空頁的數據並回收空間:
delete時使用表鎖:數據頁會被釋放,但IAM頁保留
truncate table:這個是針對清空表,會釋放所有頁,包括IAM頁
創建並刪除一個聚集索引
使用alter table ...rebuild
更新數據行
SQL Server會自動選擇最優的數據更新策略。基於受影響行數,訪問數據的方式和是否需要修改索引鍵來選擇最優的策略。更新實現方式包括:直接將舊值原地修改爲新值和插入新值後刪除舊值。
堆表中的數據行移動
堆表中數據行的變長列的數據更新爲更大尺寸的數據後,原來的數據頁不能再存儲它,就會發生數據行移動。數據行被移動到新頁時,原來的位置上會放置一個轉發指針(Forwarding Pointer)。這個指針指向行的現在的地址。這樣的好處,就是當發生數據行移動時不需要移動頁上所有的數據,只需要移動特定行並生成轉發指針即可。
下面的示例,創建包含變長列的表,然後更新一行的變長列,使得超出原來頁的容量。然後觀察頁的轉發情況。
if OBJECT_ID('bigrows') is not null drop TABLE bigrows go CREATE TABLE bigrows ( a int IDENTITY , b varchar(1600), c varchar(1600)); GO INSERT INTO bigrows VALUES (REPLICATE('a', 1600), ''); INSERT INTO bigrows VALUES (REPLICATE('b', 1600), ''); INSERT INTO bigrows VALUES (REPLICATE('c', 1600), ''); INSERT INTO bigrows VALUES (REPLICATE('d', 1600), ''); INSERT INTO bigrows VALUES (REPLICATE('e', 1600), ''); GO SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc FROM sys.dm_db_database_page_allocations (db_id('test'), object_id('bigrows'), NULL, NULL, 'DETAILED'); go UPDATE bigrows SET c = REPLICATE('x', 1600) WHERE a = 3; GO SELECT allocated_page_file_id, allocated_page_page_id, 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,163,1) go
然後觀察Slot2的內容,發現記錄類型爲9個字節的轉發存根(Forwarding Stub)。
Slot 2, Offset 0xcfe, Length 9, DumpStyle BYTE Record Type =FORWARDING_STUB Record Attributes = Record Size = 9 Memory Dump @0x000000000B4AACFE 0000000000000000: 04a50000 00010000 00
轉發存根的16進制內容可心劃分成4個部分,每部分代表的含義如下:
04-a5000000-0100-0000
轉發存根標誌字節位-轉記錄所在的頁號-文件號-Slot編號
由於SQL Server採用的是Little-Endian字節序來組織字節存放的(也就說看到就是它在內存中存放的順序),所以我們要看到數據本來的順序和內容,還需要將之轉換成Big-Endian的字節序組織的樣子,即做一次高低位轉換,然後才能轉成10進製表示的內容。
高低位轉換後:04-000000a5-0001-000
轉10進制後:4-165-1-0
4就是表示這是一條轉發存根,轉發後記錄的位置是:文件1中165號頁內的Slot0上。也可以DBCC PAGE查看165號頁Slot0長什麼樣。可以看到Record Type = FORWARDED_RECORD,表示這是一條轉發記錄。行內容是大量的c和x字符。
Slot 0, Offset 0x60, Length 3229, DumpStyle BYTE
Record Type =FORWARDED_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
Record Size = 3229
Memory Dump @0x000000000B4AA060
轉發指針只存在於堆表上。一個轉發指針不會指向另一個轉發指針,如果轉發記錄再次被轉發,則原來的轉發指針會指向轉發記錄的新地址。一旦一個轉發指針被生成,它會一直存在。有些情況會轉發指針會被清除:
轉發記錄尺寸縮小了,並且原來的頁能夠存放得下它,它就會回到原來的頁,轉發指針被清除
收縮數據庫。數據文件收縮不會產生任何新的轉發指針,它會重新分配書籤,並且會刪除一些數據頁。如果這些頁包含轉發記錄和存根,會被重新組織到其它頁,從而消除轉發。
使用ALTER Table Rebuild重建堆表
轉發行被刪除
建立聚集索引,變成聚集索引表
原地(In Place)更新
原地更新是SQL Server的更新規則。原地更新只在原來的位置上修改受影響的字節內容。每更新一行就會向事務日誌寫一條記錄。如果表有更新觸發器或者行被標記爲複製,則更新一行,會寫兩條事務日誌記錄。先寫一條刪除,再寫一條插入記錄。原地更新發生的兩種情況:
不需要用到轉發指針的堆表更新
不需要修改聚集鍵的聚集索引
聚集索引鍵存放是有序的,當修改聚集索引鍵的值,但不影響其排序位置時,也會是原地更新。比如某表的聚集索引列Name的值包括:Allen,Bill,Charlie。如果將Bill更新爲Bily,是原地更新,將Bill更新爲David,就是非原地升級(可能新行還會存在當前的數據頁上)。
非原地更新
非原地更新發生在更新聚集索引的索引鍵時。更新會變成先刪除再插入兩個操作。更新索引鍵也有可能是混合更新,即有些行是原地更新,其它行是非原地更新。更新聚集索引鍵時,SQL Server會生成一個包含刪除和插入操作涉及到的所有行的列表。這個列表小的話就存在內存,大的話就存在tempdb。然後根據鍵值和操作符(刪除或者插入)對列表排序。接下來分種情況:
如果索引鍵值非唯一,則先刪除再插入。
如果索引鍵值唯一,則會將刪除和插入這兩個操作合併成一個更新操作。這樣更高效。
參考及引用:
《Mcrosoft SQL Server Internals》