一行數據是如何來存儲的呢? 變長列與定長列,NULL與NOT NULL,實際是如何整理存放到 8k的數據頁上呢? 對錶格進行增減列,修改長度,添加預設值等DDL SQL,對行存儲結構又會有怎麼樣的影響呢? 什麼是大對象,什麼是行溢出,存儲引擎是如何處理它們呢? 如果轉載,請註明博文來源: www.c ...
一行數據是如何來存儲的呢? 變長列與定長列,NULL與NOT NULL,實際是如何整理存放到 8k的數據頁上呢? 對錶格進行增減列,修改長度,添加預設值等DDL SQL,對行存儲結構又會有怎麼樣的影響呢? 什麼是大對象,什麼是行溢出,存儲引擎是如何處理它們呢?
如果轉載,請註明博文來源: www.cnblogs.com/xinysu/ ,版權歸 博客園 蘇家小蘿蔔 所有。望各位支持!
1 引入
在一個DB內,每一個table都能在sys.sysobjects中找到對應的描述,每一個列,都能從sys.columns中找到說明。 這裡發個SQL是日常管理中使用到的,用於描述一個表格的數據結構情況。1 SELECT 2 3 表名 = CASE WHEN A.COLORDER=1 THEN D.NAME ELSE '' END, 4 表說明 = CASE WHEN A.COLORDER=1 THEN ISNULL(F.VALUE,'') ELSE '' END, 5 列序列號 = A.COLORDER, 6 列名 = A.NAME, 7 標識 = CASE WHEN COLUMNPROPERTY( A.ID,A.NAME,'ISIDENTITY')=1 THEN '√'ELSE '' END, 8 約束 = CASE WHEN EXISTS( 9 SELECT 1 10 FROM SYSOBJECTS 11 WHERE XTYPE='PK' AND PARENT_OBJ=A.ID AND NAME IN ( 12 SELECT 13 NAME 14 FROM SYSINDEXES 15 WHERE INDID IN( SELECT INDID FROM SYSINDEXKEYS WHERE ID = A.ID AND COLID=A.COLID ) 16 ) 17 ) THEN 'PK' 18 WHEN EXISTS ( 19 SELECT 1 FROM sys.foreign_key_columns 20 WHERE parent_object_id=A.ID AND parent_column_id=A.COLID 21 ) THEN 'FK'+'('+(SELECT OBJECT_NAME(referenced_object_id)+'.'+COL_NAME(referenced_object_id,referenced_column_id)+')' FROM sys.foreign_key_columns WHERE parent_object_id=A.ID AND parent_column_id=A.COLID) 22 ELSE '' END, 23 數據類型 = CASE WHEN B.NAME IN ('CHAR','NCHAR','VARCHAR','NVARCHAR') THEN B.NAME+'('+ISNULL(CAST(case when COLUMNPROPERTY(A.ID,A.NAME,'PRECISION')=-1 then null else COLUMNPROPERTY(A.ID,A.NAME,'PRECISION') end AS VARCHAR(10)),'MAX')+')' 24 WHEN B.NAME ='DECIMAL' THEN B.NAME+'('+CAST(COLUMNPROPERTY(A.ID,A.NAME,'PRECISION') AS VARCHAR(10))+','+CAST(ISNULL(COLUMNPROPERTY(A.ID,A.NAME,'SCALE'),0) AS VARCHAR(10))+')' 25 ELSE B.NAME END, 26 占用位元組長度 = A.LENGTH, 27 --長度 = COLUMNPROPERTY(A.ID,A.NAME,'PRECISION'), 28 --小數位數 = ISNULL(COLUMNPROPERTY(A.ID,A.NAME,'SCALE'),0), 29 允許空 = CASE WHEN A.ISNULLABLE=1 THEN '√'ELSE '' END, 30 預設值 = case when E.TEXT is not null then 31 32 case when substring(e.text,1,2)='((' then substring(e.text,3,len(e.text)-4) 33 when substring(e.text,1,1)='(' then substring(e.text,2,len(e.text)-2) 34 else e.text end 35 else '' end , 36 列說明 = ISNULL(G.[VALUE],'') 37 FROM SYSCOLUMNS A LEFT JOIN SYSTYPES B ON A.XUSERTYPE=B.XUSERTYPE 38 INNER JOIN SYSOBJECTS D ON A.ID=D.ID AND D.XTYPE='U' AND D.NAME<>'DTPROPERTIES' 39 LEFT JOIN SYSCOMMENTS E ON A.CDEFAULT=E.ID 40 LEFT JOIN sys.extended_properties G ON A.ID=G.major_id AND A.COLID=G.minor_id 41 LEFT JOIN sys.extended_properties F ON D.ID=F.major_id AND F.minor_id=0 42 WHERE D.NAME IN ('area','','') 43 ORDER BY A.ID,A.COLORDER查詢表結構SQL
2 數據行
2.1 數據行結構
數據行在數據頁面的存儲結構詳見下表,分為幾個部分:基礎信息4位元組、定長列相關、變長列相關及null點陣圖。詳見下表。這部分的內容具體參考《SQL server技術內幕:存儲引擎》第6章。 參考下圖,一行數據的大小是這麼計算的:Row_Size=Fixed_Data_Size+Variable_Data_Size+Null_Bitmap+4 。 各個部分其實都比較好理解,狀態B位未使用,狀態A位,詳細描述如下。- 狀態位A:表示行屬性的點陣圖,1位元組,8bit
-
- Bit 0 位,版本信息
- Bits 1-3 位,行記錄類型
-
- 0,primary record,主記錄
- 1,forwarded record
- 2,forwarding stub
- 3,index record,索引記錄
- 4,blob或者行溢出數據
- 5,ghost索引記錄
- 6,ghost數據記錄
- Bit 4 位,NULL點陣圖
- Bit 5 位,表示行中有變長列
- Bit 6 位,保留
- Bit 7 位,ghost record(幽靈記錄)
- 列偏移矩陣
-
- 如果一個表格,沒有變長列,那麼這個表格則不需要列偏移矩陣
- 一個變長列,有一個列偏移矩陣,一個列偏移矩陣2個位元組,用於表示變長列中每個列的結束位置。
2.2 特殊情況(大對象、行溢出及forword)
2.2.1 大對象
text, ntext, image, nvarchar(max), varchar(max), varbinary(max), and xml這種數據列,稱為大對象列, 註意,變長數據類型nvarchar,varchar,varbinary只有當存儲內容大於8k才變為大對象列。 行不能跨頁,但是行的部分可以移出行所在的頁,因此行實際可能非常大。頁的單個行中的最大數據量和開銷是 8,060 位元組 (8 KB)。考慮大對象列極為占用空間,所以在一行數據的主記錄中,是不存儲大對象列的,僅存儲 16位元組 指向 大對象列實際存儲到LOB data頁面的位置。 比如,一個大對象列text,text列存儲5000的字元,其他列占用50個字元,如果是放在一起存儲的話,10行數據就需要10個page,掃描就需要10次IO;而如果不放在一次,一個IN-ROW-DATA page就能存儲這10行數據,text列單獨存放在 LOB data列,那麼,掃描這10行的主記錄,僅需要1次IO。所以,大對象列是不跟主記錄存儲在一起。 這樣,一個8k的數據頁,就能儘可能多的存儲主記錄,可以在查詢的時候,避免 大對象列占用主記錄空間,導致IO次數增增加。2.2.2 行溢出
超過 8,060 位元組的行大小限制可能會影響性能,因為 SQL Server 仍保持每頁 8 KB 的限制。當合併 varchar、nvarchar、varbinary、sql_variant 或 CLR 用戶定義類型的列超過此限制時,SQL Server 資料庫引擎 將把最大寬度的記錄列移動到 ROW_OVERFLOW_DATA 分配單元的另一頁上,然後在主記錄記錄一個24位元組的指針,用與描述 被移出的列 實際存儲位置。比如,一行數據總大小超過8k,那麼在insert的過程中,會把最大寬度的記錄移動到另外的數據頁面。 如果更新操作使記錄變長,大型記錄將被動態移動到另一頁。如果更新操作使記錄變短,記錄可能會移回 IN_ROW_DATA 分配單元中的原始頁。此外,執行查詢和其他選擇操作(例如,對包含行溢出數據的大型記錄進行排序或合併)將延長處理時間,因為這些記錄將同步處理,而不是非同步處理。 一行數據(不包括大對象列)總長度超過了8k,則會把最大寬度的列內容移動到ROW_OVERFLOW_DATA頁面上,主記錄上留下一個24位元組的指針 描述 被溢出挪走的列內容 實際存儲位置,這個稱為行溢出。2.2.3 forword
在一堆表內的一個數據頁面,存儲了N行數據,現在,其中一行數據的某一列發生修改,導致其列的長度加大,而剩餘的頁面空間無法存儲該列數據,那麼這個時候,就會把該列數據移動到新的 IN_ROW_DATA 頁面上,在主記錄留下一個 9個位元組的 指針,指向實際列的存儲位置,這個稱之為 forword。 forward的條件是:堆表、變長列、更新操作及其數據頁面剩餘空間不足存儲新列內容。 為什麼一定要是堆表呢?因為如果是聚集索引表格,遇到這種情況,數據頁會split,把一半的內容另外存儲到新的數據頁,由於聚集索引上的非聚集索引鍵值查詢根據是主鍵,所以split操作不會影響到非聚集索引,但是堆表的非聚集索引結構查找行是根據RID,如果也split,那麼所有非聚集索引都需要修改鍵值RID,故在堆表上,使用了forword。 為什麼是更新操作呢?因為如果是INSERT操作,一開始就出現空間不足的情況,它老早就跑路到新的數據頁上了,不會再空間不足的數據頁面坐INSERT操作。 比如,一行數據原本存儲在一個數據頁面中,但是update某一列,增大其存儲內容,發現該數據頁沒有空閑的空間可以存儲該列內容,該列則會forword到另外的數據頁IN_ROW_DATA存儲,主記錄留下一個9位元組的指針。3 測試存儲情況
測試思路- 先建立一個只有2列非空定長列的堆表,然後INSERT一行數據,檢查page頁面存儲內容
- 添加主鍵,檢查存儲頁面內容
- 增加一列:可空變長列
- 增加一列:非空變長列+預設值(分大對象和非大對象)
- 刪除無數據的列
- 刪除有數據的列
- 行溢出
- forword
3.1 堆表分析
create table tbrow(id int not null identity(1,1),name char(20) not null) insert into tbrow(name) select 'xinysu'; dbcc traceon(3604) dbcc ind('dbpage','tbrow',-1) --根據返回結果,判斷324為數據頁,如果不理解,請查看本系列第一篇博文 dbcc page('dbpage',1,324,3) 查看 `消息` 內容,可以看到 slot 0 存儲的行數據大小為21位元組,由於現在的 tbrow表格中,只有兩列 int 跟 char ,由於都是定長列,所有變長列的存儲模塊均為空,但是註意一點,即使整個表格都沒有允許Null的列,Null點陣圖仍然會占用一個位元組。 所以 該行記錄的長度=狀態A+狀態B+定長欄位長度+定長欄位內容+總烈屬+null點陣圖=1+1+2+(4+10)+2+1= 21 bytes。 根據行的16進位記錄:10001200 01000000 78696e79 73752020 2020020000,來詳細分析這行數據的存儲情況。先把這串字元按照位元組數區分,其中註意部分需要反序後再轉換十進位。詳細分析及推導見下圖。3.2 添加主鍵
alter table tbrow add constraint pk_tbrow primary key(id) dbcc traceon(3604) dbcc ind('dbpage','tbrow',-1) 可以看到,表格的IAM頁及數據頁全部都改變了,因為當一個堆表添加主鍵變為聚集索引表格的時候,需要重新組織數據頁,按照聚集索引的鍵值順序存儲,所以看到,整個數據頁存儲情況發生了變化。如果是一個大堆表添加聚集索引,那麼這是一個非常耗時及耗費IO、CPU的操作,並且會鎖表直到操作結束,需謹慎操作。 再次來分析現在的行記錄。 dbcc page('dbpage',1,311,3) 可以看到,數據行的內容並沒有發生變化,添加主鍵(聚集唯一索引),會重組整個表格的存儲順序,但是不會影響到行內的數據情況。3.3 增加一列:可空變長列
alter table tbrow add constraint pk_tbrow primary key(id) dbcc traceon(3604) dbcc ind('dbpage','tbrow',-1) dbcc page('dbpage',1,311,3) 這裡開始有趣了,發現,添加了一列可空可null的列後,行記錄16進位並沒有發生變化。對比如下。 /* 第一個行為堆表行記錄 第二個行為添加主鍵後的行記錄 第三個行為添加可空變長列後的行記錄 10001200 01000000 78696e79 73752020 2020020000 10001200 01000000 78696e79 73752020 2020020000 10001200 01000000 78696e79 73752020 2020020000 */ 即使表格有為null的列,有變長的列,但是,只有這些列上沒有值,是不會影響這一行的數據記錄的,這非常重要!因為意味著,給一個表格添加可為空的列時,存儲引擎不需要去修改表格內的行記錄存儲情況,只需要在數據字典上添加做變動即可,這需要獲取到表格的架構鎖,然後執行,這個執行速度非常快。 這一點的處理,跟MySQL的處理極為不一樣,雖然5.6添加了OnLine DDL,避免了DDL期間對錶格鎖表影響,但是處理添加列的時候,涉及表結構變動,需要新建臨時文件來存儲frm跟ibd文件,這是一個耗費IO的處理方式,詳細可查看之前博文:MySQL Online DDL的改進與應用 。3.4 增加一列:非空變長列+預設值
3.4.1 非大對象列
alter table tbrow add task varchar(20) not null default 'all A' ; dbcc traceon(3604) dbcc ind('dbpage','tbrow',-1) dbcc page('dbpage',1,311,3) 查看16進位的行記錄:10001200 01000000 78696e79 73752020 2020020000,發現與之前的是一樣的,查看表格內容,設置了NOT NULL帶預設值的列後,實際上,查詢出來 task列是有值存儲的,存儲內容為 'all A',但是查看16進位內容的時候,卻發現,這個數據頁內的行記錄存儲內容並沒有發生變化。 這是一個神奇的處理方式!為啥呢? 仔細查看page的解析內容,發現 :Slot 0 Column 4 Offset 0x0 Length 5 Length (physical) 0 。該列數據長度為5,但是,實際存儲長度為0,也就是這一列壓根沒有存儲在數據頁面中。 個人推測:當添加了NOT NULL列+預設值(非大對象列)的情況下,不對以往數據存儲記錄發生修改,但是在查詢的時候,會判斷該列是否有存儲數據,如果沒有則使用預設值顯示。 這樣有一個非常大的好處:節約存儲空間,不變更行記錄,DDL期間,無需對以往記錄做處理,僅需修改數據字典即可。 3.4.2 大對象列 alter table tbrow add descriptions text not null default 'i love sql server' ; dbcc traceon(3604) dbcc ind('dbpage','tbrow',-1) 單薄的表格,一行的記錄,因為添加了大對象列,來了個 LOB data的IAM頁 以及 LOB data的數據頁 。不過,這次僅分析主記錄數據頁面pageid=311。 --主記錄數據頁面pageid=311 dbcc page('dbpage',1,311,3) 依舊來分析下這行存儲記錄,原先長度都是21,為啥添加了一個 text帶預設值的列,長度就增加為50bytes呢? 這裡註意兩個地方:原先的 task列跟 description列。task列之前是實際不存儲數據內容的,但是現在存儲了數據內容,description大對象列並沒有存儲數據在主記錄中,而是存儲在另外的lob data數據頁中,在主記錄僅存儲 描述 該列具體位置內容,占16bytes。 所以 該行記錄的長度=狀態A+狀態B+定長欄位長度+定長欄位內容+總列數+null點陣圖+變長列數量+列偏移矩陣+變長數據內容=1+1+2+(4+10)+2+1+2+2*3+(5+16)= 50 bytes。 來看看這個16進位的字元串:30001200 01000000 78696e79 73752020 20200500 0403001d 00220032 80616c6c 20410000 d1070000 00004b01 00000100 0000,詳細分析這行數據的存儲情況。先把這串字元按照位元組數區分,詳細分析及推導見下圖。由此可以得到幾個推論:大對象的列NOT NULL+預設值,是在數據頁上實際存儲預設值的,而且會對錶格中的其他原本不存儲預設值的列造成影響,整個表格變成了把預設值實際存儲到數據頁面中去。當一個大表,需要增加一列大對象列NOT NULL+預設值時,會影響到表格裡面的每一行記錄,每行記錄都要增加一個16位元組的來描述 大對象列的存儲位置,同時,原本不存儲預設值的列,也會實際存儲預設值到數據頁面中,這是一個鎖表久耗費IO的操作,對於一個大表來說。 是不是發現自己 添加一個大對象列+預設值是一件可怕的事情?如果真有這種需求,而且還是個大表,請謹慎考慮。