堆表修改內幕

      堆的修改需要使用到PFS頁(PageFreeSpace)。PFS記錄着數據頁的空間使用情況。PFS頁上使用1個字節(Byte)表示一個頁的使用情況。一個PFS頁可以表示8088個數據頁,於是每8088個數據頁就會有一個PFS頁。一個數據文件的第二個頁就是PFS頁。PFS頁上1個字節的結構:

wKiom1XT2d-glvmvAAFJpl8-_Oc233.jpg

  • 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。然後根據鍵值和操作符(刪除或者插入)對列表排序。接下來分種情況:

  1. 如果索引鍵值非唯一,則先刪除再插入。

  2. 如果索引鍵值唯一,則會將刪除和插入這兩個操作合併成一個更新操作。這樣更高效。


參考及引用:

 《Mcrosoft SQL Server Internals》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章