我們都知道,Sql Server在一個數據量巨大的表中添加一個非空欄位是比較費心的,缺乏經驗的DBA或是開發人員甚至可能魯莽地直接添加導致阻塞相應業務,甚至可能因為資源欠缺造成實例的全局問題.當然這都是Sql 2008R2及以前版本的情況.在SQL2012中采用了新的實現方式.這里我將對比相應的實現方式給大家做個介紹.並簡單說明Sql Server早期版本添加非空列的方法.
添加非空欄位的實現方式
早期版本(Sql Server2008R2及以前)添加非空欄位(要求有默認值)是對表中的所有數據行依次修改調整
我們通過一個簡單的實例來看下
Sql 2008R2 SP2 Code
Create database tadnull go use tadnull go create table t2(id int not null identity (1,1),dystr varchar(20),fixstr char(30)); go set nocount on declare @batchSize int set @batchSize=1000 declare @i int set @i=0 while(@i<20000) begin if (@i%@batchSize=0) begin if (@@TRANCOUNT>0)COMMIT TRAN BEGIN TRAN end insert into t2(dystr,fixstr)values('aaa'+str(RAND()*100000000),'bbb'+str(RAND()*100000000)) set @i=@i+1 end if (@@TRANCOUNT>0)COMMIT TRAN dbcc ind(tadnull,t2,1) -----find a datapage pageid 21 dbcc traceon(3604) dbcc page('tadnull',1,21,3)-----view the datapage 21
通過DBCC PAGE我們打印其中的一個數據頁進行分析.
可以看到圖1-1當前數據頁的最后修改的日志記錄為m_lsn = (28:69:279)
數據頁中第一行數據(slot 0)頁偏移量0x60,行長度為58
圖1-1
添加非空欄位code
alter table t2 add tt int not null default '10' dbcc page('tadnull',1,21,3)-----view the datapage 21 select b.* from sys.system_internals_partitions a join sys.system_internals_partition_columns b on a.partition_id = b.partition_id where a.object_id = object_id('t2');-----查看行數修改情況
可以看到圖1-2添加非空欄位后,字節數據頁最后修改的LSN號由m_lsn = (28:69:279)變為了
m_lsn = (43:432:186),數據頁中首行長度也由58變為了62正好是一個int數據類型長度.
當然我們也可以看到添加過程中會有大量的key鎖出現.圖1-3
(可以通過DMV查看,或者Xevents,trace profiler等探究)
從特定的DMV中,我們看到新加列的修改行數為200000,為表的數據行數,這也反應的數據修改為逐行修改.圖1-4
圖1-2
圖1-3
圖1-4
由此我們可以看到,在sql2008R2及以前版本中添加非空欄位會對表中數據逐行操作.操作執行成本很高.
接下來我們再看看sql2012采用的新的實現方式.
Sql 2012 SP1 code
Create database tadnull go use tadnull create table t2(id int not null identity (1,1),dystr varchar(20),fixstr char(30)); go set nocount on declare @batchSize int set @batchSize=1000 declare @i int set @i=0 while(@i<200000) begin if (@i%@batchSize=0) begin if (@@TRANCOUNT>0)COMMIT TRAN BEGIN TRAN end insert into t2(dystr,fixstr)values('aaa'+str(RAND()*100000000),'bbb'+str(RAND()*100000000)) set @i=@i+1 end if (@@TRANCOUNT>0)COMMIT TRAN dbcc ind('tadnull','t2',1)--------find a data page (pageid 120) dbcc traceon(3604) dbcc page('tadnull',1,120,3)
通過DBCC PAGE我們打印其中的一個數據頁進行分析.
可以看到圖2-1當前數據頁的最后修改的日志記錄為 m_lsn = (33:155:157)
數據頁中第一行數據(slot 0)頁偏移量0x60,行長度為58
圖2-1
添加非空欄位code
alter table t2 add tt int not null default '10' dbcc page('tadnull',1, 120,3)-----view the datapage 120 select b.* from sys.system_internals_partitions a join sys.system_internals_partition_columns b on a.partition_id = b.partition_id where a.object_id = object_id('t2');-----查看行數修改情況
通過圖2-2可以看出,頁的最后修改的LSN記錄m_lsn = (33:155:157)行長度 Length 58與添加欄位前一致!這與sql2008R2截然不同.
通過圖2-3可以看出新增欄位的修改行數為0,這反應了添加新行的修改非逐行修改.
由以上兩個圖我們可以推斷出添加新的非空欄位並非在新數據頁上逐一操作,而是元數據級的操作.
感興趣讀者可以通過Xevent,Trace Profiler的鎖捕捉來進一步驗證.
圖2-2
圖2-3
在添加完后的打印數據頁120的內容中我們可以看到圖2-4中欄位號4名稱dd已經被添加,但其物理長度為0,及行中欄位值未添加到此數據頁上
圖2-4
問題來了,那么這個欄位就永遠不會去填充數據頁而只是標識嗎?
我們簡單修改一下120頁中的一條數據然后再打印此頁內容
修改CODE
update t2 set dystr='shanksgao1111' where id=1 checkpoint dbcc page('tadnull',1,120,3)
由於我得屏幕不能完全打印單頁中我們所需的內容,此頁沒截圖,直接輸出.截取其中需要信息
可以從紅色的字體中看到,此頁上的LSN變化為m_lsn = (157:336:2),第一行長度(id=1)變為了62(58+4)雖然我只是修改了變長欄位dystr但變長長度和以前一樣.但新添加的非空欄位的值也添加到此頁上來. Slot 0 Column 4 Offset 0x26 Length 4 Length (physical) 4.
******************************************打印數據頁內容******************************************
m_freeData = 7640 m_reservedCnt = 0 m_lsn = (157:336:2)
m_xactReserved = 0 m_xdesId = (0:0) m_ghostRecCnt = 0
m_tornBits = -1896544491 DB Frag ID = 1
Allocation Status
GAM (1:2) = ALLOCATED SGAM (1:3) = NOT ALLOCATED
PFS (1:1) = 0x64 MIXED_EXT ALLOCATED 100_PCT_FULL DIFF (1:6) = CHANGED
ML (1:7) = NOT MIN_LOGGED
Slot 0 Offset 0x1d9a Length 62
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
Record Size = 62
Memory Dump @0x000000002F5EBD9A
0000000000000000: 30002a00 01000000 62626220 20203838 39373839 0.*.....bbb 889789
0000000000000014: 30202020 20202020 20202020 20202020 20200a00 0 ..
0000000000000028: 00000400 0001003e 00736861 6e6b7367 616f3131 .......>.shanksgao11
000000000000003C: 3131 11
Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4
id = 1
Slot 0 Column 2 Offset 0x31 Length 13 Length (physical) 13
dystr = shanksgao1111
Slot 0 Column 3 Offset 0x8 Length 30 Length (physical) 30
fixstr = bbb 8897890
Slot 0 Column 4 Offset 0x26 Length 4 Length (physical) 4
dd = 10
******************************************打印數據頁內容******************************************
通過上面的實例我們可以看到,在sql2012中,當我們添加一個非空欄位時,實際對相應數據頁上的操作並非逐一修改添加.因此執行速度非常快,對線上業務影響很低.但由於其並非實際填充數據頁使的Sql Server在應用,維護數據頁時進行額外的工作.對此Sql Server采用了較為平滑的處理方式.當更新某(些)行時,再進行頁中數據填充,降低了影響粒度.更友好地滿足了線上業務的需求.
注:此方式添加新的非空欄位時不包括Blob數據類型.
添加行版本戳(rowversion/ timestamp)不支持.
早期版本調整方式
我們可以看到sql2012的改進,但早期版本如果維護該如何操作?
早期版本中添加非空欄位針對數據量較小的表還好,但是針對大表由於其添加非空欄位的實現特性,致使阻塞發生的幾率明顯加大.因此針對大表我們必須謹慎進行.
a 如果有能調整的時間窗口則可以先預估下執行時間(相似環境中測試)然后時間窗口中操作.
b 如果沒有可供調整的時間窗口而需求又看似急茬,這樣我們可以采用如下步驟循序漸進的調整降低其影響的粒度.
添加非空欄位
1 添加一個帶默認值得可為空欄位
alter table t2 add col5 int default 100
2 添加一個約束使其col5不可為空,但不檢查現有數據
ALTER TABLE t2 WITH NOCHECK ADD CONSTRAINT CK_NOTNULL CHECK (col5 IS NOT NULL)
3 輪詢批量修改現有數據,更改現有col5為默認值
WHILE 1=1 BEGIN BEGIN TRAN UPDATE TOP(2000) t2 SET col5 = 100 WHERE col5 IS NULL IF @@rowcount < 2000 BEGIN COMMIT TRAN BREAK; END COMMIT TRAN END-------------限制數據量修改,盡量避免鎖升級
添加行版本戳
由上面的內容我們已經知道添加行版本戳(rowversion)時無法使用新特性的,那面對大表DBA該如何操作呢?如果有相應時間窗口那是最好,如果沒有,DBA需要和研發溝通利用sql server相應知識變向調整.
這里我借鑒與sql 2012 online添加非空欄位時類似的實現方式但需要研發同事配合
大表中添加行版本戳
1 創建一個與原表SCHEMA完全一致的表
2 創建一個視圖為原表union all 新表
3 需研發同事配合調整訪問實現方式
a 讀訪問從視圖
b 插入操作插入到新表
c 修改操作原表移動到新表
4 采用批量輪詢修改移動
5 移動完畢后調整表名稱(原表改隨意名,新表調整為原表名,告知研發調整,刪除視圖與原表)
如果研發無法配合調整,如此情況下就比較麻煩了.此時DBA需要一個時間窗口.
為了更迅捷地將相應欄位加上減小阻塞時間我們可以/盡量采取如下一系列的方式來縮短窗口時間.
1 選擇實例中相關對象負載最低的時間段進行
2 為執行操作提供盡量多的資源來確保其盡快完成.
如 調整加大buffer pool的大小
操作前騰出足夠的內存供其使用
Checkpoint
Dbcc dropcleanbuffers-----注意這些操作需保證實例應用在可接受的情況下進行
3 執行的會話中提前獲取表級排他鎖.(由於添加行版本戳本身也是逐行添加,會話中提前獲取表的排他鎖可以減小總共獲取鎖的次數進而加快執行速度)
begin tran ttt update t2 with(tablockx) set dystr='aaa' where 1=0 /*調整代碼*/ alter table t2 add rv rowversion commit tran ttt
