這篇文章我想談下空值位圖掩碼(NULL bitmap mask) ,并揭開它的神秘面紗。空值位圖掩碼是在存儲引擎層為列是否存儲NULL值進行編碼。它是一個簡單的位圖掩碼,如果值是1的話,表示這列有NULL值,如果是0的話,表示這列有具體的值(非NULL值)。
這樣的解釋聽起來非常直接,但當我們進一步分析時,用這個方法還是有啟發性的。首先我們來看看數據行的組合結構,這是存儲引擎用來在磁盤上存儲記錄的結構。結構如下圖所示:
這個格式被稱為定長變量格式(FixedVar format),因為SQL Server總是先存儲定長列(像INT,CHAR),再存儲變長列(像VARCHAR)。從圖中我們可以看到,SQL Server以存儲2 bytes的狀態位開始,接著用2 bytes存儲由行頭到定長列結尾長度(包含所有定長列數據)。然后用2 bytes存儲列個數,緊隨其他的就是真正的空值位圖掩碼(NULL bitmap mask)。
所有的一切第一眼看起來都很合理,但我們再仔細看下的話,我們就開始思考,并且你可能會問:為什么SQL Server在每條數據行里存儲具體的列數?對于每條數據行,列數都不是一樣的么?為什么SQL Server要存儲這些冗余的數據?
第1個答案是非常簡單和有邏輯性的:SQL Server需要列數來計算用作實際空值位圖掩碼(NULL bitmap mask)的字節數。小于等于8列的表需要1 byte,9到16列需要 2 bytes,17到24列需要3 bytes,以此類推。明白了么?但在表里每條記錄的列數必須是一樣的!
我們來看第2個用實例分析的技術性正確答案:首先,你要知道空值位圖掩碼(NULL bitmap mask)是用在數據引擎級別,即當前記錄中的列數。這就是說SQL Server在物理行可以存儲不同數量的列。額,好像說的有點含糊不清…………物理數據行列數和表元數據層(sys.dolumns)里列數并不一致。這些也是SQL Server內部的真正不同層級。
因此在什么情況下這些層級間會彼此不相等呢?很簡單:當你往表里增加列的時候!如果你加的列是NULL還是NOT NULL,SQL Server會作出完全不同的區別。當你增加一個新的NULL列到表時,SQL Server只更新表元數據層,一點也不接觸到存儲引擎層。也就是說當你增加一個NULL列時,所有的記錄物理存儲上不發生任何改變。另一方面,當你增加一個NOT NULL列時,SQL Server會更新表元數據層,同時也會更新存儲引擎層,這就是說,SQL Server會接觸并重寫表里的每一條記錄,在那里你增加了一個NOT NULL列。這會帶來性能上的巨大區別!因此SQL Server需要在每條數據記錄里存儲具體的列數,因為這里的列數不能和表元數據層的列數同步。
我們來拿具體的例子來詳細分析下。這個例子我們創建了一個簡單的含8列的表,SQL Server需要使用1 bytes來作為空值位圖掩碼(NULL bitmap mask)。
1 CREATE TABLE TestTable 2 ( 3 Column1 INT IDENTITY(1, 1) NOT NULL, 4 Column2 CHAR(600) NOT NULL, 5 Column3 CHAR(600) NOT NULL, 6 Column4 CHAR(600) NOT NULL, 7 Column5 CHAR(600) NOT NULL, 8 Column6 VARCHAR(600) NOT NULL, 9 Column7 VARCHAR(600) NOT NULL,10 Column8 VARCHAR(600) NOT NULL11 )12 GO
然后,我們往表里插入2條記錄:
1 INSERT INTO TestTable VALUES 2 ( 3 REPLICATE('2', 600), 4 REPLICATE('3', 600), 5 REPLICATE('4', 600), 6 REPLICATE('5', 600), 7 REPLICATE('6', 600), 8 REPLICATE('7', 600), 9 REPLICATE('8', 600)10 ),11 (12 REPLICATE('2', 600),13 REPLICATE('3', 600),14 REPLICATE('4', 600),15 REPLICATE('5', 600),16 REPLICATE('6', 600),17 REPLICATE('7', 600),18 REPLICATE('8', 600)19 )20 GO
我們通過DBCC PAGE命令查看下具體的數據頁:
1 DBCC IND(ALLOCATIONDB, TestTable, -1)2 GO
1 DBCC TRACEON(3604)2 GO3 DBCC PAGE(ALLOCATIONDB, 1, 24993, 1)4 GO5 6 DBCC TRACEON(3604)7 GO8 DBCC PAGE(ALLOCATIONDB, 1, 24995, 1)9 GO
可以看到,每條記錄的長度是 4129 bytes(4204 bytes 數據+ 7 bytes 行開銷+ 2 bytes 變長列個數 + 3 * 2 bytes 每個變長列結束位置的偏移量)。
現在我們往表里加一個新的NULL列:
1 ALTER TABLE TestTable ADD Column9 CHAR(600) NULL2 GO
這是表里的第9列,也就是說SQL Server對于這個列數需要2 bytes。但是 SQL Server并不在存儲引擎層改變物理數據行,因為我們只加了一個NULL列。SQL Server不需要在存儲引擎層做任何處理。我們可以通過查看數據頁來驗證下:
記錄還是同樣4219 bytes的長度,但是我們邏輯上已經在表上加了1列。現在我們來更新表的1條記錄,這樣的話,新加列就有具體值了:
1 UPDATE TestTable SET Column9 = REPLICATE('9', 600)2 WHERE Column1 = 13 GO
當你查看表里第2條記錄的數據頁時,記錄大小還是原來的4219 bytes。
1 DBCC TRACEON(3604)2 GO3 DBCC PAGE(ALLOCATIONDB, 1, 24995, 1)4 GO
你現在創建了一個場景:SQL Server在數據行內部存儲了不同長度的空值位圖掩碼(NULL bitmap mask)。這就是說你有定長列的表,在存儲引擎級別,卻有不同長度的行大小!很有趣,是不是?
現在我們刪除表并重建,繼續往表里插入2條記錄:
1 DROP TABLE dbo.TestTable 2 3 CREATE TABLE TestTable 4 ( 5 Column1 INT IDENTITY(1, 1) NOT NULL, 6 Column2 CHAR(600) NOT NULL, 7 Column3 CHAR(600) NOT NULL, 8 Column4 CHAR(600) NOT NULL, 9 Column5 CHAR(600) NOT NULL,10 Column6 VARCHAR(600) NOT NULL,11 Column7 VARCHAR(600) NOT NULL,12 Column8 VARCHAR(600) NOT NULL13 )14 GO15 16 INSERT INTO TestTable VALUES17 (18 REPLICATE('2', 600),19 REPLICATE('3', 600),20 REPLICATE('4', 600),21 REPLICATE('5', 600),22 REPLICATE('6', 600),23 REPLICATE('7', 600),24 REPLICATE('8', 600)25 ),26 (27 REPLICATE('2', 600),28 REPLICATE('3', 600),29 REPLICATE('4', 600),30 REPLICATE('5', 600),31 REPLICATE('6', 600),32 REPLICATE('7', 600),33 REPLICATE('8', 600)34 )35 GO
現在我們往表里增加一個NOT NULL列:
1 ALTER TABLE TestTable ADD Column9 CHAR(600) NOT NULL2 DEFAULT REPLICATE('9', 600)3 GO
現在,SQL Server需要在存儲引擎層改變每條記錄,因為新列的默認值必須被增加(當你表里已經有記錄存儲時,新加列必須要定義一個默認值),而且SQL Server需要擴展空值位圖掩碼(NULL bitmap mask)。
1 DBCC IND(ALLOCATIONDB, TestTable, -1) 2 GO 3 4 DBCC TRACEON(3604) 5 GO 6 DBCC PAGE(ALLOCATIONDB, 1, 24993, 1) 7 GO 8 9 DBCC TRACEON(3604)10 GO11 DBCC PAGE(ALLOCATIONDB, 1, 24995, 1)12 GO
當你處理大表,給表增加NOT NULL列時,這個現象會導致嚴重的性能問題。想象下我們往表里插入100萬條記錄。當我們增加NULL列時,SQL Server只需要幾毫秒,因為只進行元數據修改操作。但當我們往表里增加NOT NULL列時,SQL Server待ALTER TABLE操作完成需要花費40秒!在處理大表,往表里增加NOT NULL列,這的確是個非常嚴重的性能降級?。?/p>
希望你現在已經理解了為什么SQL Server在存儲引擎層對每條記錄存儲具體的列數,還有在SQL Server里,當你往大表里增加NOT NULL列,會出現嚴重的性能問題。
新聞熱點
疑難解答